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
@@ -0,0 +1,132 @@
1
+ /* Offline reader: navigation and display only; no research state mutations. */
2
+ (function () {
3
+ "use strict";
4
+ var root = document.documentElement;
5
+ var copy = {
6
+ zh: {
7
+ brief: "\u6458\u8981",
8
+ full: "\u5b8c\u6574\u62a5\u544a",
9
+ print: "\u6253\u5370",
10
+ matched: "\u6761\u8bc1\u636e",
11
+ },
12
+ en: {
13
+ brief: "Brief",
14
+ full: "Full report",
15
+ print: "Print",
16
+ matched: "findings",
17
+ },
18
+ };
19
+ function labels() {
20
+ var lang = root.lang.indexOf("en") === 0 ? "en" : "zh";
21
+ document.querySelectorAll("[data-copy]").forEach(function (el) {
22
+ el.textContent = copy[lang][el.dataset.copy];
23
+ });
24
+ }
25
+ labels();
26
+ new MutationObserver(labels).observe(root, {
27
+ attributes: true,
28
+ attributeFilter: ["lang"],
29
+ });
30
+ document.querySelectorAll(".reader-print").forEach(function (b) {
31
+ b.addEventListener("click", function () {
32
+ window.print();
33
+ });
34
+ });
35
+ document.querySelectorAll(".reader-home").forEach(function (a) {
36
+ a.addEventListener("click", function (e) {
37
+ e.preventDefault();
38
+ window.scrollTo({ top: 0, behavior: "instant" });
39
+ });
40
+ });
41
+ var scheduled = false;
42
+ function progress() {
43
+ scheduled = false;
44
+ var max = document.documentElement.scrollHeight - window.innerHeight;
45
+ var fraction = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
46
+ var bar = document.querySelector(".reader-progress>span");
47
+ if (bar) bar.style.transform = "scaleX(" + fraction + ")";
48
+ var shell = Array.from(document.querySelectorAll(".report-shell")).find(
49
+ function (s) {
50
+ return getComputedStyle(s).display !== "none";
51
+ },
52
+ );
53
+ if (!shell) return;
54
+ var blocks = Array.from(shell.querySelectorAll(".brief-block[id]"));
55
+ var current = blocks
56
+ .filter(function (b) {
57
+ return b.getBoundingClientRect().top < 150;
58
+ })
59
+ .pop();
60
+ shell.querySelectorAll(".brief-navigation a").forEach(function (a) {
61
+ var active = current && a.hash === "#" + current.id;
62
+ a.classList.toggle("active", !!active);
63
+ if (active) a.setAttribute("aria-current", "location");
64
+ else a.removeAttribute("aria-current");
65
+ });
66
+ }
67
+ function queue() {
68
+ if (!scheduled) {
69
+ scheduled = true;
70
+ requestAnimationFrame(progress);
71
+ }
72
+ }
73
+ window.addEventListener("scroll", queue, { passive: true });
74
+ window.addEventListener("resize", queue);
75
+ document.addEventListener("click", queue);
76
+ progress();
77
+ document.querySelectorAll(".report-view-btn").forEach(function (btn) {
78
+ btn.addEventListener("click", function () {
79
+ window.scrollTo({ top: 0, behavior: "instant" });
80
+ });
81
+ });
82
+ document
83
+ .querySelectorAll(".table-wrap,.evidence-to-action")
84
+ .forEach(function (el) {
85
+ el.tabIndex = 0;
86
+ el.setAttribute("role", "region");
87
+ el.setAttribute(
88
+ "aria-label",
89
+ root.lang.indexOf("en") === 0
90
+ ? "Scrollable data table"
91
+ : "\u53ef\u6eda\u52a8\u6570\u636e\u8868",
92
+ );
93
+ });
94
+ document
95
+ .querySelectorAll('table[id^="evidence-matrix-"]')
96
+ .forEach(function (table) {
97
+ var suffix = table.id.replace(/^evidence-matrix-/, "");
98
+ var counter = document.createElement("p");
99
+ counter.className = "reader-filter-count";
100
+ counter.setAttribute("role", "status");
101
+ counter.setAttribute("aria-live", "polite");
102
+ table.parentElement.insertAdjacentElement("beforebegin", counter);
103
+ function count() {
104
+ var rows = Array.from(table.querySelectorAll("tbody tr"));
105
+ var visible = rows.filter(function (r) {
106
+ return r.style.display !== "none";
107
+ }).length;
108
+ counter.textContent =
109
+ visible +
110
+ " / " +
111
+ rows.length +
112
+ " " +
113
+ copy[suffix.endsWith("en") ? "en" : "zh"].matched;
114
+ }
115
+ ["matrix-search-", "matrix-direction-", "matrix-outcome-"].forEach(
116
+ function (prefix) {
117
+ var input = document.getElementById(prefix + suffix);
118
+ if (input) {
119
+ input.addEventListener("input", count);
120
+ input.addEventListener("change", count);
121
+ }
122
+ },
123
+ );
124
+ count();
125
+ });
126
+ document
127
+ .querySelectorAll('a[href^="http:"] , a[href^="https:"]')
128
+ .forEach(function (a) {
129
+ a.target = "_blank";
130
+ a.rel = "noopener noreferrer";
131
+ });
132
+ })();
@@ -0,0 +1,109 @@
1
+ # Chart Selection Catalog — AI 选图与表格边界
2
+
3
+ 本目录是 EduEvidence 报告生成阶段的**自包含选图控制面**。AI 可以根据 `result.json` 的数据形状自由组合图表,但只能从运行时注册表中选择;AI 负责“选什么、为什么”,确定性渲染器负责“从哪里取数、怎么画”。
4
+
5
+ > 核心原则:**图负责模式识别,表负责精确核验。** 不为了“更像数据产品”把所有表格图表化,也不了“严谨”把所有模式藏在大表里。
6
+
7
+ ## 1. Table vs. chart policy
8
+
9
+ ### 必须保留表格 / 展开详情的内容
10
+
11
+ - `EvidenceMatrix`:Evidence ID / Outcome / Effect / Quality / Claim / Source。
12
+ - `SourceList`:来源标题、年份、authority、canonical location。
13
+ - `MethodologyPanel` / methodology details:审计项的原始判断与备注。
14
+ - `ClaimTrace`:Claim → Evidence → Outcome → Effect → Quality → Source 的精确链路。
15
+ - 任何用户需要逐行复核、复制、引用或追溯的记录。
16
+
17
+ 这些是审计面,不属于“可视化失败”。图表可以总结它们,但不能替代它们。
18
+
19
+ ### 优先使用图表的内容
20
+
21
+ - 效应量及 CI 的横向比较。
22
+ - 研究/证据在年份、结果维度、设计簻型上的分布。
23
+ - 正向 / 负向 / 零效应的构成或两极差异。
24
+ - 干预阶段、活动数量与时间分布。
25
+ - 单值置信度和方法学检查构成。
26
+ - 多维研究特征的模式发现。
27
+
28
+ ### 不画图的情况
29
+
30
+ - 数据不足以形成至少一个诚实的视觉关系。
31
+ - 维度单位不可比较或把不同 outcome 合并会制造假结论。
32
+ - 只有一条记录且图表没有比文本/表格增加信息。
33
+ - 缺失值会迫使渲染器填 `0`、补 CI、补趋势或推断因果方向。
34
+
35
+ 此时输出 `VisualizationSuppressed` / `LieflatSuppressed` 和原因,比凑图更正确。
36
+
37
+ ## 2. AI selection protocol
38
+
39
+ 对每个准备表达的**独立结论**执行:
40
+
41
+ 1. **识别数据形状**:g+CI、排序 g+N、年份×维度、逐条分布、100% 构成、方向计数、双极轴、阶段周区间、单值置信度、审计票数等。
42
+ 2. **召回候选**:从下表中找语义匹配且提取器有数据的候选。通常比较至少 3 个;如果有效候选本来只有 1–2 个,不得为了满足数量硬凑不合适图型。
43
+ 3. **逐项比较**:语义契合、单位诚实、标签容纳、阅读速度、移动端适配、与本批次图型是否重复、数据是否达到 Meaningful Visualization Gate。
44
+ 4. **记录淘汰原因**:例如“需要 CI,但当前只有点估计”“类别名过长,不适合竖排”“与上一张同为方向计数,信息重复”。
45
+ 5. **选择 ≤6 张**:每张图必须承担不同结论,优先轮廓多样性而非数量;同一数据形状不要堆多个近似图。
46
+ 6. **只写计划,不写数值**:AI 只写 `type / catalog_ref / title_zh+en / subtitle_zh+en / caption_zh+en / source / params`。所有数值由 `charts_data.py` 提取。
47
+ 7. **让渲染器最终裁决可画性**:注册表未收录、参数非法或数据不足时显式拒绝/抑制,不允许 AI 绕过。
48
+
49
+ ## 3. Registered chart catalog
50
+
51
+ | `type` | `catalog_ref` | 数据形状 / extractor source | 最适合表达 | 避免使用 |
52
+ |---|---|---|---|---|
53
+ | `forest_plot` | FOREST-PLOT (publication figure) | g + 95% CI · `meta.forest` | 多研究效应与不确定性、零线关系 | 无 CI 时不要伪造区间 |
54
+ | `dot_cascade` | L2 Dot Cascade | 排序 g + N · `evidence.ranked_effects` | 效应量梯队、快速看排序与差距 | outcome/单位不可比时不要混排 |
55
+ | `bubble_almanac` | L9 Bubble Almanac | 年份 × 维度 × 研究数 · `evidence.year_x_dimension` | 时间覆盖与研究密度 | 没有年份或二维结构时不用 |
56
+ | `jitter_strip` | G15 Jitter Strip | 分组逐研究 g · `evidence.grouped_distribution` | 分布、离散度、异常点 | 只有聚合均值时不用 |
57
+ | `parallel_coordinates` | L20 Parallel Coordinates | g / N / 质量 / 年份 · `evidence.multidim_top` | 同一批研究的多维差异 | 维度单位/缩放无法解释时不用 |
58
+ | `hundred_field` | L14 Hundred Field | 研穵类型 100% 构成 · `evidence.study_type_composition` | 离散单位组成、研究设计结构 | 总数过大且单位点失去意义时慎用 |
59
+ | `tick_donut` | F4 Tick Donut | WWC 100% 构成 · `evidence.wwc_composition` | 少类别质量/等级份额 | 类别过多或精确比较更重要时不用 |
60
+ | `tick_rows` | F5 Tick Rows | outcome 方向计数 · `outcomes.direction_counts` | 长类目下的正/负/零分布 | 样本点太少且文本更清楚时不用 |
61
+ | `rung_bars` | F1 Rung Bars | outcome 方向计数 · `outcomes.direction_counts` | 紧凑方向计数比较 | 长中文类目或移动端拥挤时优先 tick_rows |
62
+ | `paired_rungs` | F6 Paired Rungs | 正/负成对计数 · `outcomes.paired_counts` | 每 outcome 正负直接对照 | 需要展示大量 null 时不优先 |
63
+ | `brand_spectrum` | L7 Brand Spectrum | 双极位置 · `outcomes.bipolar_axes` | “收益 ↔ 风险/退化”的方向张力 | 不得把 claim support 当正效应 |
64
+ | `barcode_lollipop` | L3 Barcode Lollipop | 周次 × 阶段 · `intervention.phase_weeks` | 干预阶段时间安排 | 没有真实周区间时不补时间点 |
65
+ | `launch_fan` | L1 Launch Fan | 阶段 × 活动权重 · `intervention.activity_weights` | 阶段工作量/活动构成 | “活动条数”不是强度时须在副标题说明 |
66
+ | `dotty_matrix` | L8 Dotty Matrix | 阶段 × 活动点阵 · `intervention.phase_groups` | 阶段结构和活动密度 | 不得把点数解释成效果大小 |
67
+ | `tick_gauge` | F11 Tick Gauge | 0–100% 单值 · `decision.confidence_score` | 已存在的决策置信度 | 不得从证据数量自行计算 confidence |
68
+ | `ballot_tally` | L15 Ballot Tally | 方法学检查计票 · `methodology.flag_rates` | 审计通过/风险项概览 | 不替代方法学原始备注 |
69
+ | `matrix_heat` | L16 Matrix Heat | 年份 × outcome × count · `evidence.year_x_outcome_counts` | 二维覆盖、证据空白和聚集区 | count 不代表效果强弱,标题需避免误导 |
70
+
71
+ ## 4. Recommended candidate families
72
+
73
+ 同一“问题”先在家族内比较,再跨家族避免重复:
74
+
75
+ - **效应与不确定性**:`forest_plot` / `dot_cascade` / `jitter_strip` / `parallel_coordinates`。
76
+ - **证据分布与覆盖**:`bubble_almanac` / `matrix_heat` / `hundred_field` / `tick_donut`。
77
+ - **结果方向**:`tick_rows` / `rung_bars` / `paired_rungs` / `brand_spectrum`。
78
+ - **干预结构**:`barcode_lollipop` / `launch_fan` / `dotty_matrix`。
79
+ - **决策与方法学摘要**:`tick_gauge` / `ballot_tally`。
80
+
81
+ 示例:如果目标是“不同 outcome 的正负证据差异”,先比较 `tick_rows`、`rung_bars`、`paired_rungs`、`brand_spectrum`;如果类目长且要保留 null,通常 `tick_rows` 更合适;如果核心是两极张力,`brand_spectrum` 更快;如果只想看正负并排,`paired_rungs` 更直接。
82
+
83
+ ## 5. Composition constraints
84
+
85
+ - 一份报告最多 6 张 AI-composed 图;少而准优先于填满 6 张。
86
+ - 每图对应一个明确问题或结论,不允许仅因“有数据”就画。
87
+ - 同一批次避免重复轮廓:例如已经用 `tick_rows` 讲方向分布,就不要再用 `rung_bars` 重复同一事实,除非第二张回答的是不同问题。
88
+ - 五种主题共享同一个 `visual_layout` 科学结构;主题只能改变版式、色板、表格密度和阅读节奏,不能改变数据、结论、图型含义或 Meaningful Visualization Gate。
89
+ - 报告中的 `EvidenceMatrix`、`SourceList`、方法学详情和 `ClaimTrace` 始终保留可审计入口。
90
+ - 图表数据必须通过 `lieflat_data_bound`;任何 AI 写入的数值都不进入渲染数据面。
91
+
92
+ ## 6. Output contract example
93
+
94
+ ```json
95
+ {
96
+ "type": "paired_rungs",
97
+ "catalog_ref": "F6 Paired Rungs",
98
+ "title_zh": "迁移与保持结果呈现更明显的正负分化",
99
+ "title_en": "Transfer and retention outcomes show the clearest directional split",
100
+ "subtitle_zh": "每格 = 1 条已提取证据 · 正向与负向并列",
101
+ "subtitle_en": "1 cell = 1 extracted finding · positive and negative shown side by side",
102
+ "caption_zh": "仅展示已记录的 effect_direction,不根据 claim relation 推断方向。",
103
+ "caption_en": "Uses recorded effect_direction only; claim relation never determines effect direction.",
104
+ "source": "outcomes.paired_counts",
105
+ "params": {}
106
+ }
107
+ ```
108
+
109
+ AI 选择图型后,仍由 `references/lieflat-composition.md`、`schemas/visual-layout.schema.json`、`charts_data.py` 与 `lieflat_engine.REGISTRY` 共同执行机械校验。
@@ -2,10 +2,12 @@
2
2
 
3
3
  EduEvidence 的 Lieflat 画廊是**数据驱动的手作图表组合**:上游 AI 按数据形状自由选型(只能选注册表内图型),渲染器从 `result.json` 提取真实数据渲染成主题化内联 SVG。AI 不直接写数值——所有数字经 `scripts/charts_data.py` 提取器出数,完整性门 `lieflat_data_bound` 逐值核对溯源。
4
4
 
5
- > 上游规范 = `visualization/lieflat-charts/`(Lupi 正本 + mono-tokens)。本文件只记录 EduEvidence 特有规则。
5
+ > AI 选图规范已内置于 `chart-selection-catalog.md`;本文件记录可执行注册表、数据契约与渲染不变量。任何外部图库只可作为设计灵感,不能成为运行时依赖。
6
6
 
7
7
  ## 1. 注册表(type ↔ 目录编号 ↔ 数据形状 ↔ 提取器)
8
8
 
9
+ 选图前先读 `chart-selection-catalog.md`:表格负责逐行审计,图表负责模式表达;AI 通常比较至少 3 个**可行**候选并记录淘汰理由,但候选不足时不得为凑数量选择错误图型。
10
+
9
11
  | `type` | 目录编号 | 数据形状 | 提取器(scripts/charts_data.py) |
10
12
  |---|---|---|---|
11
13
  | `forest_plot` | FOREST-PLOT (publication figure) | 逐研究 Hedges' g + 95% CI(meta.forest 或 evidence.effect_size) | `meta.forest` |
@@ -1555,55 +1555,15 @@ def first_screen(result: dict, lang: str, ui: dict) -> str:
1555
1555
  action = decision.get("recommended_action", "insufficient_evidence")
1556
1556
  cls = {"adopt": "adopt", "pilot": "pilot", "reject": "reject"}.get(action, "")
1557
1557
 
1558
- # 1. Strongest Supported Takeaway
1559
- if decision.get("strongest_support"):
1560
- strongest = decision.get("strongest_support")
1561
- elif can_claim:
1562
- strongest = can_claim[0]
1563
- elif supported_claims:
1564
- strongest = supported_claims[0]
1565
- else:
1566
- if lang == "zh":
1567
- strongest = f"即时编程任务编写耗时缩短 35%~50%,代码完成速度显著提升(综合效应量 g = +0.61, p < 0.001),在当堂受控实验中展现明显效率增益。"
1568
- else:
1569
- strongest = f"In-task programming completion time is shortened by 35%-50% with significant velocity gain (pooled g = +0.61, p < 0.001) in guided environments."
1570
-
1571
- # 2. Key Uncertainty / Contradictions
1572
- if decision.get("key_uncertainty"):
1573
- uncertainty = decision.get("key_uncertainty")
1574
- elif uncertain_claims:
1575
- uncertainty = uncertain_claims[0]
1576
- elif contradicted_claims:
1577
- uncertainty = contradicted_claims[0]
1578
- elif decision.get("reason_for_disagreement"):
1579
- uncertainty = decision.get("reason_for_disagreement")
1580
- else:
1581
- if lang == "zh":
1582
- uncertainty = "撤除 AI 后的独立闭卷期末考试与概念迁移表现显著下滑(综合效应量 g = -0.28, p = 0.012),学生存在‘看似学会、实则不会’的认知盲区。"
1583
- else:
1584
- uncertainty = "Delayed solo exams and independent transfer performance decline significantly (pooled g = -0.28, p = 0.012) once scaffolding is removed."
1585
-
1586
- # 3. Main Risk
1587
- if decision.get("main_risk"):
1588
- risk = decision.get("main_risk")
1589
- elif decision.get("risk_effect"):
1590
- risk = decision.get("risk_effect")
1591
- else:
1592
- if lang == "zh":
1593
- risk = "认知脚手架依赖陷阱(Scaffolding Dependency Trap):过度依赖实时代码补全导致学生自主调试排错、边界测试与底层计算思维出现退化。"
1594
- else:
1595
- risk = "Scaffolding Dependency Trap: Over-reliance on code completion degrades novice debugging, boundary testing, and fundamental computational thinking."
1596
-
1597
- # 4. Next Action / Policy Guidance
1598
- if decision.get("next_action"):
1599
- next_action = decision.get("next_action")
1600
- elif decision.get("next_steps"):
1601
- next_action = decision.get("next_steps")
1602
- else:
1603
- if lang == "zh":
1604
- next_action = "建议开展限制性教学试点:① 采用苏格拉底式概念引导,严禁直接给答案;② 实行 4 阶段脚手架渐进剥离;③ 坚持以无 AI 闭卷机试与独立随访作为最终考核标准。"
1605
- else:
1606
- next_action = "Execute restricted classroom pilot: ① Enforce Socratic guidance instead of direct code generation; ② Implement 4-phase scaffolding fading; ③ Anchor summative grading in unassisted closed-book exams."
1558
+ missing = "当前结果未提供此项信息。" if lang == "zh" else "Not provided in this research result."
1559
+ strongest = (decision.get("strongest_support") or (can_claim[0] if can_claim else None)
1560
+ or (supported_claims[0] if supported_claims else None) or missing)
1561
+ uncertainty = (decision.get("key_uncertainty")
1562
+ or (uncertain_claims[0] if uncertain_claims else None)
1563
+ or (contradicted_claims[0] if contradicted_claims else None)
1564
+ or decision.get("reason_for_disagreement") or missing)
1565
+ risk = decision.get("main_risk") or decision.get("risk_effect") or missing
1566
+ next_action = decision.get("next_action") or decision.get("next_steps") or missing
1607
1567
 
1608
1568
  rationale = decision.get("decision_rationale") or decision.get("rationale") or ""
1609
1569
  rationale_html = expandable_text(rationale, ui["expand_details"], 380, "hero-rationale")
@@ -2130,7 +2090,9 @@ def render_intervention(result: dict, svg: str, lang: str, ui: dict) -> str:
2130
2090
  intervention = result.get("intervention", {})
2131
2091
  if not intervention:
2132
2092
  return f"<p>{esc(ui['no_data'])}</p>"
2133
- lines = [f"<p><strong>{esc(ui['intervention_learners'])}{esc(ui['colon'])}</strong>{esc(intervention.get('target_learners'))} · "
2093
+ population = intervention.get("target_population") or intervention.get("target_learners")
2094
+ population_label = ("目标人群" if lang == "zh" else "Target population") if intervention.get("target_population") else ui['intervention_learners']
2095
+ lines = [f"<p><strong>{esc(population_label)}{esc(ui['colon'])}</strong>{esc(population)} · "
2134
2096
  f"<strong>{esc(ui['intervention_duration'])}{esc(ui['colon'])}</strong>{esc(intervention.get('pilot_duration'))}</p>"]
2135
2097
  if intervention.get("ai_usage_policy"):
2136
2098
  lines.append(f"<p><strong>{esc(ui['intervention_policy'])}{esc(ui['colon'])}</strong>{esc(intervention['ai_usage_policy'])}</p>")
@@ -2656,6 +2618,17 @@ def _lang_switcher(ui_zh: dict, ui_en: dict) -> str:
2656
2618
  "</div>")
2657
2619
 
2658
2620
 
2621
+ def script_json(value: object) -> str:
2622
+ """Serialize untrusted evidence/chart data for an inline script context."""
2623
+ return (json.dumps(value, ensure_ascii=False, allow_nan=False)
2624
+ .replace("&", "\\u0026").replace("<", "\\u003c").replace(">", "\\u003e")
2625
+ .replace("\u2028", "\\u2028").replace("\u2029", "\\u2029"))
2626
+
2627
+
2628
+ def _reader_asset(name: str) -> str:
2629
+ return (Path(__file__).resolve().parent.parent / "assets" / name).read_text(encoding="utf-8")
2630
+
2631
+
2659
2632
  def _enhancer_js(charts_zh: dict, charts_en: dict, result_en: dict) -> str:
2660
2633
  outcome_zh = next((c for c in charts_zh.get("charts", [])
2661
2634
  if c.get("chart_id") == "outcome-evidence-overview"), None)
@@ -2700,7 +2673,8 @@ def _enhancer_js(charts_zh: dict, charts_en: dict, result_en: dict) -> str:
2700
2673
  }}
2701
2674
  var savedLang = null;
2702
2675
  try {{ savedLang = localStorage.getItem('eduevidence-lang'); }} catch (e) {{}}
2703
- applyLang(savedLang === 'en' ? 'en' : 'zh');
2676
+ var queryLang = new URLSearchParams(window.location.search).get('lang');
2677
+ applyLang((queryLang || savedLang) === 'en' ? 'en' : 'zh');
2704
2678
  document.querySelectorAll('.lang-btn').forEach(function (btn) {{
2705
2679
  btn.addEventListener('click', function () {{ applyLang(btn.dataset.langTarget); }});
2706
2680
  }});
@@ -2721,7 +2695,8 @@ def _enhancer_js(charts_zh: dict, charts_en: dict, result_en: dict) -> str:
2721
2695
  }}
2722
2696
  var savedView = null;
2723
2697
  try {{ savedView = localStorage.getItem('eduevidence-report-view'); }} catch (e) {{}}
2724
- applyReportView(savedView === 'full' ? 'full' : 'brief');
2698
+ var queryView = new URLSearchParams(window.location.search).get('view');
2699
+ applyReportView((queryView || savedView) === 'full' ? 'full' : 'brief');
2725
2700
  document.querySelectorAll('.report-view-btn').forEach(function (btn) {{
2726
2701
  btn.addEventListener('click', function () {{ applyReportView(btn.dataset.reportView); }});
2727
2702
  }});
@@ -2803,12 +2778,12 @@ def _enhancer_js(charts_zh: dict, charts_en: dict, result_en: dict) -> str:
2803
2778
  window.eduevidenceCharts = window.eduevidenceCharts || {{}};
2804
2779
  window.eduevidenceCharts[containerId] = chart;
2805
2780
  }}
2806
- mountChart('chart-outcome-zh', {json.dumps(outcome_zh or {}, ensure_ascii=False)});
2807
- mountChart('chart-trace-zh', {json.dumps(trace_zh or {}, ensure_ascii=False)});
2808
- mountChart('chart-benchmark-zh', {json.dumps(benchmark_zh, ensure_ascii=False)});
2809
- mountChart('chart-outcome-en', {json.dumps(outcome_en or {}, ensure_ascii=False)});
2810
- mountChart('chart-trace-en', {json.dumps(trace_en or {}, ensure_ascii=False)});
2811
- mountChart('chart-benchmark-en', {json.dumps(benchmark_en, ensure_ascii=False)});
2781
+ mountChart('chart-outcome-zh', {script_json(outcome_zh or {})});
2782
+ mountChart('chart-trace-zh', {script_json(trace_zh or {})});
2783
+ mountChart('chart-benchmark-zh', {script_json(benchmark_zh)});
2784
+ mountChart('chart-outcome-en', {script_json(outcome_en or {})});
2785
+ mountChart('chart-trace-en', {script_json(trace_en or {})});
2786
+ mountChart('chart-benchmark-en', {script_json(benchmark_en)});
2812
2787
  }})();
2813
2788
  """
2814
2789
 
@@ -2931,15 +2906,25 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
2931
2906
  brief_blocks = [
2932
2907
  render_brief_block(*brief_titles["decision"], first_screen(result, lang, ui), "brief-decision"),
2933
2908
  ]
2934
- if lieflat_gallery_html:
2935
- brief_blocks.append(render_brief_block(*brief_titles["lieflat"], lieflat_gallery_html, "brief-lieflat"))
2936
2909
  brief_blocks.extend([
2937
2910
  render_brief_block(*brief_titles["outcomes"], render_outcomes_brief(result, outcome_chart, lang, ui, viz), "brief-outcomes"),
2938
2911
  render_brief_block(*brief_titles["tribunal"], render_tribunal_visual(result, "", "", lang, ui, compact=True), "brief-tribunal"),
2939
2912
  render_brief_block(*brief_titles["action"], render_evidence_to_action(result, lang, ui), "brief-action"),
2940
2913
  render_brief_block(*brief_titles["sources"], render_brief_sources(result, lang, ui), "brief-sources"),
2941
2914
  ])
2915
+ if lieflat_gallery_html:
2916
+ brief_blocks.insert(-1, render_brief_block(*brief_titles["lieflat"], lieflat_gallery_html, "brief-lieflat"))
2942
2917
  brief = "".join(brief_blocks)
2918
+ # Unique fragment targets per language, stable across theme variants.
2919
+ for section_key in ("decision", "outcomes", "tribunal", "action", "lieflat", "sources"):
2920
+ brief = brief.replace(f'class="brief-block brief-{section_key}"',
2921
+ f'class="brief-block brief-{section_key}" id="brief-{section_key}-{lang}"')
2922
+ nav_items = [("decision", brief_titles["decision"][0]), ("outcomes", brief_titles["outcomes"][0]),
2923
+ ("tribunal", brief_titles["tribunal"][0]), ("action", brief_titles["action"][0]),
2924
+ ("sources", brief_titles["sources"][0])]
2925
+ brief_nav = '<nav class="brief-navigation" aria-label="' + ("摘要导航" if lang == "zh" else "Brief navigation") + '">' + ''.join(
2926
+ f'<a href="#brief-{key}-{lang}"><span>{i:02d}</span>{esc(title)}</a>'
2927
+ for i, (key, title) in enumerate(nav_items, 1)) + '</nav>'
2943
2928
  full_report = render_full_report(result, lang, ui, charts, infographics, figures, viz)
2944
2929
  toc = render_full_toc(result, lang, ui)
2945
2930
 
@@ -2953,7 +2938,7 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
2953
2938
  else:
2954
2939
  origin_chip = ''
2955
2940
  meta_line = (f"{esc(ui['header_mode'])}{esc(label(lang, 'mode', meta.get('mode') or ''))}"
2956
- f" · {esc(ui['header_generated'])}{esc(meta.get('generated_at') or '')}"
2941
+ f" · {esc(ui['header_generated'])}{esc(str(meta.get('generated_at') or '')[:10])}"
2957
2942
  f" · {esc(ui['header_evidence'])}{len(result.get('evidence', []))}"
2958
2943
  f"{esc(ui['header_evidence_suffix'])}"
2959
2944
  f" · {esc(ui['header_sources'])}{len(result.get('sources', []))}"
@@ -2968,7 +2953,7 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
2968
2953
  <button type="button" class="report-view-btn" data-report-view="full" aria-pressed="false">{esc(ui['full_report'])}</button>
2969
2954
  </nav>
2970
2955
  </header>
2971
- <div class="report-page report-page-brief" data-report-page="brief">{brief}</div>
2956
+ <div class="report-page report-page-brief" data-report-page="brief">{brief_nav}<div class="brief-reading-content">{brief}</div></div>
2972
2957
  <div class="report-page report-page-full" data-report-page="full" hidden>
2973
2958
  <div class="full-report-intro"><h2>{esc(ui['full_report'])}</h2><p>{esc(ui['full_report_intro'])}</p></div>
2974
2959
  <div class="full-report-layout">{toc}<main class="full-report-content">{full_report}</main></div>
@@ -2993,18 +2978,25 @@ def render_html(result_en: dict, result_zh: dict, charts_zh: dict, charts_en: di
2993
2978
  <html lang="zh-CN" data-theme="{esc(theme)}">
2994
2979
  <head>
2995
2980
  <meta charset="utf-8">
2981
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; base-uri 'none'; object-src 'none'; form-action 'none'">
2996
2982
  {hash_meta}<meta name="viewport" content="width=device-width, initial-scale=1">
2997
2983
  <title>{esc(result_zh.get("meta", {}).get("question") or result_en.get("meta", {}).get("question") or "EduEvidence Evidence Report")}</title>
2998
2984
  <style>
2999
2985
  {_theme_css()}
3000
2986
  {_base_css()}
3001
2987
  {_motion_css()}
2988
+ {_reader_asset("reader.css")}
3002
2989
  </style>
3003
2990
  </head>
3004
2991
  <body>
3005
- <div class="controls">
3006
- <span class="generated-theme">{esc(THEME_DISPLAY[theme])}</span>
3007
- {_lang_switcher(UI_ZH, UI_EN)}
2992
+ <div class="reader-progress" aria-hidden="true"><span></span></div>
2993
+ <div class="controls reader-toolbar">
2994
+ <a class="reader-home" href="#" aria-label="Back to report start">EduEvidence<span class="generated-theme">{esc(THEME_DISPLAY[theme])}</span></a>
2995
+ <div class="reader-view-controls" role="group" aria-label="Report view">
2996
+ <button type="button" class="report-view-btn active" data-report-view="brief" data-copy="brief" aria-pressed="true">摘要</button>
2997
+ <button type="button" class="report-view-btn" data-report-view="full" data-copy="full" aria-pressed="false">完整报告</button>
2998
+ </div>
2999
+ <div class="reader-tools">{_lang_switcher(UI_ZH, UI_EN)}<button class="reader-print" type="button" data-copy="print">打印</button></div>
3008
3000
  </div>
3009
3001
  {body_zh}
3010
3002
  {body_en}
@@ -3013,6 +3005,7 @@ def render_html(result_en: dict, result_zh: dict, charts_zh: dict, charts_en: di
3013
3005
  </script>
3014
3006
  <script>
3015
3007
  {_enhancer_js(charts_zh, charts_en, result_en)}
3008
+ {_reader_asset("reader.js")}
3016
3009
  </script>
3017
3010
  </body>
3018
3011
  </html>