eduevidence 5.2.0 → 6.0.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 (276) hide show
  1. package/README.md +54 -42
  2. package/README.zh-CN.md +51 -28
  3. package/SKILL.md +390 -133
  4. package/agents/openai.yaml +4 -0
  5. package/assets/readme/controlled-execution.svg +34 -0
  6. package/assets/readme/logo.png +0 -0
  7. package/assets/readme/research-workflow.svg +56 -0
  8. package/assets/readme/studio-graph.png +0 -0
  9. package/assets/readme/studio-overview.png +0 -0
  10. package/assets/readme/studio-reports.png +0 -0
  11. package/autoevolve/config.yaml +17 -0
  12. package/autoevolve/program.md +25 -0
  13. package/autoevolve/protected.manifest.yaml +34 -0
  14. package/benchmarks/adversarial/cases.jsonl +7 -0
  15. package/benchmarks/evidence-library.json +5268 -0
  16. package/benchmarks/partitions.json +8 -0
  17. package/docs/architecture.md +220 -0
  18. package/docs/autoresearch-evolution-plan.md +2903 -0
  19. package/docs/autoresearch-implementation-status.md +101 -0
  20. package/docs/demo-storyboard.md +20 -0
  21. package/docs/demo-workplace-ai.md +92 -0
  22. package/docs/demo.md +32 -0
  23. package/docs/install-guide.md +150 -0
  24. package/docs/orchestration-role-model.md +1254 -0
  25. package/docs/release-closeout/README.md +17 -0
  26. package/docs/release-closeout/frontend-acceptance.md +23 -0
  27. package/docs/release-closeout/issues.md +19 -0
  28. package/docs/release-closeout/verification.md +28 -0
  29. package/docs/release-contract.md +108 -0
  30. package/docs/research-studio-guide.zh-CN.md +166 -0
  31. package/eduevidence_cli.py +17 -11
  32. package/engine/_resources.py +13 -0
  33. package/engine/autoevolve/__init__.py +3 -0
  34. package/engine/autoevolve/agent_view.py +167 -0
  35. package/engine/autoevolve/core.py +357 -0
  36. package/engine/autoevolve/events.py +11 -0
  37. package/engine/autoevolve/git_workspace.py +77 -0
  38. package/engine/autoevolve/projection.py +23 -0
  39. package/engine/autoevolve/runner.py +413 -0
  40. package/engine/autoevolve/trust.py +146 -0
  41. package/engine/autoresearch/__init__.py +6 -0
  42. package/engine/autoresearch/commit.py +132 -0
  43. package/engine/autoresearch/contracts.py +126 -0
  44. package/engine/autoresearch/controller.py +207 -0
  45. package/engine/autoresearch/events.py +12 -0
  46. package/engine/autoresearch/gap_priority.py +168 -0
  47. package/engine/autoresearch/projection.py +30 -0
  48. package/engine/autoresearch/research_memory.py +59 -0
  49. package/engine/autoresearch/saturation.py +91 -0
  50. package/engine/briefs.py +2 -1
  51. package/engine/capabilities.py +1 -0
  52. package/engine/contracts.py +3 -1
  53. package/engine/evidencecore.py +7 -5
  54. package/engine/gaps.py +90 -51
  55. package/engine/judge_pack.py +65 -0
  56. package/engine/library_builtin.py +3 -1
  57. package/engine/living.py +2 -1
  58. package/engine/meta_synthesis.py +3 -1
  59. package/engine/orchestration.py +460 -0
  60. package/engine/pilot.py +2 -1
  61. package/engine/project.py +2 -2
  62. package/engine/research_service.py +113 -0
  63. package/engine/studio_read_model.py +400 -0
  64. package/engine/tribunal.py +1 -2
  65. package/engine/update.py +1 -0
  66. package/engine/versions.py +1 -1
  67. package/engine/worker_result.py +109 -0
  68. package/engine/workflows.py +70 -0
  69. package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +1720 -0
  70. package/examples/ai-coding-assistant-evidence/artifact_manifest.json +15 -0
  71. package/examples/ai-coding-assistant-evidence/citation_check.json +79 -0
  72. package/examples/ai-coding-assistant-evidence/claims.jsonl +12 -0
  73. package/examples/ai-coding-assistant-evidence/evaluation.json +35 -0
  74. package/examples/ai-coding-assistant-evidence/evidence.jsonl +12 -0
  75. package/examples/ai-coding-assistant-evidence/frame.json +48 -0
  76. package/examples/ai-coding-assistant-evidence/intervention.json +51 -0
  77. package/examples/ai-coding-assistant-evidence/methodology.json +36 -0
  78. package/examples/ai-coding-assistant-evidence/raw_verdict.json +86 -0
  79. package/examples/ai-coding-assistant-evidence/report_spec.json +219 -0
  80. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +2614 -0
  81. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +2614 -0
  82. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +2614 -0
  83. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +2614 -0
  84. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +2614 -0
  85. package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +1720 -0
  86. package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +1720 -0
  87. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +1720 -0
  88. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +1720 -0
  89. package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +1720 -0
  90. package/examples/ai-coding-assistant-evidence/result.json +1453 -0
  91. package/examples/ai-coding-assistant-evidence/result.zh.json +1453 -0
  92. package/examples/ai-coding-assistant-evidence/sources.jsonl +8 -0
  93. package/examples/ai-coding-assistant-evidence/verdict.json +103 -0
  94. package/examples/workplace-ai-assistant/claims.jsonl +4 -0
  95. package/examples/workplace-ai-assistant/evaluation.json +19 -0
  96. package/examples/workplace-ai-assistant/evidence.jsonl +4 -0
  97. package/examples/workplace-ai-assistant/evidence_graph.json +444 -0
  98. package/examples/workplace-ai-assistant/frame.json +41 -0
  99. package/examples/workplace-ai-assistant/intervention.json +27 -0
  100. package/examples/workplace-ai-assistant/legacy-link-check.json +16 -0
  101. package/examples/workplace-ai-assistant/methodology.json +60 -0
  102. package/examples/workplace-ai-assistant/report_spec.json +55 -0
  103. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +2484 -0
  104. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +2484 -0
  105. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +2484 -0
  106. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +2484 -0
  107. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +2484 -0
  108. package/examples/workplace-ai-assistant/result.json +553 -0
  109. package/examples/workplace-ai-assistant/result.zh.json +553 -0
  110. package/examples/workplace-ai-assistant/search_log.json +19 -0
  111. package/examples/workplace-ai-assistant/sources.jsonl +3 -0
  112. package/examples/workplace-ai-assistant/validation_result.json +9 -0
  113. package/examples/workplace-ai-assistant/verdict.json +52 -0
  114. package/install.sh +7 -7
  115. package/integrations/orchestration_dispatch.py +146 -0
  116. package/package.json +37 -3
  117. package/pyproject.toml +11 -20
  118. package/references/autoresearch.md +30 -0
  119. package/references/evaluation-policy.md +24 -0
  120. package/references/orchestration.md +22 -0
  121. package/references/scientific-invariants.md +19 -0
  122. package/retrieval/audit.py +154 -0
  123. package/schemas/intervention.schema.json +106 -21
  124. package/schemas/report-result.schema.json +9 -1
  125. package/schemas/v2/project.schema.json +2 -2
  126. package/schemas/v2/run.schema.json +1 -1
  127. package/schemas/vNext/autoevolve-session.schema.json +1 -0
  128. package/schemas/vNext/eval-snapshot.schema.json +1 -0
  129. package/schemas/vNext/execution-plan.schema.json +1 -0
  130. package/schemas/vNext/gap-priority.schema.json +1 -0
  131. package/schemas/vNext/negative-search-record.schema.json +1 -0
  132. package/schemas/vNext/research-iteration.schema.json +1 -0
  133. package/schemas/vNext/research-strategy.schema.json +1 -0
  134. package/schemas/vNext/skill-experiment.schema.json +1 -0
  135. package/schemas/vNext/task-spec.schema.json +1 -0
  136. package/schemas/vNext/worker-result.schema.json +1 -0
  137. package/scripts/benchmark_judge.py +2 -2
  138. package/scripts/benchmark_v3.py +26 -43
  139. package/scripts/build_esl_artifacts.py +2 -2
  140. package/scripts/build_evidence_library.py +2 -2
  141. package/scripts/build_gh_pages.py +98 -0
  142. package/scripts/build_readme_diagrams.py +72 -0
  143. package/scripts/build_report_variants.py +85 -0
  144. package/scripts/check_autoresearch_invariants.py +95 -0
  145. package/scripts/daily_evolve.py +30 -0
  146. package/scripts/dashboard_server.py +130 -101
  147. package/scripts/did_regression.py +5 -30
  148. package/scripts/enrich_projects_human_and_lieflat.py +1 -1
  149. package/scripts/generate_metrics.py +4 -3
  150. package/scripts/generate_new_projects.py +1 -1
  151. package/scripts/orchestrator.py +172 -18
  152. package/scripts/rebake_all_5themes.py +1 -2
  153. package/scripts/research_auto_cli.py +475 -0
  154. package/scripts/run_workspace.py +17 -7
  155. package/scripts/search_provenance.py +64 -0
  156. package/scripts/serve_web.py +9 -10
  157. package/scripts/skill_lint.py +1 -1
  158. package/scripts/skill_payload.py +78 -0
  159. package/scripts/validate_schema.py +15 -1
  160. package/scripts/vnext_cli.py +133 -0
  161. package/setup.py +12 -0
  162. package/skill/roles/registry.yaml +45 -0
  163. package/skill/sub-skills/report-generation/SKILL.md +12 -6
  164. package/skill/task-briefs/applicability.md +3 -0
  165. package/skill/task-briefs/projection.md +3 -0
  166. package/skill/workflows/decision-and-pilot.md +10 -0
  167. package/skill/workflows/evaluate-and-update.md +10 -0
  168. package/skill/workflows/evidence-review.md +13 -0
  169. package/visualization/eduevidence-report/assets/base.css +2 -2
  170. package/visualization/eduevidence-report/assets/reader.css +752 -0
  171. package/visualization/eduevidence-report/assets/reader.js +132 -0
  172. package/visualization/eduevidence-report/references/chart-selection-catalog.md +109 -0
  173. package/visualization/eduevidence-report/references/lieflat-composition.md +3 -1
  174. package/visualization/eduevidence-report/scripts/build_report.py +58 -65
  175. package/visualization/eduevidence-report/scripts/lieflat_engine.py +24 -100
  176. package/visualization/eduevidence-report/themes/academic.css +1 -1
  177. package/visualization/eduevidence-report/themes/claude.css +1 -1
  178. package/visualization/eduevidence-report/themes/datalab-dark.css +2 -2
  179. package/visualization/eduevidence-report/themes/datalab.css +2 -2
  180. package/visualization/eduevidence-report/themes/presentation.css +2 -2
  181. package/web/README.md +18 -0
  182. package/web/index.html +53 -0
  183. package/web/studio/THIRD_PARTY_LICENSES.txt +146 -0
  184. package/web/studio/assets/index-CzXocaGv.css +1 -0
  185. package/web/studio/assets/index-pa7jD7n4.js +230 -0
  186. package/web/studio/config.json +1 -0
  187. package/web/studio/index.html +14 -0
  188. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  189. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  190. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  191. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  192. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  193. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  194. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  195. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  196. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  197. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  198. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  199. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  200. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  201. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  202. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  203. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  204. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  205. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  206. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  207. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  208. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  209. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  210. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  211. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  212. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  213. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  214. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  215. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  216. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  217. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  218. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  219. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  220. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  221. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  222. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  223. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  224. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  225. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  226. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  227. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  228. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  229. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  230. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  231. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  232. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  233. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  234. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  235. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  236. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  237. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  238. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  239. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  240. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  241. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  242. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  243. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  244. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  245. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  246. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  247. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  248. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  249. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  250. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  251. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  252. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  253. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  254. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  255. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  256. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  257. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  258. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  259. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  260. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  261. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  262. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  263. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  264. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  265. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  266. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  267. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  268. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  269. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  270. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  271. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  272. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  273. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  274. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  275. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  276. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
@@ -34,8 +34,8 @@ THEME_PALETTES = {
34
34
  "presentation": {"bg": "#140A08", "card_bg": "#1C0D0A", "text": "#FBBF24", "subtext": "#D97706", "muted": "#9A3412", "grid": "#2D120B", "border": "#4A2218", "primary": "#F59E0B", "secondary": "#F24D29", "accent": "#10B981", "ribbon": "#2A140E"},
35
35
  }
36
36
 
37
- STAGGER_DOT = 12 # ms per dot (token range 8–15)
38
- STAGGER_BAR = 100 # ms per bar (token range 80–130)
37
+ STAGGER_DOT = 12
38
+ STAGGER_BAR = 100
39
39
  MIN_FONT = 6.5
40
40
 
41
41
 
@@ -77,9 +77,6 @@ def _fmt(v: float, digits: int = 2) -> str:
77
77
 
78
78
 
79
79
  class Audit:
80
- """Collects every displayed number as (origin, value) for the
81
- lieflat_data_bound integrity gate."""
82
-
83
80
  def __init__(self, out: Optional[list] = None):
84
81
  self.out: list[Tuple[str, Any]] = [] if out is None else out
85
82
 
@@ -96,10 +93,6 @@ def _svg_open(p: Dict[str, str], w: int, h: int, aria: str) -> List[str]:
96
93
  f'style="background:{p["card_bg"]};">']
97
94
 
98
95
 
99
- # ══════════════════════════════════════════════════════════════════════════
100
- # meta.forest — forest plot (Hedges' g + 95% CI)
101
- # ══════════════════════════════════════════════════════════════════════════
102
-
103
96
  def render_forest_plot(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
104
97
  p = get_theme(theme)
105
98
  A = Audit(audit)
@@ -122,7 +115,6 @@ def render_forest_plot(bundle: dict, theme: str, meta: dict, audit: Optional[lis
122
115
  out.append(f'<text x="{cx:.0f}" y="{head_y}" text-anchor="middle" font-size="10" font-weight="700" fill="{p["subtext"]}" {lf_fade(60)}>Hedges&#39; g [95% CI]</text>')
123
116
  out.append(f'<text x="{w - 24}" y="{head_y}" text-anchor="end" font-size="10" font-weight="700" fill="{p["subtext"]}" {lf_fade(90)}>Effect [95% CI]</text>')
124
117
  out.append(f'<line x1="20" y1="{head_y + 8}" x2="{w - 20}" y2="{head_y + 8}" stroke="{p["border"]}" stroke-width="1" {lf_draw(0)}/>')
125
- # zero line
126
118
  out.append(f'<line x1="{cx:.1f}" y1="{head_y + 8}" x2="{cx:.1f}" y2="{h - 56}" stroke="{p["muted"]}" stroke-dasharray="3,3" stroke-width="1" {lf_fade(120)}/>')
127
119
  row_y = head_y + 26
128
120
  for i, s in enumerate(studies):
@@ -145,11 +137,11 @@ def render_forest_plot(bundle: dict, theme: str, meta: dict, audit: Optional[lis
145
137
  out.append(f'<line x1="{xl:.1f}" y1="{row_y}" x2="{xu:.1f}" y2="{row_y}" stroke="{color}" stroke-width="1.5" {lf_draw(i * STAGGER_BAR + 120)}/>')
146
138
  out.append(f'<line x1="{xl:.1f}" y1="{row_y - 4}" x2="{xl:.1f}" y2="{row_y + 4}" stroke="{color}" stroke-width="1.5" {lf_fade(i * STAGGER_BAR + 140)}/>')
147
139
  out.append(f'<line x1="{xu:.1f}" y1="{row_y - 4}" x2="{xu:.1f}" y2="{row_y + 4}" stroke="{color}" stroke-width="1.5" {lf_fade(i * STAGGER_BAR + 140)}/>')
148
- out.append(f'<rect x="{x_pt - 3.5:.1f}" y="{row_y - 3.5:.1f}" width="7" height="7" fill="{color}" {lf_pop(i * STAGGER_BAR + 150)}><title>{esc(str(s["label"]))} — g = {g:+.2f}{f", N = {s["n"]:.0f}" if s.get("n") else ""}</title></rect>')
140
+ n_suffix = (", N = %.0f" % s["n"]) if s.get("n") else ""
141
+ out.append(f'<rect x="{x_pt - 3.5:.1f}" y="{row_y - 3.5:.1f}" width="7" height="7" fill="{color}" {lf_pop(i * STAGGER_BAR + 150)}><title>{esc(str(s["label"]))} — g = {g:+.2f}{n_suffix}</title></rect>')
149
142
  ci_txt = f'{g:+.2f} [{ci_l:+.2f}, {ci_u:+.2f}]' if ci_l is not None and ci_u is not None else f"{g:+.2f}"
150
143
  out.append(f'<text x="{w - 24}" y="{row_y + 4}" text-anchor="end" font-size="10" font-weight="800" fill="{color}" {lf_fade(i * STAGGER_BAR + 200)}>{ci_txt}</text>')
151
144
  row_y += 27
152
- # axis
153
145
  axis_y = h - 48
154
146
  out.append(f'<line x1="{cx - 0.34 * w:.0f}" y1="{axis_y}" x2="{cx + 0.34 * w:.0f}" y2="{axis_y}" stroke="{p["text"]}" stroke-width="1" {lf_draw(200)}/>')
155
147
  for tick in (-1.0, -0.5, 0.0, 0.5, 1.0):
@@ -165,10 +157,6 @@ def render_forest_plot(bundle: dict, theme: str, meta: dict, audit: Optional[lis
165
157
  return "\n".join(out)
166
158
 
167
159
 
168
- # ══════════════════════════════════════════════════════════════════════════
169
- # evidence.ranked_effects — L2 Dot Cascade
170
- # ══════════════════════════════════════════════════════════════════════════
171
-
172
160
  def render_dot_cascade(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
173
161
  p = get_theme(theme)
174
162
  A = Audit(audit)
@@ -204,10 +192,6 @@ def render_dot_cascade(bundle: dict, theme: str, meta: dict, audit: Optional[lis
204
192
  return "\n".join(out)
205
193
 
206
194
 
207
- # ══════════════════════════════════════════════════════════════════════════
208
- # evidence.year_x_dimension — L9 Bubble Almanac
209
- # ══════════════════════════════════════════════════════════════════════════
210
-
211
195
  def render_bubble_almanac(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
212
196
  p = get_theme(theme)
213
197
  A = Audit(audit)
@@ -247,10 +231,6 @@ def render_bubble_almanac(bundle: dict, theme: str, meta: dict, audit: Optional[
247
231
  return "\n".join(out)
248
232
 
249
233
 
250
- # ══════════════════════════════════════════════════════════════════════════
251
- # evidence.grouped_distribution — G15 Jitter Strip
252
- # ══════════════════════════════════════════════════════════════════════════
253
-
254
234
  def render_jitter_strip(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
255
235
  p = get_theme(theme)
256
236
  A = Audit(audit)
@@ -280,10 +260,6 @@ def render_jitter_strip(bundle: dict, theme: str, meta: dict, audit: Optional[li
280
260
  return "\n".join(out)
281
261
 
282
262
 
283
- # ══════════════════════════════════════════════════════════════════════════
284
- # evidence.multidim_top — L20 Parallel Coordinates
285
- # ══════════════════════════════════════════════════════════════════════════
286
-
287
263
  def render_parallel_coordinates(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
288
264
  p = get_theme(theme)
289
265
  A = Audit(audit)
@@ -329,16 +305,13 @@ def render_parallel_coordinates(bundle: dict, theme: str, meta: dict, audit: Opt
329
305
  if len(d_parts) < 2:
330
306
  continue
331
307
  title = f'{esc(str(r["label"]))} — ' + ", ".join(f'{ax["key"]}={r.get(ax["key"])}' for ax in axes)
332
- out.append(f'<polyline points="{" ".join(d_parts)}" fill="none" stroke="{p["primary"]}" stroke-width="1" opacity="0.45" {lf_draw(r_i * STAGGER_BAR + 120)}><title>{title}</title></polyline>')
308
+ points_text = " ".join(d_parts)
309
+ out.append(f'<polyline points="{points_text}" fill="none" stroke="{p["primary"]}" stroke-width="1" opacity="0.45" {lf_draw(r_i * STAGGER_BAR + 120)}><title>{title}</title></polyline>')
333
310
  out.append(f'<circle cx="{d_parts[0].split()[0]}" cy="{d_parts[0].split()[1]}" r="2.4" fill="{p["primary"]}" {lf_pop(r_i * STAGGER_BAR + 140)}/>')
334
311
  out.append("</svg>")
335
312
  return "\n".join(out)
336
313
 
337
314
 
338
- # ══════════════════════════════════════════════════════════════════════════
339
- # evidence.study_type_composition / wwc — L14 Hundred Field
340
- # ══════════════════════════════════════════════════════════════════════════
341
-
342
315
  def render_hundred_field(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
343
316
  p = get_theme(theme)
344
317
  A = Audit(audit)
@@ -363,10 +336,6 @@ def render_hundred_field(bundle: dict, theme: str, meta: dict, audit: Optional[l
363
336
  return "\n".join(out)
364
337
 
365
338
 
366
- # ══════════════════════════════════════════════════════════════════════════
367
- # evidence.study_type_composition / wwc — F4 Tick Donut
368
- # ══════════════════════════════════════════════════════════════════════════
369
-
370
339
  def render_tick_donut(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
371
340
  p = get_theme(theme)
372
341
  A = Audit(audit)
@@ -376,7 +345,6 @@ def render_tick_donut(bundle: dict, theme: str, meta: dict, audit: Optional[list
376
345
  out = _svg_open(p, w, h, meta.get("title", "tick donut"))
377
346
  cx, cy, r0, r1 = 200, 158, 88, 104
378
347
  colors = [p["primary"], p["secondary"], p["accent"], p["muted"]]
379
- # largest-remainder share of 100 ticks
380
348
  ticks_per = []
381
349
  assigned = 0
382
350
  for c_i, c in enumerate(cats):
@@ -385,13 +353,13 @@ def render_tick_donut(bundle: dict, theme: str, meta: dict, audit: Optional[list
385
353
  n = int(round(share))
386
354
  ticks_per.append((c_i, n))
387
355
  assigned += n
388
- while assigned > 100: # trim the largest over-assignment
356
+ while assigned > 100:
389
357
  c_i, n = max(ticks_per, key=lambda t: t[1])
390
358
  ticks_per[ticks_per.index((c_i, n))] = (c_i, n - 1)
391
359
  assigned -= 1
392
- while assigned < 100: # give the remainder to the largest category
393
- c_i, _ = max(ticks_per, key=lambda t: t[1])
394
- ticks_per[ticks_per.index((c_i, _))] = (c_i, _ + 1)
360
+ while assigned < 100:
361
+ c_i, n = max(ticks_per, key=lambda t: t[1])
362
+ ticks_per[ticks_per.index((c_i, n))] = (c_i, n + 1)
395
363
  assigned += 1
396
364
  tick_i = 0
397
365
  for c_i, n in ticks_per:
@@ -408,10 +376,6 @@ def render_tick_donut(bundle: dict, theme: str, meta: dict, audit: Optional[list
408
376
  return "\n".join(out)
409
377
 
410
378
 
411
- # ══════════════════════════════════════════════════════════════════════════
412
- # outcomes.direction_counts — F5 Tick Rows
413
- # ══════════════════════════════════════════════════════════════════════════
414
-
415
379
  def _dir_colors(p: Dict[str, str]) -> Dict[str, str]:
416
380
  return {"positive": p["secondary"], "negative": p["primary"], "null": p["muted"]}
417
381
 
@@ -448,10 +412,6 @@ def render_tick_rows(bundle: dict, theme: str, meta: dict, audit: Optional[list]
448
412
  return "\n".join(out)
449
413
 
450
414
 
451
- # ══════════════════════════════════════════════════════════════════════════
452
- # outcomes.direction_counts — F1 Rung Bars
453
- # ══════════════════════════════════════════════════════════════════════════
454
-
455
415
  def render_rung_bars(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
456
416
  p = get_theme(theme)
457
417
  A = Audit(audit)
@@ -461,8 +421,6 @@ def render_rung_bars(bundle: dict, theme: str, meta: dict, audit: Optional[list]
461
421
  colors = _dir_colors(p)
462
422
  x_start, x_end, y_base = 60, 516, 240
463
423
  bar_w = min(52, (x_end - x_start) / max(1, len(rows)) * 0.62)
464
- total_max = max((r["positive"] + r["negative"] + r["null"]) for r in rows) or 1
465
- rung_h = 5.5
466
424
  for r_i, r in enumerate(rows):
467
425
  cx = x_start + (r_i + 0.5) * (x_end - x_start) / len(rows)
468
426
  label = r.get("label_" + meta.get("lang", "en"), r.get("label_en", r["label"]))
@@ -474,8 +432,8 @@ def render_rung_bars(bundle: dict, theme: str, meta: dict, audit: Optional[list]
474
432
  for _ in range(count):
475
433
  if y_base - y > 118:
476
434
  break
477
- y -= rung_h + 2.2
478
- out.append(f'<rect x="{cx - bar_w / 2:.1f}" y="{y:.1f}" width="{bar_w:.1f}" height="{rung_h}" rx="2.6" fill="{colors[key]}" {lf_fade(r_i * STAGGER_BAR + shown * STAGGER_DOT + 60)}><title>{esc(str(label))} — {key} evidence</title></rect>')
435
+ y -= 7.7
436
+ out.append(f'<rect x="{cx - bar_w / 2:.1f}" y="{y:.1f}" width="{bar_w:.1f}" height="5.5" rx="2.6" fill="{colors[key]}" {lf_fade(r_i * STAGGER_BAR + shown * STAGGER_DOT + 60)}><title>{esc(str(label))} — {key} evidence</title></rect>')
479
437
  shown += 1
480
438
  if shown < count:
481
439
  out.append(f'<text x="{cx:.1f}" y="{y - 4:.1f}" text-anchor="middle" font-size="{MIN_FONT + 1.5}" font-weight="800" fill="{p["subtext"]}" {lf_fade(r_i * STAGGER_BAR + 500)}>+{count - shown}</text>')
@@ -486,10 +444,6 @@ def render_rung_bars(bundle: dict, theme: str, meta: dict, audit: Optional[list]
486
444
  return "\n".join(out)
487
445
 
488
446
 
489
- # ══════════════════════════════════════════════════════════════════════════
490
- # outcomes.paired_counts — F6 Paired Rungs
491
- # ══════════════════════════════════════════════════════════════════════════
492
-
493
447
  def render_paired_rungs(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
494
448
  p = get_theme(theme)
495
449
  A = Audit(audit)
@@ -506,6 +460,7 @@ def render_paired_rungs(bundle: dict, theme: str, meta: dict, audit: Optional[li
506
460
  for r_i, r in enumerate(rows):
507
461
  cx = 60 + (r_i + 0.5) * group_w
508
462
  label = r.get("label_" + lang, r.get("label_en", r["label"]))
463
+ last_y = y_base
509
464
  for col, key in ((0, "positive"), (1, "negative")):
510
465
  count = int(r.get(key) or 0)
511
466
  A.log(f"rows[{r_i}].{key}", count)
@@ -514,23 +469,20 @@ def render_paired_rungs(bundle: dict, theme: str, meta: dict, audit: Optional[li
514
469
  for _ in range(count):
515
470
  if y_base - y > 130:
516
471
  break
517
- y -= 5.5 + 2.2
472
+ y -= 7.7
518
473
  x = cx - col_w - 4 + col * (col_w + 8)
519
474
  out.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{col_w}" height="5.5" rx="2.6" fill="{colors[key]}" {lf_fade(r_i * STAGGER_BAR + shown * STAGGER_DOT + 60)}><title>{esc(str(label))} — {key}</title></rect>')
520
475
  shown += 1
476
+ last_y = min(last_y, y)
521
477
  if shown < count:
522
478
  out.append(f'<text x="{cx:.1f}" y="{y - 4:.1f}" text-anchor="middle" font-size="{MIN_FONT + 1.5}" font-weight="800" fill="{p["subtext"]}" {lf_fade(r_i * STAGGER_BAR + 500)}>+{count - shown}</text>')
523
479
  out.append(f'<text x="{cx:.1f}" y="{y_base + 16}" text-anchor="middle" font-size="{MIN_FONT + 1.5}" font-weight="600" fill="{p["subtext"]}" {lf_fade(r_i * STAGGER_BAR + 100)}>{esc(str(label)[:10])}</text>')
524
- out.append(f'<text x="{cx:.1f}" y="{y - 8:.1f}" text-anchor="middle" font-size="8" font-weight="800" fill="{p["text"]}" {lf_fade(r_i * STAGGER_BAR + 520)}>{r["positive"]} / {r["negative"]}</text>')
480
+ out.append(f'<text x="{cx:.1f}" y="{last_y - 8:.1f}" text-anchor="middle" font-size="8" font-weight="800" fill="{p["text"]}" {lf_fade(r_i * STAGGER_BAR + 520)}>{r["positive"]} / {r["negative"]}</text>')
525
481
  out.append(f'<line x1="46" y1="{y_base}" x2="512" y2="{y_base}" stroke="{p["text"]}" stroke-width="1.2" {lf_draw(120)}/>')
526
482
  out.append("</svg>")
527
483
  return "\n".join(out)
528
484
 
529
485
 
530
- # ══════════════════════════════════════════════════════════════════════════
531
- # outcomes.bipolar_axes — L7 Brand Spectrum
532
- # ══════════════════════════════════════════════════════════════════════════
533
-
534
486
  def render_brand_spectrum(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
535
487
  p = get_theme(theme)
536
488
  A = Audit(audit)
@@ -545,11 +497,12 @@ def render_brand_spectrum(bundle: dict, theme: str, meta: dict, audit: Optional[
545
497
  out.append(f'<text x="{x0 - 12}" y="62" text-anchor="end" font-size="9" font-weight="700" fill="{p["subtext"]}" {lf_fade(40)}>{esc(left_t)}</text>')
546
498
  out.append(f'<text x="{x1 + 12}" y="62" font-size="9" font-weight="700" fill="{p["subtext"]}" {lf_fade(80)}>{esc(right_t)}</text>')
547
499
  px = lambda t: x0 + t * (x1 - x0)
548
- ribbon = [f"{px(axes[0]['position']):.1f} {y0}"]
500
+ ribbon = [f"{px(axes[0]['position']):.1f} {y0}"] if axes else []
549
501
  for i in range(1, len(axes)):
550
502
  ribbon.append(f"C {px(axes[i-1]['position']):.1f} {y0 + (i-1)*gap + gap/2:.1f}, {px(axes[i]['position']):.1f} {y0 + i*gap - gap/2:.1f}, {px(axes[i]['position']):.1f} {y0 + i*gap}")
551
- ribbon_d = f"M {ribbon[0]} " + " ".join(ribbon[1:])
552
- out.append(f'<path d="{ribbon_d}" fill="none" stroke="{p["ribbon"]}" stroke-width="26" stroke-linecap="round" stroke-linejoin="round" opacity="0.95" {lf_draw(60)}/>')
503
+ if ribbon:
504
+ ribbon_d = f"M {ribbon[0]} " + " ".join(ribbon[1:])
505
+ out.append(f'<path d="{ribbon_d}" fill="none" stroke="{p["ribbon"]}" stroke-width="26" stroke-linecap="round" stroke-linejoin="round" opacity="0.95" {lf_draw(60)}/>')
553
506
  for i, ax in enumerate(axes):
554
507
  y = y0 + i * gap
555
508
  pos = float(ax["position"])
@@ -573,10 +526,6 @@ def render_brand_spectrum(bundle: dict, theme: str, meta: dict, audit: Optional[
573
526
  return "\n".join(out)
574
527
 
575
528
 
576
- # ══════════════════════════════════════════════════════════════════════════
577
- # intervention.phase_weeks — L3 Barcode Lollipop
578
- # ══════════════════════════════════════════════════════════════════════════
579
-
580
529
  def render_barcode_lollipop(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
581
530
  p = get_theme(theme)
582
531
  A = Audit(audit)
@@ -601,7 +550,7 @@ def render_barcode_lollipop(bundle: dict, theme: str, meta: dict, audit: Optiona
601
550
  out.append(f'<line x1="{x:.1f}" y1="{y_base}" x2="{x:.1f}" y2="{y:.1f}" stroke="{p["grid"]}" stroke-width="0.7" {lf_fade(60)}/>')
602
551
  is_peak = wk["week"] in peak_weeks
603
552
  out.append(f'<line x1="{x:.1f}" y1="{y:.1f}" x2="{x:.1f}" y2="{min(y_base, y + 12 + ((i * 7) % 12)):.1f}" stroke="{p["text"]}" stroke-width="{1.4 if is_peak else 0.9}" {lf_fade(min(i * STAGGER_BAR, 1200) + 80)}/>')
604
- out.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{4.8 if is_peak else 2.5:.1f}" fill="{p["primary"] if is_peak else p["text"]}" {lf_pop(min(i * STAGGER_DOT, 700) + 160)}><title>{esc(lang == "zh" and f"第 {wk['week']} 周 — 阶段 {phase}" or f"week {wk['week']} — phase {phase}")}</title></circle>')
553
+ out.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{4.8 if is_peak else 2.5:.1f}" fill="{p["primary"] if is_peak else p["text"]}" {lf_pop(min(i * STAGGER_DOT, 700) + 160)}/>')
605
554
  if is_peak:
606
555
  pk = next((q for q in peaks if q["week"] == wk["week"]), None)
607
556
  pk_txt = (pk or {}).get("label_zh" if lang == "zh" else "label_en", "")
@@ -616,10 +565,6 @@ def render_barcode_lollipop(bundle: dict, theme: str, meta: dict, audit: Optiona
616
565
  return "\n".join(out)
617
566
 
618
567
 
619
- # ══════════════════════════════════════════════════════════════════════════
620
- # intervention.activity_weights — L1 Launch Fan
621
- # ══════════════════════════════════════════════════════════════════════════
622
-
623
568
  def render_launch_fan(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
624
569
  p = get_theme(theme)
625
570
  A = Audit(audit)
@@ -647,10 +592,6 @@ def render_launch_fan(bundle: dict, theme: str, meta: dict, audit: Optional[list
647
592
  return "\n".join(out)
648
593
 
649
594
 
650
- # ══════════════════════════════════════════════════════════════════════════
651
- # intervention.phase_groups — L8 Dotty Matrix
652
- # ══════════════════════════════════════════════════════════════════════════
653
-
654
595
  def render_dotty_matrix(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
655
596
  p = get_theme(theme)
656
597
  A = Audit(audit)
@@ -661,6 +602,7 @@ def render_dotty_matrix(bundle: dict, theme: str, meta: dict, audit: Optional[li
661
602
 
662
603
  def P(c, r, k):
663
604
  return 236 + (c - r) * 17, 244 + (c + r) * 8.4 - k * 52
605
+
664
606
  for k in range(len(layers)):
665
607
  cs = [P(-0.6, -0.6, k), P(5.6, -0.6, k), P(5.6, 5.6, k), P(-0.6, 5.6, k)]
666
608
  poly_d = "M " + " L ".join(f"{x:.1f} {y:.1f}" for x, y in cs) + " Z"
@@ -679,10 +621,6 @@ def render_dotty_matrix(bundle: dict, theme: str, meta: dict, audit: Optional[li
679
621
  return "\n".join(out)
680
622
 
681
623
 
682
- # ══════════════════════════════════════════════════════════════════════════
683
- # decision.confidence_score — F11 Tick Gauge
684
- # ══════════════════════════════════════════════════════════════════════════
685
-
686
624
  def render_tick_gauge(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
687
625
  p = get_theme(theme)
688
626
  A = Audit(audit)
@@ -705,10 +643,6 @@ def render_tick_gauge(bundle: dict, theme: str, meta: dict, audit: Optional[list
705
643
  return "\n".join(out)
706
644
 
707
645
 
708
- # ══════════════════════════════════════════════════════════════════════════
709
- # methodology.flag_rates — L15 Ballot Tally
710
- # ══════════════════════════════════════════════════════════════════════════
711
-
712
646
  def render_ballot_tally(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
713
647
  p = get_theme(theme)
714
648
  A = Audit(audit)
@@ -730,17 +664,14 @@ def render_ballot_tally(bundle: dict, theme: str, meta: dict, audit: Optional[li
730
664
  x = 162 + t * 11
731
665
  if x > 470:
732
666
  break
733
- out.append(f'<rect x="{x:.1f}" y="{yc - 4:.1f}" width="8" height="8" rx="2" fill="{p["primary"] if t < flagged else p["grid"]}" {lf_pop(min(r_i * STAGGER_BAR + t * STAGGER_DOT, 1400))}><title>{esc(str(label))} — verdict {t + 1}/{total}{" (flagged)" if t < flagged else ""}</title></rect>')
667
+ flag_suffix = " (flagged)" if t < flagged else ""
668
+ out.append(f'<rect x="{x:.1f}" y="{yc - 4:.1f}" width="8" height="8" rx="2" fill="{p["primary"] if t < flagged else p["grid"]}" {lf_pop(min(r_i * STAGGER_BAR + t * STAGGER_DOT, 1400))}><title>{esc(str(label))} — verdict {t + 1}/{total}{flag_suffix}</title></rect>')
734
669
  out.append(f'<text x="512" y="{yc + 3:.1f}" text-anchor="end" font-size="9" font-weight="800" fill="{p["text"]}" {lf_fade(r_i * STAGGER_BAR + 300)}>{flagged}/{total}</text>')
735
670
  out.append(f'<text x="30" y="280" font-size="7.5" font-weight="600" fill="{p["muted"]}" {lf_fade(500)}>{esc(str(bundle.get("derived", ""))[:70])}</text>')
736
671
  out.append("</svg>")
737
672
  return "\n".join(out)
738
673
 
739
674
 
740
- # ══════════════════════════════════════════════════════════════════════════
741
- # evidence.year_x_outcome_counts — L16 Matrix Heat
742
- # ══════════════════════════════════════════════════════════════════════════
743
-
744
675
  def render_matrix_heat(bundle: dict, theme: str, meta: dict, audit: Optional[list] = None) -> str:
745
676
  p = get_theme(theme)
746
677
  A = Audit(audit)
@@ -772,15 +703,10 @@ def render_matrix_heat(bundle: dict, theme: str, meta: dict, audit: Optional[lis
772
703
  else:
773
704
  out.append(f'<rect x="{x + 2:.1f}" y="{y + 2:.1f}" width="{cell_w - 4:.1f}" height="{cell_h - 4}" rx="4" fill="{p["grid"]}" fill-opacity="0.5" {lf_fade((i + j) * STAGGER_DOT)}/>')
774
705
  out.append(f'<text x="{x + cell_w / 2:.1f}" y="{y + cell_h / 2 + 3:.1f}" text-anchor="middle" font-size="{MIN_FONT + 1.5}" font-weight="800" fill="{p["text"]}" {lf_pop(min((i + j) * STAGGER_DOT + 60, 900))}>{v}</text>')
775
- out.append(f'<title>{esc(str(label))} × {esc(str(years[j]))} — {v} studies</title>')
776
706
  out.append("</svg>")
777
707
  return "\n".join(out)
778
708
 
779
709
 
780
- # ══════════════════════════════════════════════════════════════════════════
781
- # Registry + dispatcher
782
- # ══════════════════════════════════════════════════════════════════════════
783
-
784
710
  REGISTRY: Dict[str, Dict[str, Any]] = {
785
711
  "forest_plot": {"catalog_ref": "FOREST-PLOT (publication figure)", "source": "meta.forest",
786
712
  "extractor": CD.extract_meta_forest, "renderer": render_forest_plot,
@@ -844,8 +770,6 @@ ACADEMIC_FIGURE_KEYS = ("outcome-comparison.svg", "benchmark-citation-support.sv
844
770
 
845
771
  def render_figure(fig_type: str, bundle: dict, theme: str, meta: dict,
846
772
  audit: Optional[list] = None) -> str:
847
- """Dispatch to the registered renderer. Unknown types raise ValueError —
848
- the caller records the reason and never silently falls back."""
849
773
  entry = REGISTRY.get(fig_type)
850
774
  if entry is None:
851
775
  raise ValueError(f"unregistered lieflat chart type: {fig_type!r}")
@@ -13,7 +13,7 @@
13
13
  :root[data-theme="academic"] body { font-size:15px; line-height:1.58; }
14
14
  :root[data-theme="academic"] .report-shell { max-width:980px; padding-top:22px; }
15
15
  :root[data-theme="academic"] .report-header { border-bottom:2px solid #222; margin-bottom:34px; }
16
- :root[data-theme="academic"] .report-header h1 { font-size:2rem; line-height:1.18; text-align:center; }
16
+ :root[data-theme="academic"] .report-header h1 { font-size:clamp(1.5rem,2.2vw,1.7rem); line-height:1.18; text-align:center; }
17
17
  :root[data-theme="academic"] .report-header .meta { text-align:center; }
18
18
  :root[data-theme="academic"] .report-section { background:transparent; border:0; box-shadow:none; padding:0; margin-bottom:34px; }
19
19
  :root[data-theme="academic"] .report-section>h2 { font-size:1.08rem; border-bottom:1px solid #444; border-radius:0; padding:0 0 5px; }
@@ -12,7 +12,7 @@
12
12
  }
13
13
  :root[data-theme="claude"] .report-shell { max-width:1240px; padding-top:34px; }
14
14
  :root[data-theme="claude"] .report-header { max-width:900px; margin:0 auto 42px; padding-bottom:22px; }
15
- :root[data-theme="claude"] .report-header h1 { font-size:clamp(1.9rem,4vw,3rem); line-height:1.18; font-weight:500; }
15
+ :root[data-theme="claude"] .report-header h1 { font-size:clamp(1.55rem,2.8vw,2.25rem); line-height:1.18; font-weight:500; }
16
16
  :root[data-theme="claude"] .report-section { background:transparent; border:0; box-shadow:none; padding:0; }
17
17
  :root[data-theme="claude"] .report-section>h2 { border:0; font-size:1.4rem; padding:0; margin-bottom:7px; }
18
18
  :root[data-theme="claude"] .section-lead { background:transparent; border:0; padding:0; max-width:760px; margin-bottom:24px; }
@@ -22,7 +22,7 @@
22
22
  width:100%; max-width:none; margin:0 0 22px; padding:18px 20px;
23
23
  border:1px solid var(--border); border-radius:12px; background:var(--surface); box-shadow:var(--shadow);
24
24
  }
25
- :root[data-theme="datalab-dark"] .report-header h1 { max-width:1100px; font-size:clamp(1.55rem,2.6vw,2.15rem); line-height:1.22; }
25
+ :root[data-theme="datalab-dark"] .report-header h1 { max-width:1100px; font-size:clamp(1.4rem,2vw,1.75rem); line-height:1.22; }
26
26
  :root[data-theme="datalab-dark"] .report-header .meta { margin:8px 0 0; font-size:.85rem; color:var(--text-muted); }
27
27
  :root[data-theme="datalab-dark"] .report-brand-row { margin-bottom:8px; }
28
28
  :root[data-theme="datalab-dark"] .report-brand { color:var(--primary); text-transform:uppercase; letter-spacing:.08em; font-size:.83rem; }
@@ -134,7 +134,7 @@
134
134
  :root[data-theme="datalab-dark"] .controls { width:calc(100% - 24px); padding:8px 10px; }
135
135
  :root[data-theme="datalab-dark"] .report-shell { padding-left:14px; padding-right:14px; }
136
136
  :root[data-theme="datalab-dark"] .report-header { padding:15px 14px; }
137
- :root[data-theme="datalab-dark"] .report-header h1 { font-size:1.45rem; }
137
+ :root[data-theme="datalab-dark"] .report-header h1 { font-size:1.3rem; }
138
138
  :root[data-theme="datalab-dark"] .brief-block { padding:15px 14px; }
139
139
  :root[data-theme="datalab-dark"] .brief-decision { padding:0; }
140
140
  :root[data-theme="datalab-dark"] .brief-decision .brief-block-header { padding:15px 14px 0; }
@@ -23,7 +23,7 @@
23
23
  width:100%; max-width:none; margin:0 0 22px; padding:18px 20px;
24
24
  border:1px solid var(--border); border-radius:12px; background:var(--surface); box-shadow:var(--shadow);
25
25
  }
26
- :root[data-theme="datalab"] .report-header h1 { max-width:1100px; font-size:clamp(1.55rem,2.6vw,2.15rem); line-height:1.22; }
26
+ :root[data-theme="datalab"] .report-header h1 { max-width:1100px; font-size:clamp(1.4rem,2vw,1.75rem); line-height:1.22; }
27
27
  :root[data-theme="datalab"] .report-header .meta { margin:8px 0 0; font-size:.85rem; }
28
28
  :root[data-theme="datalab"] .report-brand-row { margin-bottom:8px; }
29
29
  :root[data-theme="datalab"] .report-brand { color:var(--primary); text-transform:uppercase; letter-spacing:.08em; font-size:.83rem; }
@@ -138,7 +138,7 @@
138
138
  :root[data-theme="datalab"] .controls { width:calc(100% - 24px); padding:8px 10px; }
139
139
  :root[data-theme="datalab"] .report-shell { padding-left:14px; padding-right:14px; }
140
140
  :root[data-theme="datalab"] .report-header { padding:15px 14px; }
141
- :root[data-theme="datalab"] .report-header h1 { font-size:1.45rem; }
141
+ :root[data-theme="datalab"] .report-header h1 { font-size:1.3rem; }
142
142
  :root[data-theme="datalab"] .brief-block { padding:15px 14px; }
143
143
  :root[data-theme="datalab"] .brief-decision { padding:0; }
144
144
  :root[data-theme="datalab"] .brief-decision .brief-block-header { padding:15px 14px 0; }
@@ -31,7 +31,7 @@
31
31
  :root[data-theme="presentation"] .report-brand-row { margin-bottom:12px; }
32
32
  :root[data-theme="presentation"] .report-brand { color:var(--primary); text-transform:uppercase; letter-spacing:.1em; font-size:.8rem; }
33
33
  :root[data-theme="presentation"] .generated-theme-chip { background:#242A33; border-color:#3C4450; color:#B8C0CB; }
34
- :root[data-theme="presentation"] .report-header h1 { max-width:980px; font-size:clamp(1.5rem,2.6vw,2.1rem); line-height:1.08; letter-spacing:-.022em; }
34
+ :root[data-theme="presentation"] .report-header h1 { max-width:980px; font-size:clamp(1.35rem,2vw,1.7rem); line-height:1.08; letter-spacing:-.022em; }
35
35
  :root[data-theme="presentation"] .report-header .meta { margin:10px 0 0; color:var(--text-muted); font-size:.86rem; overflow-wrap:anywhere; }
36
36
  :root[data-theme="presentation"] .report-view-switcher { display:inline-flex; margin-top:18px; padding:4px; border:1px solid var(--border); border-radius:999px; background:#12151A; }
37
37
  :root[data-theme="presentation"] .report-view-btn { min-height:36px; border:0; background:transparent; color:#BBC2CC; padding:7px 15px; }
@@ -114,7 +114,7 @@
114
114
  :root[data-theme="presentation"] .controls { width:calc(100% - 24px); border-radius:10px; }
115
115
  :root[data-theme="presentation"] .report-shell { padding-left:14px; padding-right:14px; }
116
116
  :root[data-theme="presentation"] .report-header { padding:18px 16px; border-radius:12px; }
117
- :root[data-theme="presentation"] .report-header h1 { font-size:1.25rem; }
117
+ :root[data-theme="presentation"] .report-header h1 { font-size:1.2rem; }
118
118
  :root[data-theme="presentation"] .brief-block { padding:18px 16px; }
119
119
  :root[data-theme="presentation"] .brief-decision { padding:0; }
120
120
  :root[data-theme="presentation"] .brief-decision .decision-hero { min-height:0; padding:24px 18px; border-radius:13px; }
package/web/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # web/ — 前端分发与兼容边界
2
+
3
+ EduEvidence 当前正式控制台是 **Research Studio**。开发源与运行时分发物必须明确区分:
4
+
5
+ - **唯一权威可编辑控制台源**:`studio/`(React + TypeScript + Vite)。新功能、交互、可访问性、移动端和数据契约只在这里开发。
6
+ - **正式运行时控制台**:`web/studio/`。它是 `studio/` 的可复现构建产物,由 Python 本地服务、Skill 上架包和 GitHub Pages 直接提供;不要手工编辑其中的 bundle。
7
+ - `web/landing.html/.css/.js`:公开介绍页,保持独立视觉叙事;它只负责介绍和进入 Research Studio,不拥有研究状态。
8
+ - `web/index.html`、`web/js/`、`web/styles.css`:v5 时代三页控制台的**兼容层 / 历史实现**。现有兼容 API 或旧链接在确有需要时可以继续读取它们,但不得把它们当作新 UI 的开发入口,也不要向其中增加新的产品功能。
9
+ - `web/showcase_v2/`:冻结展示页。除安全或断链修复外不再与 Research Studio 同步演进。
10
+
11
+ ## 约定
12
+
13
+ 1. 新的 Research Studio 页面、组件和交互只能进入 `studio/src/`;禁止继续扩展旧 `web/js/` 控制台。
14
+ 2. `npm run build --prefix studio` 必须可复现 `web/studio/`;CI 使用 `git diff --exit-code -- web/studio` 守护这一点。
15
+ 3. 本地正式入口是 `http://127.0.0.1:<port>/studio/`;`dashboard_server.py` 的 `/`、`/console` 也应指向同一构建。
16
+ 4. GitHub Pages 根页保留介绍页,`/studio/` 是公开只读 Research Studio;静态导出只包含仓库示例,不包含本地研究项目、运行事件或 Autoevolve 会话。
17
+ 5. 五种报告主题属于 `visualization/eduevidence-report/` 的**生成时视觉身份**,不是 Research Studio 的运行时换肤;同一研究版本的科学内容不得因主题改变。
18
+ 6. 报告本体保持 single-file / zero-CDN;营销介绍页可以使用外部字体等展示资源,但不应把它们引入研究报告的离线能力声明。
package/web/index.html ADDED
@@ -0,0 +1,53 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>EduEvidence Research Studio · Build unavailable</title>
7
+ <style>
8
+ :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
9
+ * { box-sizing: border-box; }
10
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f7f6f3; color: #242321; padding: 24px; }
11
+ main { width: min(680px, 100%); background: #fff; border: 1px solid #dedbd5; border-radius: 18px; padding: clamp(28px, 6vw, 52px); box-shadow: 0 16px 55px rgba(42, 37, 31, .08); }
12
+ .eyebrow { margin: 0 0 18px; font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: #827d74; }
13
+ h1 { margin: 0; font-family: Georgia, "Times New Roman", serif; font-size: clamp(2rem, 7vw, 3.4rem); line-height: 1.02; font-weight: 500; letter-spacing: -.035em; }
14
+ p { margin: 22px 0 0; max-width: 56ch; line-height: 1.8; color: #625e57; }
15
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; background: #f2f0ec; border-radius: 6px; padding: .15em .4em; color: #393631; }
16
+ .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
17
+ a { display: inline-flex; align-items: center; min-height: 42px; padding: 0 15px; border-radius: 9px; border: 1px solid #ccc7bf; color: inherit; text-decoration: none; font-weight: 650; font-size: 14px; }
18
+ a.primary { background: #242321; color: #fff; border-color: #242321; }
19
+ small { display: block; margin-top: 30px; color: #8b867e; line-height: 1.65; }
20
+ @media (prefers-color-scheme: dark) {
21
+ body { background: #181817; color: #efede8; }
22
+ main { background: #211f1d; border-color: #3b3833; box-shadow: none; }
23
+ p, small, .eyebrow { color: #aaa49a; }
24
+ code { background: #302d29; color: #eeeae3; }
25
+ a { border-color: #504b44; }
26
+ a.primary { background: #efede8; color: #211f1d; border-color: #efede8; }
27
+ }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <main>
32
+ <p class="eyebrow">EduEvidence · Compatibility boundary</p>
33
+ <h1>Research Studio build unavailable.</h1>
34
+ <p>
35
+ 旧版 v5 三页控制台已经退出正式产品路径。看到这个页面说明当前分发目录缺少
36
+ <code>web/studio/</code> 的可复现构建产物;服务不会再静默降级到旧控制台。
37
+ </p>
38
+ <p>
39
+ 开发仓库请运行 <code>npm ci --prefix studio</code> 与
40
+ <code>npm run build --prefix studio</code>,然后重新启动本地 Python 服务。
41
+ 正常入口始终是 <code>/studio/</code>。
42
+ </p>
43
+ <div class="actions">
44
+ <a class="primary" href="/studio/">重新检查 Research Studio</a>
45
+ <a href="/api/studio/catalog">检查只读数据接口</a>
46
+ </div>
47
+ <small>
48
+ This page is intentionally a fail-closed compatibility tombstone. It contains no research UI,
49
+ no legacy dashboard runtime, and no scientific fallback data.
50
+ </small>
51
+ </main>
52
+ </body>
53
+ </html>