eduevidence 6.2.0 → 6.3.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 (114) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +22 -13
  3. package/README.zh-CN.md +15 -8
  4. package/SKILL.md +10 -9
  5. package/benchmarks/evidence-library.json +277 -1
  6. package/docs/architecture.md +6 -3
  7. package/docs/j-ev-experimental.md +250 -0
  8. package/docs/reproducibility.md +138 -0
  9. package/domains/_neutral/copy/few_shots.json +21 -0
  10. package/domains/_neutral/copy/framing_lexicon.json +19 -0
  11. package/domains/_neutral/copy/module_labels.json +5 -0
  12. package/domains/_neutral/copy/module_labels_footer.json +102 -0
  13. package/domains/_neutral/copy/module_labels_modules.json +204 -0
  14. package/domains/_neutral/copy/module_labels_nav.json +126 -0
  15. package/domains/_neutral/copy/module_labels_summary.json +98 -0
  16. package/domains/_neutral/copy/module_labels_tables.json +164 -0
  17. package/domains/_neutral/copy/module_labels_v2.json +90 -0
  18. package/domains/_neutral/copy/risk_constructs.json +20 -0
  19. package/domains/_neutral/copy/section_titles.json +66 -0
  20. package/domains/_neutral/copy/terminology.json +11 -0
  21. package/domains/check_copy_packs.py +103 -0
  22. package/domains/education/copy/few_shots.json +22 -0
  23. package/domains/education/copy/framing_enums.json +167 -0
  24. package/domains/education/copy/framing_lexicon.json +166 -0
  25. package/domains/education/copy/module_labels.json +169 -0
  26. package/domains/education/copy/risk_constructs.json +48 -0
  27. package/domains/education/copy/section_titles.json +186 -0
  28. package/domains/education/copy/terminology.json +70 -0
  29. package/domains/education/manifest.json +1 -1
  30. package/domains/education/outcome_taxonomy.json +2 -2
  31. package/domains/manifest.json +1 -1
  32. package/domains/policy/copy/few_shots.json +22 -0
  33. package/domains/policy/copy/framing_enums.json +94 -0
  34. package/domains/policy/copy/framing_lexicon.json +174 -0
  35. package/domains/policy/copy/module_labels.json +168 -0
  36. package/domains/policy/copy/risk_constructs.json +33 -0
  37. package/domains/policy/copy/section_titles.json +186 -0
  38. package/domains/policy/copy/terminology.json +64 -0
  39. package/engine/capabilities.py +57 -5
  40. package/engine/decision_policy.py +88 -17
  41. package/engine/library_builtin.py +7 -4
  42. package/engine/tribunal.py +17 -23
  43. package/engine/versions.py +1 -1
  44. package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +4 -4
  45. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +4 -4
  46. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +4 -4
  47. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
  48. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +4 -4
  49. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +4 -4
  50. package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
  51. package/examples/spaced-retrieval-practice/report.html +2522 -0
  52. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +4 -4
  53. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +4 -4
  54. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
  55. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +4 -4
  56. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +4 -4
  57. package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
  58. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +36 -36
  59. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +36 -36
  60. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +36 -36
  61. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +36 -36
  62. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +36 -36
  63. package/integrations/jev/__init__.py +115 -0
  64. package/integrations/jev/approval.py +212 -0
  65. package/integrations/jev/cli.py +84 -0
  66. package/integrations/jev/config.py +112 -0
  67. package/integrations/jev/gateway.py +128 -0
  68. package/integrations/jev/modes.py +38 -0
  69. package/integrations/jev/tools_classify.py +88 -0
  70. package/integrations/jev/tools_extract.py +111 -0
  71. package/integrations/jev/tools_rerank.py +71 -0
  72. package/integrations/jev/tools_screen.py +87 -0
  73. package/integrations/jev/tools_verify.py +95 -0
  74. package/integrations/jev_mcp.py +22 -0
  75. package/integrations/semantic_decide.py +286 -0
  76. package/integrations/semdecide_cli.py +55 -0
  77. package/package.json +9 -1
  78. package/pyproject.toml +1 -1
  79. package/references/report-copy-style.md +43 -3
  80. package/schemas/v2/decision-snapshot.schema.json +20 -9
  81. package/schemas/v2/intake.schema.json +191 -0
  82. package/scripts/build_evidence_library.py +15 -5
  83. package/scripts/dashboard_server.py +13 -2
  84. package/scripts/intake/__init__.py +31 -0
  85. package/scripts/intake/__main__.py +18 -0
  86. package/scripts/intake/background.py +78 -0
  87. package/scripts/intake/browser.py +79 -0
  88. package/scripts/intake/cli.py +57 -0
  89. package/scripts/intake/constants.py +57 -0
  90. package/scripts/intake/depth.py +53 -0
  91. package/scripts/intake/enhancements.py +106 -0
  92. package/scripts/intake/hooks.py +90 -0
  93. package/scripts/intake/prefs.py +76 -0
  94. package/scripts/intake/prompts.py +85 -0
  95. package/scripts/intake/session.py +152 -0
  96. package/scripts/lint_file_layers.py +126 -0
  97. package/scripts/orchestrator.py +68 -17
  98. package/scripts/pre_verdict_gate.py +21 -7
  99. package/scripts/skill_lint.py +11 -1
  100. package/scripts/skill_payload.py +3 -3
  101. package/scripts/test_adversarial_empirical.py +70 -6
  102. package/skill/agents/evidence-judge.md +49 -7
  103. package/skill/workflows/experimental-jev.md +170 -0
  104. package/skill/workflows/intake.md +120 -0
  105. package/visualization/eduevidence-report/scripts/build_infographics.py +32 -14
  106. package/visualization/eduevidence-report/scripts/build_report.py +75 -662
  107. package/visualization/eduevidence-report/scripts/report_copy_pack.py +296 -0
  108. package/visualization/eduevidence-report/scripts/report_copy_policy_guard.py +47 -0
  109. package/visualization/eduevidence-report/scripts/zh_labels.py +61 -0
  110. package/scripts/build_esl_artifacts.py +0 -1921
  111. package/scripts/build_killer_demo.py +0 -295
  112. package/scripts/enrich_projects_human_and_lieflat.py +0 -315
  113. package/scripts/generate_new_projects.py +0 -686
  114. package/scripts/sync_killer_demo_report.py +0 -270
@@ -37,7 +37,7 @@ from typing import Any
37
37
  from build_charts import build_all as build_chart_specs, effect_outcomes
38
38
  from build_figures import build_figure_data, render_figures, render_lieflat_gallery
39
39
  from build_infographics import build_all as build_infographics
40
- from lieflat_engine import REGISTRY as LIEFLAT_REGISTRY, LEGACY_TYPES, ACADEMIC_FIGURE_KEYS
40
+ from lieflat_engine import REGISTRY as LIEFLAT_REGISTRY, ACADEMIC_FIGURE_KEYS
41
41
  from zh_labels import label
42
42
 
43
43
  THEMES_DIR = Path(__file__).resolve().parent.parent / "themes"
@@ -65,197 +65,21 @@ DATA_ORIGIN_LABELS = {
65
65
  # Full report is intentionally NOT a fixed 12-chapter template. The template exposes
66
66
  # semantic modules; an upstream AI may group them into any 5–7 chapter outline by writing
67
67
  # `report_outline.chapters`. If it does not, a six-chapter fallback keeps the report usable.
68
- FULL_REPORT_MODULES = (
69
- "decision", "scope", "retrieval", "outcomes", "evidence", "quality", "conflicts",
70
- "trace", "applicability", "intervention", "evaluation", "sources",
68
+ # Domain copy packs (domains/<id>/copy/) — UI 词典 + 域文案, see report_copy_pack.py.
69
+ from report_copy_pack import ( # noqa: E402
70
+ FULL_REPORT_MODULES,
71
+ activate_copy_pack,
72
+ brief_block_titles,
73
+ build_ui,
74
+ default_full_report_plan,
75
+ default_leads,
76
+ evidence_detail_label_table,
77
+ frame_enum_table,
78
+ methodology_label_table,
79
+ outcome_group_map,
80
+ scope_field_labels,
81
+ scope_subfield_labels,
71
82
  )
72
- DEFAULT_FULL_REPORT_PLAN = (
73
- {"key": "decision", "title_zh": "结论、裁决与研究边界", "title_en": "Decision, Adjudication & Research Boundary",
74
- "modules": ("decision", "scope")},
75
- {"key": "evidence", "title_zh": "关键证据与结果分离", "title_en": "Key Evidence & Outcome Separation",
76
- "modules": ("retrieval", "outcomes", "evidence")},
77
- {"key": "quality", "title_zh": "证据可信度、反证与方法审计", "title_en": "Evidence Quality, Counterevidence & Method Audit",
78
- "modules": ("quality", "conflicts", "trace")},
79
- {"key": "action", "title_zh": "适用范围与教学行动", "title_en": "Applicability & Teaching Action",
80
- "modules": ("applicability", "intervention")},
81
- {"key": "evaluation", "title_zh": "试点设计、评估与停止条件", "title_en": "Pilot, Evaluation & Stop Conditions",
82
- "modules": ("evaluation",)},
83
- {"key": "sources", "title_zh": "来源、溯源与附录", "title_en": "Sources, Traceability & Appendix",
84
- "modules": ("sources",)},
85
- )
86
-
87
- METHODOLOGY_LABELS_ZH = {
88
- "control_group": "对照组", "randomization": "随机分配", "pre_test": "前测",
89
- "post_test": "后测", "retention_test": "保持测试", "transfer_test": "迁移测试",
90
- "sample_bias": "样本偏差", "self_selection": "自我选择偏差",
91
- "measurement_validity": "测量效度", "confounders": "混杂因素",
92
- "instructor_effect": "教师效应", "novelty_effect": "新奇效应",
93
- "tool_version_effect": "工具版本效应", "ai_usage_policy": "AI 使用规则",
94
- "dropout": "样本流失",
95
- }
96
-
97
- FRAME_ENUM_ZH = {
98
- "teaching_decision": "教学决策",
99
- "evidence_review": "证据评审",
100
- "pilot_design": "试点设计",
101
- "evaluation_design": "评价设计",
102
- "undergraduate_year_1": "大学一年级",
103
- "undergraduate_year_2": "大学二年级",
104
- "undergraduate_year_3": "大学三年级",
105
- "undergraduate_year_4": "大学四年级",
106
- "postgraduate": "研究生",
107
- "high_school": "高中",
108
- "middle_school": "初中",
109
- "primary_school": "小学",
110
- "workplace_adult": "在职成人",
111
- "computer_science": "计算机科学与技术",
112
- "information_systems": "信息管理",
113
- "software_engineering": "软件工程",
114
- "data_science": "数据科学",
115
- "mathematics": "数学",
116
- "medicine": "医学",
117
- "humanities": "人文",
118
- "social_sciences": "社会科学",
119
- "business": "商科",
120
- "C_programming": "C 语言程序设计",
121
- "introductory_programming": "程序设计入门",
122
- "data_structures": "数据结构",
123
- "algorithms": "算法",
124
- "database_systems": "数据库系统",
125
- "machine_learning": "机器学习",
126
- "academic_writing": "学术写作",
127
- "tesol": "英语教学",
128
- "compulsory_core_course": "必修核心课程",
129
- "elective_course": "选修课程",
130
- "lecture_lab": "讲授 + 实验课",
131
- "seminar": "研讨课",
132
- "online_course": "在线课程",
133
- "blended_learning": "混合式教学",
134
- "professional_training": "职业培训",
135
- "enterprise_customer_support": "企业客户支持",
136
- "software_development": "软件开发",
137
- "knowledge_work": "知识型工作",
138
- "primary": "主要结果",
139
- "secondary": "次要结果",
140
- "risk": "风险结果",
141
- "adopt": "采纳",
142
- "modify": "修改",
143
- "terminate": "终止",
144
- "maintain": "维持",
145
- "evaluate_impact": "评估影响",
146
- "rct": "随机对照试验",
147
- "quasi_experimental": "准实验",
148
- "observational": "观察性研究",
149
- "meta_analysis": "元分析",
150
- "systematic_review": "系统综述",
151
- "qualitative": "质性研究",
152
- "mixed_methods": "混合方法",
153
- "survey": "调查研究",
154
- "case_study": "案例研究",
155
- "literature_review": "文献综述",
156
- "online": "线上",
157
- "offline": "线下",
158
- "hybrid": "混合",
159
- "under_design_pending_evidence_review": "设计中(待证据评审)",
160
- "human_supervised_customer_support_assistant": "人工监督的客服助手",
161
- "approved_knowledge_base_and_agent_review": "已批准知识库 + 坐席复核",
162
- "institutional_reform": "机构改革",
163
- "enterprise_customer_support_staff": "企业客服人员",
164
- "autonomous_agents_and_high_stakes_specialist_advice": "自主代理与高风险专业建议",
165
- "lecture_with_lab_exercises": "讲授 + 实验练习",
166
- "generative_ai_coding_assistant": "生成式 AI 编程助手",
167
- "first_programming_course_no_prior_text_based_programming": "首次程序设计课程,无文本编程基础",
168
- "mixed_ability_large_class_60_students": "混合能力大班(60 人)",
169
- "weekly_lab_sessions": "每周实验课",
170
- "one_semester": "一学期",
171
- "16_weeks_one_semester": "16 周(一学期)",
172
- "60_students": "60 名学生",
173
- "TA_supported_two_TAs": "助教支持(2 名助教)",
174
- "daily": "每日",
175
- "weekly": "每周",
176
- "monthly": "每月",
177
- }
178
-
179
- FRAME_ENUM_EN = {
180
- "teaching_decision": "Teaching decision",
181
- "evidence_review": "Evidence review",
182
- "pilot_design": "Pilot design",
183
- "evaluation_design": "Evaluation design",
184
- "undergraduate_year_1": "First-year undergraduate",
185
- "undergraduate_year_2": "Second-year undergraduate",
186
- "undergraduate_year_3": "Third-year undergraduate",
187
- "undergraduate_year_4": "Fourth-year undergraduate",
188
- "postgraduate": "Postgraduate",
189
- "high_school": "High school",
190
- "middle_school": "Middle school",
191
- "primary_school": "Primary school",
192
- "workplace_adult": "Working adults",
193
- "computer_science": "Computer science",
194
- "information_systems": "Information systems",
195
- "software_engineering": "Software engineering",
196
- "data_science": "Data science",
197
- "mathematics": "Mathematics",
198
- "medicine": "Medicine",
199
- "humanities": "Humanities",
200
- "social_sciences": "Social sciences",
201
- "business": "Business",
202
- "C_programming": "C programming",
203
- "introductory_programming": "Introductory programming",
204
- "data_structures": "Data structures",
205
- "algorithms": "Algorithms",
206
- "database_systems": "Database systems",
207
- "machine_learning": "Machine learning",
208
- "academic_writing": "Academic writing",
209
- "tesol": "TESOL",
210
- "compulsory_core_course": "Compulsory core course",
211
- "elective_course": "Elective course",
212
- "lecture_lab": "Lecture + lab",
213
- "seminar": "Seminar",
214
- "online_course": "Online course",
215
- "blended_learning": "Blended learning",
216
- "professional_training": "Professional training",
217
- "enterprise_customer_support": "Enterprise customer support",
218
- "software_development": "Software development",
219
- "knowledge_work": "Knowledge work",
220
- "primary": "Primary outcomes",
221
- "secondary": "Secondary outcomes",
222
- "risk": "Risk outcomes",
223
- "adopt": "Adopt",
224
- "modify": "Modify",
225
- "terminate": "Terminate",
226
- "maintain": "Maintain",
227
- "evaluate_impact": "Evaluate impact",
228
- "rct": "Randomised controlled trial",
229
- "quasi_experimental": "Quasi-experimental",
230
- "observational": "Observational",
231
- "meta_analysis": "Meta-analysis",
232
- "systematic_review": "Systematic review",
233
- "qualitative": "Qualitative",
234
- "mixed_methods": "Mixed methods",
235
- "survey": "Survey",
236
- "case_study": "Case study",
237
- "literature_review": "Literature review",
238
- "online": "Online",
239
- "offline": "Offline",
240
- "hybrid": "Hybrid",
241
- "under_design_pending_evidence_review": "Under design (pending evidence review)",
242
- "approved_knowledge_base_and_agent_review": "Approved knowledge base + agent review",
243
- "institutional_reform": "Institutional reform",
244
- "enterprise_customer_support_staff": "Customer-support staff",
245
- "autonomous_agents_and_high_stakes_specialist_advice": "Autonomous agents and high-stakes specialist advice",
246
- "lecture_with_lab_exercises": "Lecture with lab exercises",
247
- "generative_ai_coding_assistant": "Generative AI coding assistant",
248
- "first_programming_course_no_prior_text_based_programming": "First programming course, no prior text-based programming",
249
- "mixed_ability_large_class_60_students": "Mixed-ability large class (60 students)",
250
- "weekly_lab_sessions": "Weekly lab sessions",
251
- "one_semester": "One semester",
252
- "16_weeks_one_semester": "16 weeks (one semester)",
253
- "60_students": "60 students",
254
- "TA_supported_two_TAs": "TA supported (2 TAs)",
255
- "daily": "Daily",
256
- "weekly": "Weekly",
257
- "monthly": "Monthly",
258
- }
259
83
 
260
84
  DIR_LABEL = {"support": "支持", "contradict": "反驳", "neutral": "中性"}
261
85
  DIR_CLASS = {"support": "pos", "contradict": "neg", "neutral": "neu"}
@@ -266,359 +90,6 @@ EFFECT_CLASS = {"positive": "pos", "negative": "neg", "null": "neu", "neutral":
266
90
  # 双语 UI 文案
267
91
  # ---------------------------------------------------------------------------
268
92
 
269
- UI_ZH = {
270
- "theme_label": "主题",
271
- "lang_label": "语言",
272
- "zh": "中文",
273
- "en": "EN",
274
- "visual_brief": "可视化摘要",
275
- "full_report": "完整报告",
276
- "contents": "目录",
277
- "collapse_contents": "收起目录",
278
- "expand_contents": "展开目录",
279
- "expand_evidence": "查看完整证据",
280
- "expand_methodology": "查看审计依据",
281
- "expand_source": "查看来源与溯源",
282
- "expand_details": "展开完整说明",
283
- "what_this_means": "这意味着什么",
284
- "original_title": "原文标题",
285
- "original_text": "原文",
286
- "full_report_intro": "结论前置:全部可追溯证据与方法学细节都在这里,关键论证位置穿插有意义的可视化,每个数字都能回查到 result.json。",
287
- "section_titles": {
288
- "01": "01 执行决策", "02": "02 结果证据概览", "03": "03 证据矩阵",
289
- "04": "04 证据裁决", "05": "05 方法学审计", "06": "06 冲突分析",
290
- "07": "07 主张-证据追溯", "08": "08 适用性", "09": "09 教学干预",
291
- "10": "10 评价方案", "11": "11 基准测试", "12": "12 来源与溯源",
292
- },
293
- "section_leads": {
294
- "01": "本节先给结论:最终怎么裁决、置信度多高、靠哪几条证据。",
295
- "02": "一图看清:哪些学习结果有支持证据、哪些被反驳。",
296
- "03": "每条证据来自哪项研究、测了什么、方向与质量如何;可筛选、可搜索。",
297
- "04": "证据允许主张什么、不允许主张什么;缺失的关键证据是什么。",
298
- "05": "研究质量可靠吗?哪些方法学问题让结论打折。",
299
- "06": "不同研究为何结论不同;分歧出在哪一环。",
300
- "07": "从结论到证据到原始来源,每一步都可追查。",
301
- "08": "结论适用于谁、什么课程与结果、需要什么条件。",
302
- "09": "试点怎么分阶段放开 AI 规则;什么情况必须叫停。",
303
- "10": "如何验证效果:指标、对照、成功阈值。",
304
- "11": "EduEvidence 自身基准表现:引用精度与成本。",
305
- "12": "每篇文献是谁、出自哪里、如何获取。",
306
- },
307
- "decision_kpi": ["决策", "置信度", "证据最充分的结果", "最不确定的结果", "主要风险", "来源数量"],
308
- "summary_title": "一句话结论",
309
- "summary_question": "问题",
310
- "summary_evidence": "依据",
311
- "summary_action": "行动",
312
- "outcome_table": ["结果类型", "正向效应", "负向效应", "零效应", "证据"],
313
- "figure1_caption": "图 1. 各结果类型的正向 / 负向 / 零效应证据数量(基于 effect_direction,不等同于 Claim 是否被支持)。",
314
- "matrix_filter": "筛选 / 搜索",
315
- "matrix_search_ph": "搜索证据…",
316
- "matrix_all_dir": "全部效应",
317
- "matrix_all_outcome": "全部结果",
318
- "matrix_heads": ["ID", "结果", "效应", "质量", "主张", "来源"],
319
- "matrix_details": "查看完整证据",
320
- "matrix_detail_labels": ["研究标题", "研究设计", "人群", "干预", "直接性"],
321
- "matrix_source_missing": "无可验证来源",
322
- "hero_action": "建议决策",
323
- "hero_confidence": "置信度",
324
- "hero_supported": "最强支持结论",
325
- "hero_uncertain": "关键不确定性 / 反例",
326
- "hero_risk": "主要风险",
327
- "hero_next": "下一步",
328
- "hero_provenance": "证据 / 来源",
329
- "outcome_separation_title": "结果分离 · 任务表现 ≠ 学习效果",
330
- "effect_positive": "正向效应",
331
- "effect_negative": "负向效应",
332
- "effect_null": "零效应",
333
- "outcome_group_task": "任务 / 近端表现",
334
- "outcome_group_learning": "学习 / 保持 / 迁移",
335
- "outcome_group_risk": "风险 / 依赖",
336
- "outcome_group_other": "其他结果",
337
- "outcome_sep_note": "将不同结果类型分开裁决,避免把训练时更快、更高分直接等同于真正学会。",
338
- "tribunal_decision": "决策",
339
- "tribunal_confidence": "置信度",
340
- "tribunal_can": "可以主张",
341
- "tribunal_uncertain": "尚不能主张",
342
- "tribunal_cannot": "被反驳的主张",
343
- "tribunal_missing": "缺失证据",
344
- "tribunal_flow": "EvidenceFlow 协议",
345
- "tribunal_figure": "裁决信息图",
346
- "method_audit_heads": ["检查项", "状态", "说明"],
347
- "method_guard": "任务 vs 学习护栏",
348
- "conflict_verdict": "裁决说明",
349
- "trace_decision": "决策",
350
- "trace_claim_prefix": "主张",
351
- "trace_no_source": "无来源",
352
- "applicability": ["适用于谁", "适用课程", "适用结果", "适用条件", "目标人群", "目标情境"],
353
- "intervention_learners": "目标学习者",
354
- "intervention_duration": "试点时长",
355
- "intervention_policy": "AI 使用规则",
356
- "intervention_rule": "AI 规则",
357
- "intervention_activities": "活动",
358
- "intervention_check": "结果检查",
359
- "intervention_stop": "停止条件",
360
- "intervention_timeline": "干预时间线信息图",
361
- "evaluation_question": "研究问题",
362
- "evaluation_measures": ["基线", "后测", "保持测试", "迁移测试"],
363
- "evaluation_metrics": ["过程指标", "学习指标", "风险指标"],
364
- "evaluation_threshold": "成功阈值",
365
- "evaluation_plan": "分析计划",
366
- "evaluation_figure": "评价设计信息图",
367
- "benchmark_note": "result.json 未携带 benchmark.baselines,本图不绘制;基准表现见独立基准报告。",
368
- "sources_title": "来源列表",
369
- "sources_heads": ["ID", "标题", "年份", "权威级别", "可验证位置"],
370
- "provenance_title": "Fetch 溯源",
371
- "provenance_heads": ["来源", "Fetch 方式", "状态", "降级", "时间"],
372
- "provenance_search": "搜索提供方",
373
- "provenance_time": "检索时间",
374
- "provenance_empty": "无逐条 fetch 记录(来源由研究管线直接提供)。",
375
- "no_data": "无数据。",
376
- "header_evidence": "证据 ",
377
- "header_sources": "来源 ",
378
- "header_mode": "模式:",
379
- "header_generated": "生成时间:",
380
- "header_evidence_suffix": " 条",
381
- "header_sources_suffix": " 个",
382
- "footer_schema": "Schema",
383
- "footer_claims": "Claim Binding",
384
- "footer_numbers": "Numeric Consistency",
385
- "footer_bilingual": "Bilingual Structure",
386
- "footer_language": "语言人话化",
387
- "footer_no_false_precision": "无伪精度",
388
- "footer_lieflat_bound": "Lieflat 数据溯源",
389
- "footer_no_axis_distortion": "坐标轴无失真",
390
- "footer_colorblind_safe": "色盲安全",
391
- "footer": "EduEvidence 证据报告 · {integrity} · 单文件离线可打开 · 数据源:result.json",
392
- "matrix_search_label": "筛选 / 搜索证据",
393
- "matrix_dir_filter": "按效应方向筛选",
394
- "matrix_outcome_filter": "按结果类型筛选",
395
- "svg_balance_title": "各结果类型证据效应分布",
396
- "svg_balance_desc": "各结果类型的正向 / 负向 / 零效应证据条数(基于 effect_direction,不等同于 Claim 是否被支持)。",
397
- "svg_figure1_title": "各结果类型效应方向分布(出版级学术图)",
398
- "svg_figure1_desc": "各结果类型的正向 / 负向 / 零效应证据条数;计数轴整数刻度,不随主题变化。来源:EduEvidence result.json。",
399
- "svg_benchmark_title": "基准对比:各基线引用支持精度",
400
- "svg_benchmark_desc": "各基线的引用支持精度(Citation support precision)对比;无基准数据时不绘制。",
401
- "svg_workflow_title": "EvidenceFlow 协议流程",
402
- "svg_workflow_desc": "从问题框架、检索、抓取验证、证据抽取、反方质疑、方法审计、裁决到适用性与干预评价的完整流程。",
403
- "svg_tribunal_title": "证据裁决信息图",
404
- "svg_tribunal_desc": "可以主张与不可主张的证据 ID 与建议决策徽章;完整主张文本见下方裁决卡片。",
405
- "svg_intervention_title": "教学干预时间线",
406
- "svg_intervention_desc": "各试点阶段的短名称与活动数量;完整 AI 使用规则见阶段说明块。",
407
- "svg_evaluation_title": "评价设计流程",
408
- "svg_evaluation_desc": "基线、后测、保持测试与迁移测试的评价流程;完整指标与分析计划见评估章节。",
409
- "raw_tag_title": "原始标识",
410
- "summary_tag_support": "支持",
411
- "summary_tag_contradict": "反驳",
412
- "summary_confidence_prefix": "(置信度:",
413
- "summary_confidence_suffix": ")",
414
- "method_target": "审查目标",
415
- "applicability_not_suitable": "不适用于",
416
- "applicability_conditions": "适用条件",
417
- "trace_claim_sep": ":",
418
- "colon": ":",
419
- "lang_switcher_aria": "语言切换 / Language switch",
420
- "theme_switcher_aria": "主题 / Theme",
421
- "v2_project_title": "项目与研究历史",
422
- "v2_project_id": "项目 ID",
423
- "v2_graph_revision": "证据图版本",
424
- "v2_decision_snapshot": "决策快照",
425
- "v2_timeline": "项目时间线",
426
- "v2_gaps_title": "知识缺口",
427
- "v2_gap_type": "缺口类型",
428
- "v2_gap_priority": "优先级",
429
- "v2_gap_reasoning": "依据",
430
- "v2_design_title": "研究设计",
431
- "v2_design_type": "设计类型",
432
- "v2_design_question": "研究问题",
433
- "v2_provenance_title": "数据集与分析溯源",
434
- "v2_diff_title": "决策变更",
435
- "v2_diff_action": "决策动作",
436
- "v2_diff_confidence": "置信度",
437
- "v2_diff_claims": "变更的主张",
438
- "v2_diff_gaps": "已解决/新增缺口",
439
- "v2_revision": "版本",
440
- "v2_decision": "决策",
441
- "v2_no_v2_data": "(无 V2 项目数据)",
442
- }
443
-
444
- UI_EN = {
445
- "theme_label": "Theme",
446
- "lang_label": "Language",
447
- "zh": "中文",
448
- "en": "EN",
449
- "visual_brief": "Visual Brief",
450
- "full_report": "Full Report",
451
- "contents": "Contents",
452
- "collapse_contents": "Collapse contents",
453
- "expand_contents": "Expand contents",
454
- "expand_evidence": "View full evidence",
455
- "expand_methodology": "View audit rationale",
456
- "expand_source": "View source & provenance",
457
- "expand_details": "Expand full explanation",
458
- "what_this_means": "What this means",
459
- "original_title": "Original title",
460
- "original_text": "Original text",
461
- "full_report_intro": "Conclusions first: every traceable piece of evidence and method note lives here, with visuals only at points where they add meaning. Every number traces back to result.json.",
462
- "section_titles": {
463
- "01": "01 Executive Decision", "02": "02 Outcome Evidence Overview",
464
- "03": "03 Evidence Matrix", "04": "04 Evidence Tribunal",
465
- "05": "05 Methodology Audit", "06": "06 Conflict Analysis",
466
- "07": "07 Claim-Evidence Trace", "08": "08 Applicability",
467
- "09": "09 Teaching Intervention", "10": "10 Evaluation Plan",
468
- "11": "11 Benchmark", "12": "12 Sources & Provenance",
469
- },
470
- "section_leads": {
471
- "01": "The verdict first: what we decide, at what confidence, on which evidence.",
472
- "02": "At a glance: which learning outcomes have supporting evidence, which are contradicted.",
473
- "03": "Where each piece of evidence comes from, what it measures, its direction and quality — filterable and searchable.",
474
- "04": "What the evidence lets us claim, what it does not, and what is still missing.",
475
- "05": "How reliable are these studies, and which methodological concerns discount the conclusions.",
476
- "06": "Why studies disagree — and where exactly they diverge.",
477
- "07": "Every step from conclusion to evidence to source stays traceable.",
478
- "08": "Who the conclusion applies to, for which course and outcomes, under what conditions.",
479
- "09": "How AI usage rules phase in during a pilot, and when we must stop.",
480
- "10": "How we verify real effects: metrics, comparison, success threshold.",
481
- "11": "How EduEvidence itself performs: citation precision and cost.",
482
- "12": "Who wrote each cited study, where it came from, how it was fetched.",
483
- },
484
- "decision_kpi": ["Decision", "Confidence", "Best-supported outcome", "Most uncertain outcome", "Main risk", "Sources"],
485
- "summary_title": "Bottom line",
486
- "summary_question": "Question",
487
- "summary_evidence": "Evidence",
488
- "summary_action": "Action",
489
- "outcome_table": ["Outcome", "Positive effect", "Negative effect", "Null effect", "Evidence"],
490
- "figure1_caption": "Fig. 1. Positive / negative / null effect counts per outcome (based on effect_direction, not claim support).",
491
- "matrix_filter": "Filter / Search",
492
- "matrix_search_ph": "Search evidence…",
493
- "matrix_all_dir": "All effects",
494
- "matrix_all_outcome": "All outcomes",
495
- "matrix_heads": ["ID", "Outcome", "Effect", "Quality", "Claim", "Source"],
496
- "matrix_details": "View full evidence",
497
- "matrix_detail_labels": ["Study title", "Design", "Population", "Intervention", "Directness"],
498
- "matrix_source_missing": "No verifiable source",
499
- "hero_action": "Recommended decision",
500
- "hero_confidence": "Confidence",
501
- "hero_supported": "Strongest supported conclusion",
502
- "hero_uncertain": "Key uncertainty / contradiction",
503
- "hero_risk": "Main risk",
504
- "hero_next": "Next action",
505
- "hero_provenance": "Evidence / sources",
506
- "outcome_separation_title": "Outcome Separation · Task performance ≠ learning",
507
- "effect_positive": "Positive effect",
508
- "effect_negative": "Negative effect",
509
- "effect_null": "Null effect",
510
- "outcome_group_task": "Task / proximal performance",
511
- "outcome_group_learning": "Learning / retention / transfer",
512
- "outcome_group_risk": "Risk / dependency",
513
- "outcome_group_other": "Other outcomes",
514
- "outcome_sep_note": "Outcomes are adjudicated separately so faster training performance is not silently treated as evidence of learning.",
515
- "tribunal_decision": "Decision",
516
- "tribunal_confidence": "Confidence",
517
- "tribunal_can": "Can claim",
518
- "tribunal_uncertain": "Cannot yet claim",
519
- "tribunal_cannot": "Contradicted claims",
520
- "tribunal_missing": "Missing evidence",
521
- "tribunal_flow": "EvidenceFlow Protocol",
522
- "tribunal_figure": "Tribunal infographic",
523
- "method_audit_heads": ["Item", "Status", "Note"],
524
- "method_guard": "Task vs learning guard",
525
- "conflict_verdict": "Tribunal note",
526
- "trace_decision": "Decision",
527
- "trace_claim_prefix": "Claim",
528
- "trace_no_source": "No source",
529
- "applicability": ["Suitable for", "Course", "Outcomes", "Conditions", "Target population", "Target context"],
530
- "intervention_learners": "Target learners",
531
- "intervention_duration": "Pilot duration",
532
- "intervention_policy": "AI usage policy",
533
- "intervention_rule": "AI rule",
534
- "intervention_activities": "Activities",
535
- "intervention_check": "Outcome check",
536
- "intervention_stop": "Stop conditions",
537
- "intervention_timeline": "Intervention timeline infographic",
538
- "evaluation_question": "Research question",
539
- "evaluation_measures": ["Baseline", "Post test", "Retention", "Transfer"],
540
- "evaluation_metrics": ["Process metrics", "Learning metrics", "Risk metrics"],
541
- "evaluation_threshold": "Success threshold",
542
- "evaluation_plan": "Analysis plan",
543
- "evaluation_figure": "Evaluation design infographic",
544
- "benchmark_note": "result.json carries no benchmark.baselines, so this visual is omitted; see the standalone benchmark report.",
545
- "sources_title": "Source list",
546
- "sources_heads": ["ID", "Title", "Year", "Authority", "Verifiable location"],
547
- "provenance_title": "Fetch provenance",
548
- "provenance_heads": ["Source", "Fetch method", "Status", "Fallback", "Time"],
549
- "provenance_search": "Search provider",
550
- "provenance_time": "Fetched at",
551
- "provenance_empty": "No per-source fetch records (sources provided directly by the research pipeline).",
552
- "no_data": "No data.",
553
- "header_evidence": "Evidence: ",
554
- "header_sources": "Sources: ",
555
- "header_mode": "Mode: ",
556
- "header_generated": "Generated: ",
557
- "header_evidence_suffix": "",
558
- "header_sources_suffix": "",
559
- "footer_schema": "Schema",
560
- "footer_claims": "Claim Binding",
561
- "footer_numbers": "Numeric Consistency",
562
- "footer_bilingual": "Bilingual Structure",
563
- "footer_language": "Human Language",
564
- "footer_no_false_precision": "False Precision",
565
- "footer_lieflat_bound": "Lieflat Data Bound",
566
- "footer_no_axis_distortion": "Axis Distortion",
567
- "footer_colorblind_safe": "Colorblind Safe",
568
- "footer": "EduEvidence Evidence Report · {integrity} · single-file offline · source: result.json",
569
- "matrix_search_label": "Filter / search evidence",
570
- "matrix_dir_filter": "Filter by effect direction",
571
- "matrix_outcome_filter": "Filter by outcome type",
572
- "svg_balance_title": "Outcome evidence effect balance",
573
- "svg_balance_desc": "Positive / negative / null effect counts per outcome (based on effect_direction, not claim support).",
574
- "svg_figure1_title": "Effect direction by outcome type (publication figure)",
575
- "svg_figure1_desc": "Counts of positive / negative / null effects per outcome type with an integer count axis, theme-independent. Source: EduEvidence result.json.",
576
- "svg_benchmark_title": "Benchmark: citation support precision by baseline",
577
- "svg_benchmark_desc": "Citation support precision per baseline; not drawn when no baseline data exists.",
578
- "svg_workflow_title": "EvidenceFlow Protocol",
579
- "svg_workflow_desc": "Research flow from framing, retrieval, fetch/verify, extraction, challenge, method audit and adjudication to applicability and intervention evaluation.",
580
- "svg_tribunal_title": "Evidence Tribunal infographic",
581
- "svg_tribunal_desc": "Evidence IDs for claims that can and cannot be claimed, plus the recommended action badge; full claim text is in the tribunal cards below.",
582
- "svg_intervention_title": "Teaching intervention timeline",
583
- "svg_intervention_desc": "Short phase names and activity counts; full AI usage rules are in the phase blocks.",
584
- "svg_evaluation_title": "Evaluation design flow",
585
- "svg_evaluation_desc": "Evaluation flow across baseline, post test, retention and transfer; full metrics and analysis plan are in the evaluation section.",
586
- "raw_tag_title": "raw id",
587
- "summary_tag_support": "Support",
588
- "summary_tag_contradict": "Contradict",
589
- "summary_confidence_prefix": " (confidence: ",
590
- "summary_confidence_suffix": ")",
591
- "method_target": "Audit target",
592
- "applicability_not_suitable": "Not suitable for",
593
- "applicability_conditions": "Conditions",
594
- "trace_claim_sep": ": ",
595
- "colon": ": ",
596
- "lang_switcher_aria": "语言切换 / Language switch",
597
- "theme_switcher_aria": "主题 / Theme",
598
- "v2_project_title": "Project & Research History",
599
- "v2_project_id": "Project ID",
600
- "v2_graph_revision": "Graph revision",
601
- "v2_decision_snapshot": "Decision snapshot",
602
- "v2_timeline": "Project timeline",
603
- "v2_gaps_title": "Knowledge gaps",
604
- "v2_gap_type": "Gap type",
605
- "v2_gap_priority": "Priority",
606
- "v2_gap_reasoning": "Reasoning",
607
- "v2_design_title": "Study design",
608
- "v2_design_type": "Design type",
609
- "v2_design_question": "Research question",
610
- "v2_provenance_title": "Dataset & analysis provenance",
611
- "v2_diff_title": "Decision diff",
612
- "v2_diff_action": "Decision action",
613
- "v2_diff_confidence": "Confidence",
614
- "v2_diff_claims": "Changed claims",
615
- "v2_diff_gaps": "Resolved/new gaps",
616
- "v2_revision": "Revision",
617
- "v2_decision": "Decision",
618
- "v2_no_v2_data": "(no V2 project data)",
619
- }
620
-
621
-
622
93
  class ReportInvalid(Exception):
623
94
  """Scientific Integrity Gate failure (§27/§60): report must not be published."""
624
95
 
@@ -630,7 +101,7 @@ def esc(text: Any) -> str:
630
101
  def _svg_a11y(svg: str, title: str, desc: str) -> str:
631
102
  """6.5: 为嵌入的 SVG 注入双语 <title>/<desc>,并把 aria-label 换成 UI 字典文案。
632
103
 
633
- title/desc 由调用方按当前语言从 UI_ZH / UI_EN 取;aria-label 已存在时覆盖,
104
+ title/desc 由调用方按当前语言从 build_ui() 取;aria-label 已存在时覆盖,
634
105
  不存在时补上,保证英文模式下不残留中文 aria-label。
635
106
  """
636
107
  if not svg:
@@ -1345,7 +816,7 @@ _ZH_RESIDUE_RE = [
1345
816
  ]
1346
817
 
1347
818
  #: Raw storage identifiers inside a sentence. A single-token value may be an
1348
- #: enum rendered through FRAME_ENUM_*, but an underscore inside a multi-word
819
+ #: enum rendered through frame_enum_table(), but an underscore inside a multi-word
1349
820
  #: string or inside a zh sentence is a copy defect the reader would see.
1350
821
  _RAW_IDENTIFIER_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+")
1351
822
 
@@ -1361,7 +832,8 @@ def _scan_raw_identifiers(problems: list[str], path: str, text: str) -> None:
1361
832
  return
1362
833
  for match in _RAW_IDENTIFIER_RE.finditer(cleaned):
1363
834
  token = match.group(0)
1364
- if token in FRAME_ENUM_ZH or token in FRAME_ENUM_EN:
835
+ enum_tokens = set(frame_enum_table("zh")) | set(frame_enum_table("en"))
836
+ if token in enum_tokens:
1365
837
  continue
1366
838
  problems.append(path + ": raw identifier " + repr(token) + " leaked into prose")
1367
839
  return
@@ -1464,7 +936,7 @@ def check_language_parallel(result_en: dict, result_zh: dict) -> list[str]:
1464
936
  z_text = z_item.get("claim") if isinstance(z_item, dict) else None
1465
937
  if z_text:
1466
938
  _scan_narrative(problems, key + "[" + str(i) + "].claim", e_text, z_text, strict=False)
1467
- # Frame prose: field values may be enums (rendered via FRAME_ENUM_*), but a
939
+ # Frame prose: field values may be enums (rendered via frame_enum_table), but a
1468
940
  # storage identifier must never reach the reader inside a sentence. Both
1469
941
  # language versions are checked because the leak is an authoring defect.
1470
942
  # Weeks and pilot duration are structural tokens (W1, W2-W13, one_semester):
@@ -1684,7 +1156,7 @@ def diverging_bar_svg(option: dict, width: int = 720, height: int = 300,
1684
1156
  """真 diverging 静态图(P0-10):support 从中心向右、contradict 从中心向左,
1685
1157
  neutral 走独立的细条道(第二网格),三系列互不覆盖。计数轴整数刻度(P0-11)。
1686
1158
  6.5: aria-label / title / desc 从 UI 字典按语言取。"""
1687
- ui = ui or UI_ZH
1159
+ ui = ui or build_ui("zh")
1688
1160
  cats = option.get("yAxis", [{}])[0].get("data", []) if isinstance(option.get("yAxis"), list) \
1689
1161
  else option.get("yAxis", {}).get("data", [])
1690
1162
  series = option.get("series", [])
@@ -1772,7 +1244,7 @@ def diverging_bar_svg(option: dict, width: int = 720, height: int = 300,
1772
1244
 
1773
1245
  def grouped_bar_svg(option: dict, width: int = 720, height: int = 260,
1774
1246
  note: str = "", lang: str = "zh", ui: dict | None = None) -> str:
1775
- ui = ui or UI_ZH
1247
+ ui = ui or build_ui("zh")
1776
1248
  cats = option.get("xAxis", {}).get("data", [])
1777
1249
  series = option.get("series", [])
1778
1250
  parts = [f'<rect x="0" y="0" width="{width}" height="{height}" fill="#FFFFFF"/>']
@@ -1884,10 +1356,7 @@ def _outcome_support_score(evidence: list[dict], outcome: dict) -> float:
1884
1356
  def first_screen(result: dict, lang: str, ui: dict) -> str:
1885
1357
  """Decision-first hero: meaning before raw counts."""
1886
1358
  decision = result.get("decision", {})
1887
- outcomes = result.get("outcomes", [])
1888
1359
  evidence = result.get("evidence", [])
1889
- ranked = sorted(outcomes, key=lambda o: _outcome_support_score(evidence, o), reverse=True)
1890
- best_type = next((o.get("outcome_type") for o in ranked if o.get("positive_count", 0) > 0), None)
1891
1360
  action = decision.get("recommended_action", "insufficient_evidence")
1892
1361
  cls = {"adopt": "adopt", "pilot": "pilot", "reject": "reject"}.get(action, "")
1893
1362
 
@@ -1924,23 +1393,17 @@ def first_screen(result: dict, lang: str, ui: dict) -> str:
1924
1393
  </div>"""
1925
1394
 
1926
1395
 
1927
- OUTCOME_GROUPS = {
1928
- "task": {"completion_time", "accuracy", "assignment_score", "task_performance", "code_quality"},
1929
- "learning": {"knowledge_gain", "learning_gain", "concept_understanding", "retention", "transfer",
1930
- "independent_problem_solving", "programming_skill", "writing_skill"},
1931
- "risk": {"ai_dependency", "over_reliance", "reduced_effort", "reduced_transfer",
1932
- "academic_integrity_risk", "false_confidence", "cognitive_load"},
1933
- }
1934
-
1935
-
1936
1396
  def render_outcome_separation(result: dict, lang: str, ui: dict) -> str:
1937
1397
  outcomes = effect_outcomes(result)
1938
1398
  if len(outcomes) < 2:
1939
1399
  return ""
1940
1400
  buckets: dict[str, list[dict]] = {"task": [], "learning": [], "risk": [], "other": []}
1401
+ group_map = outcome_group_map()
1941
1402
  for outcome in outcomes:
1942
1403
  kind = outcome.get("outcome_type") or ""
1943
- group = next((name for name, values in OUTCOME_GROUPS.items() if kind in values), "other")
1404
+ group = next((name for name, values in group_map.items() if kind in values), "other")
1405
+ if group not in buckets:
1406
+ group = "other"
1944
1407
  buckets[group].append(outcome)
1945
1408
  group_labels = {
1946
1409
  "task": ui["outcome_group_task"], "learning": ui["outcome_group_learning"],
@@ -2086,31 +1549,7 @@ def _quality_dims_text(lang: str, dims: Any) -> str:
2086
1549
 
2087
1550
  def render_evidence_detail(ev: dict, source: dict, lang: str, ui: dict) -> str:
2088
1551
  """Render complete traceable evidence detail without inventing missing fields."""
2089
- labels = ({
2090
- "study_id": "研究 ID", "sample_id": "样本 ID", "title": "研究标题",
2091
- "year": "年份", "study_type": "研究设计", "education_level": "教育阶段",
2092
- "population": "研究人群", "sample_size": "样本量", "intervention": "干预",
2093
- "comparison": "对照 / 比较条件", "outcome_measure": "结果测量", "claim": "完整主张",
2094
- "effect": "效应 / 结果", "effect_direction": "效应方向", "relation_to_claim": "与主张关系",
2095
- "duration": "干预时长", "method": "方法", "strengths": "优势",
2096
- "limitations": "局限", "confounders": "混杂因素", "quality_dimensions": "质量维度",
2097
- "quality_score": "质量分", "evidence_level": "证据等级", "directness": "直接性",
2098
- "applicability": "适用性", "confidence": "置信度", "status": "证据状态",
2099
- "source_location": "来源位置", "source_title": "来源标题", "source_url": "可验证链接",
2100
- "claim_id": "Claim ID",
2101
- } if lang == "zh" else {
2102
- "study_id": "Study ID", "sample_id": "Sample ID", "title": "Study title",
2103
- "year": "Year", "study_type": "Study design", "education_level": "Education level",
2104
- "population": "Population", "sample_size": "Sample size", "intervention": "Intervention",
2105
- "comparison": "Comparison", "outcome_measure": "Outcome measure", "claim": "Full claim",
2106
- "effect": "Effect / result", "effect_direction": "Effect direction", "relation_to_claim": "Relation to claim",
2107
- "duration": "Duration", "method": "Method", "strengths": "Strengths",
2108
- "limitations": "Limitations", "confounders": "Confounders", "quality_dimensions": "Quality dimensions",
2109
- "quality_score": "Quality score", "evidence_level": "Evidence level", "directness": "Directness",
2110
- "applicability": "Applicability", "confidence": "Confidence", "status": "Evidence status",
2111
- "source_location": "Source location", "source_title": "Source title", "source_url": "Verifiable link",
2112
- "claim_id": "Claim ID",
2113
- })
1552
+ labels = evidence_detail_label_table(lang)
2114
1553
 
2115
1554
  values: list[tuple[str, Any]] = []
2116
1555
  source_title = source.get("title") or ev.get("title")
@@ -2368,9 +1807,10 @@ def render_tribunal(result: dict, workflow_svg: str, tribunal_svg: str, lang: st
2368
1807
 
2369
1808
  def methodology_item_label(lang: str, item: Any) -> str:
2370
1809
  key = str(item or "")
2371
- if lang == "zh":
2372
- return METHODOLOGY_LABELS_ZH.get(key, key.replace("_", " "))
2373
- return key.replace("_", " ").strip().title()
1810
+ table = methodology_label_table(lang)
1811
+ if key in table:
1812
+ return table[key]
1813
+ return key.replace("_", " ").strip().title() if lang != "zh" else key.replace("_", " ")
2374
1814
 
2375
1815
 
2376
1816
  def render_methodology(result: dict, lang: str, ui: dict) -> str:
@@ -2478,7 +1918,8 @@ def render_intervention(result: dict, svg: str, lang: str, ui: dict) -> str:
2478
1918
  if not intervention:
2479
1919
  return f"<p>{esc(ui['no_data'])}</p>"
2480
1920
  population = intervention.get("target_population") or intervention.get("target_learners")
2481
- population_label = ("目标人群" if lang == "zh" else "Target population") if intervention.get("target_population") else ui['intervention_learners']
1921
+ population_key = "intervention_population" if intervention.get("target_population") else "intervention_learners"
1922
+ population_label = ui.get(population_key) or ui.get("intervention_learners") or ("目标人群" if lang == "zh" else "Target population")
2482
1923
  lines = [f"<p><strong>{esc(population_label)}{esc(ui['colon'])}</strong>{esc(population)} · "
2483
1924
  f"<strong>{esc(ui['intervention_duration'])}{esc(ui['colon'])}</strong>{esc(intervention.get('pilot_duration'))}</p>"]
2484
1925
  if intervention.get("ai_usage_policy"):
@@ -2645,16 +2086,16 @@ def resolve_full_report_plan(result: dict) -> list[dict[str, Any]]:
2645
2086
  raw = result.get("report_outline") or result.get("report_structure") or {}
2646
2087
  chapters = raw.get("chapters") if isinstance(raw, dict) else raw if isinstance(raw, list) else None
2647
2088
  if not isinstance(chapters, list) or not 5 <= len(chapters) <= 7:
2648
- return [dict(chapter) for chapter in DEFAULT_FULL_REPORT_PLAN]
2089
+ return default_full_report_plan()
2649
2090
 
2650
2091
  normalized: list[dict[str, Any]] = []
2651
2092
  seen_modules: list[str] = []
2652
2093
  for index, chapter in enumerate(chapters, 1):
2653
2094
  if not isinstance(chapter, dict):
2654
- return [dict(item) for item in DEFAULT_FULL_REPORT_PLAN]
2095
+ return default_full_report_plan()
2655
2096
  modules = [m for m in (chapter.get("modules") or []) if m in FULL_REPORT_MODULES]
2656
2097
  if not modules or any(m in seen_modules for m in modules):
2657
- return [dict(item) for item in DEFAULT_FULL_REPORT_PLAN]
2098
+ return [dict(item) for item in default_full_report_plan()]
2658
2099
  seen_modules.extend(modules)
2659
2100
  key = re.sub(r"[^a-z0-9-]+", "-", str(chapter.get("key") or f"chapter-{index}").lower()).strip("-")
2660
2101
  normalized.append({
@@ -2666,9 +2107,9 @@ def resolve_full_report_plan(result: dict) -> list[dict[str, Any]]:
2666
2107
  "modules": tuple(modules),
2667
2108
  })
2668
2109
  if set(seen_modules) != set(FULL_REPORT_MODULES):
2669
- return [dict(item) for item in DEFAULT_FULL_REPORT_PLAN]
2110
+ return [dict(item) for item in default_full_report_plan()]
2670
2111
  if "decision" not in normalized[0]["modules"] or "sources" not in normalized[-1]["modules"]:
2671
- return [dict(item) for item in DEFAULT_FULL_REPORT_PLAN]
2112
+ return [dict(item) for item in default_full_report_plan()]
2672
2113
  return normalized
2673
2114
 
2674
2115
 
@@ -2699,7 +2140,7 @@ def frame_enum_label(lang: str, value: Any) -> str:
2699
2140
  text = str(value or "")
2700
2141
  if not text:
2701
2142
  return ""
2702
- table = FRAME_ENUM_ZH if lang == "zh" else FRAME_ENUM_EN
2143
+ table = frame_enum_table(lang)
2703
2144
  if text in table:
2704
2145
  return table[text]
2705
2146
  return _humanize_identifier(text, lang)
@@ -2738,7 +2179,7 @@ def _frame_value_label(lang: str, value: Any) -> str:
2738
2179
  if value in (None, "", [], {}):
2739
2180
  return ""
2740
2181
  text_value = str(value)
2741
- table = FRAME_ENUM_ZH if lang == "zh" else FRAME_ENUM_EN
2182
+ table = frame_enum_table(lang)
2742
2183
  if text_value in table:
2743
2184
  return table[text_value]
2744
2185
  if " " in text_value.strip() or _HAS_CJK.search(text_value):
@@ -2752,26 +2193,10 @@ def render_research_scope(result: dict, lang: str, ui: dict) -> str:
2752
2193
  course = frame.get("course", {}) or {}
2753
2194
  intervention = frame.get("intervention", {}) or {}
2754
2195
  scope = frame.get("scope", {}) or {}
2755
- labels = ({
2756
- "question": "研究问题", "learner": "目标学习者", "course": "课程情境", "intervention": "AI 干预",
2757
- "comparison": "比较条件", "outcomes": "结果构念", "scope": "研究范围", "success": "决策成功条件",
2758
- } if lang == "zh" else {
2759
- "question": "Research question", "learner": "Target learners", "course": "Course context", "intervention": "AI intervention",
2760
- "comparison": "Comparison", "outcomes": "Outcome constructs", "scope": "Research scope", "success": "Decision success condition",
2761
- })
2762
- learner_text = labeled_pairs(lang, learner,
2763
- {"education_level":"教育阶段", "major":"专业", "prior_knowledge":"先验知识", "special_characteristics":"学习者特征"},
2764
- {"education_level":"Education level", "major":"Major", "prior_knowledge":"Prior knowledge", "special_characteristics":"Learner characteristics"})
2765
- course_text = labeled_pairs(lang, course,
2766
- {"subject":"课程", "course_type":"课程类型", "duration":"课程周期"},
2767
- {"subject":"Subject", "course_type":"Course type", "duration":"Duration"})
2768
- intervention_text = labeled_pairs(lang, intervention,
2769
- {"teaching_method": "干预方式", "ai_tool": "AI 工具", "allowed_usage": "允许使用",
2770
- "frequency": "使用频率", "duration": "干预周期", "policy_name": "政策名称",
2771
- "policy_type": "政策类型", "jurisdiction": "适用辖区", "mechanism": "作用机制"},
2772
- {"teaching_method": "Method", "ai_tool": "AI tool", "allowed_usage": "Allowed usage",
2773
- "frequency": "Frequency", "duration": "Duration", "policy_name": "Policy",
2774
- "policy_type": "Policy type", "jurisdiction": "Jurisdiction", "mechanism": "Mechanism"})
2196
+ labels = scope_field_labels(lang)
2197
+ learner_text = labeled_pairs(lang, learner, scope_subfield_labels(lang, "learner"), scope_subfield_labels(lang, "learner"))
2198
+ course_text = labeled_pairs(lang, course, scope_subfield_labels(lang, "course"), scope_subfield_labels(lang, "course"))
2199
+ intervention_text = labeled_pairs(lang, intervention, scope_subfield_labels(lang, "intervention"), scope_subfield_labels(lang, "intervention"))
2775
2200
  outcome_map = frame.get("outcomes", {}) or {}
2776
2201
  outcome_parts = []
2777
2202
  for group, values in outcome_map.items():
@@ -2779,8 +2204,8 @@ def render_research_scope(result: dict, lang: str, ui: dict) -> str:
2779
2204
  rendered = "、".join(label(lang, "outcome", v) for v in values) if lang == "zh" else ", ".join(label(lang, "outcome", v) for v in values)
2780
2205
  outcome_parts.append(f"{frame_enum_label(lang, group)}:{rendered}" if lang == "zh" else f"{frame_enum_label(lang, group)}: {rendered}")
2781
2206
  scope_parts = []
2782
- scope_labels_zh = {"time_range":"时间范围", "geography":"地域", "study_types":"研究设计"}
2783
- scope_labels_en = {"time_range":"Time range", "geography":"Geography", "study_types":"Study designs"}
2207
+ scope_labels_zh = scope_subfield_labels("zh", "scope")
2208
+ scope_labels_en = scope_subfield_labels("en", "scope")
2784
2209
  for key, value in scope.items():
2785
2210
  if value in (None, "", [], {}):
2786
2211
  continue
@@ -2890,20 +2315,13 @@ def render_full_report(result: dict, lang: str, ui: dict, charts: dict, infograp
2890
2315
  + render_v2_history(result, lang, ui)),
2891
2316
  }
2892
2317
 
2893
- default_leads = {
2894
- "decision": ("先明确最终裁决与研究边界,再解释为什么。" if lang == "zh" else "State the final adjudication and research boundary before explaining why."),
2895
- "evidence": ("把任务表现、真实学习、保持与风险放在同一证据地图中,但不混为一谈。" if lang == "zh" else "Place task performance, actual learning, retention and risk on one evidence map without conflating them."),
2896
- "quality": ("检查证据为什么可信、哪里冲突,以及哪些结论必须降级。" if lang == "zh" else "Examine why evidence is credible, where it conflicts, and which conclusions require downgrading."),
2897
- "action": ("把可外推范围、护栏和教学动作连接到具体证据。" if lang == "zh" else "Connect applicability, guardrails and teaching actions to specific evidence."),
2898
- "evaluation": ("用独立学习结果验证试点,并预先写清停止条件。" if lang == "zh" else "Validate the pilot with independent learning outcomes and pre-specified stop conditions."),
2899
- "sources": ("保留原始来源、URL、证据 ID 和获取信息,确保可回查。" if lang == "zh" else "Preserve original sources, URLs, evidence IDs and retrieval metadata for auditability."),
2900
- }
2318
+ leads_by_key = default_leads(lang)
2901
2319
 
2902
2320
  plan = resolve_full_report_plan(result)
2903
2321
  rendered = []
2904
2322
  for index, chapter in enumerate(plan, 1):
2905
2323
  content = "".join(module_content.get(module, "") for module in chapter.get("modules", ()))
2906
- lead = chapter.get("lead_zh" if lang == "zh" else "lead_en") or default_leads.get(chapter.get("key"), "")
2324
+ lead = chapter.get("lead_zh" if lang == "zh" else "lead_en") or leads_by_key.get(chapter.get("key"), "")
2907
2325
  rendered.append(render_full_chapter(
2908
2326
  chapter_dom_id(lang, str(chapter.get("key") or f"chapter-{index}"), index),
2909
2327
  full_chapter_title(chapter, lang, index), content, str(lead or "")))
@@ -3082,12 +2500,6 @@ def _enhancer_js(charts_zh: dict, charts_en: dict, result_en: dict) -> str:
3082
2500
  trace_en = next((c for c in charts_en.get("charts", [])
3083
2501
  if c.get("chart_id") == "claim-evidence-trace"), None)
3084
2502
  benchmark_en = charts_en.get("benchmark") or {}
3085
- matrix_rows = [
3086
- {"id": e.get("evidence_id"), "title": e.get("title", ""),
3087
- "direction": e.get("direction", "neutral"),
3088
- "outcome": e.get("outcome_type", ""), "quality": e.get("quality_score", 0)}
3089
- for e in result_en.get("evidence", [])
3090
- ]
3091
2503
  return f"""
3092
2504
  (function () {{
3093
2505
  'use strict';
@@ -3326,24 +2738,7 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
3326
2738
  outcome_chart = next((c for c in charts.get("charts", [])
3327
2739
  if c.get("chart_id") == "outcome-evidence-overview"), None)
3328
2740
 
3329
- if lang == "zh":
3330
- brief_titles = {
3331
- "decision": ("先看结论", "该不该做、置信度多高、最关键的证据边界在哪。"),
3332
- "lieflat": ("Lieflat 实证手作画廊", "AI 按数据形状从 Lieflat 目录选型编排;每张图的数字都可溯源到 result.json。"),
3333
- "outcomes": ("任务表现 ≠ 学习效果", "只展示真正有解释力的结果分离;正向、负向与零效应按 effect_direction 编码。"),
3334
- "tribunal": ("证据裁决", "支持、不确定、被反驳与缺失证据分开放置,不把长段落平铺在同一层。"),
3335
- "action": ("从证据到行动", "适用性、护栏、停止条件与评价连成一条可执行路径。"),
3336
- "sources": ("关键来源", "摘要页只列最关键的来源;完整溯源在完整报告中展开。"),
3337
- }
3338
- else:
3339
- brief_titles = {
3340
- "decision": ("Decision first", "What to do, how confident we are, and the most important evidence boundary."),
3341
- "lieflat": ("Lieflat Editorial Gallery", "Charts selected and composed by AI from the Lieflat catalog; every number traces back to result.json."),
3342
- "outcomes": ("Task performance ≠ learning", "Only informative outcome separation; positive, negative and null effects use effect_direction."),
3343
- "tribunal": ("Evidence tribunal", "Supported, uncertain, contradicted and missing evidence stay separated instead of flattened into long prose."),
3344
- "action": ("Evidence to action", "Applicability, guardrails, stop conditions and evaluation form one executable path."),
3345
- "sources": ("Key sources", "Only the key sources in the brief; full traceability expands in the full report."),
3346
- }
2741
+ brief_titles = brief_block_titles(lang)
3347
2742
 
3348
2743
  lieflat_layout = viz.get("lieflat_layout") or {"entries": []}
3349
2744
  lieflat_meta = (viz.get("lieflat_meta") or {}).get(lang, {})
@@ -3369,7 +2764,7 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
3369
2764
  nav_items = [("decision", brief_titles["decision"][0]), ("outcomes", brief_titles["outcomes"][0]),
3370
2765
  ("tribunal", brief_titles["tribunal"][0]), ("action", brief_titles["action"][0]),
3371
2766
  ("sources", brief_titles["sources"][0])]
3372
- brief_nav = '<nav class="brief-navigation" aria-label="' + ("摘要导航" if lang == "zh" else "Brief navigation") + '">' + ''.join(
2767
+ brief_nav = '<nav class="brief-navigation" aria-label="' + esc(ui["brief_nav_aria"]) + '">' + ''.join(
3373
2768
  f'<a href="#brief-{key}-{lang}"><span>{i:02d}</span>{esc(title)}</a>'
3374
2769
  for i, (key, title) in enumerate(nav_items, 1)) + '</nav>'
3375
2770
  full_report = render_full_report(result, lang, ui, charts, infographics, figures, viz)
@@ -3414,9 +2809,11 @@ def render_html(result_en: dict, result_zh: dict, charts_zh: dict, charts_en: di
3414
2809
  figures_en: dict, theme: str, viz: dict,
3415
2810
  result_sha256: str = "", integrity: dict | None = None) -> str:
3416
2811
  # Theme is fixed at generation time; language remains switchable in the HTML.
3417
- body_zh = render_body(result_zh, "zh", UI_ZH, charts_zh, infographics_zh, figures_zh, viz, theme,
2812
+ ui_zh = build_ui("zh")
2813
+ ui_en = build_ui("en")
2814
+ body_zh = render_body(result_zh, "zh", ui_zh, charts_zh, infographics_zh, figures_zh, viz, theme,
3418
2815
  integrity=integrity)
3419
- body_en = render_body(result_en, "en", UI_EN, charts_en, infographics_en, figures_en, viz, theme,
2816
+ body_en = render_body(result_en, "en", ui_en, charts_en, infographics_en, figures_en, viz, theme,
3420
2817
  integrity=integrity)
3421
2818
  hash_meta = (f'<meta name="eduevidence-result-sha256" content="{esc(result_sha256)}">\n'
3422
2819
  if result_sha256 else "")
@@ -3440,10 +2837,10 @@ def render_html(result_en: dict, result_zh: dict, charts_zh: dict, charts_en: di
3440
2837
  <div class="controls reader-toolbar">
3441
2838
  <a class="reader-home" href="#" aria-label="Back to report start">EduEvidence<span class="generated-theme">{esc(THEME_DISPLAY[theme])}</span></a>
3442
2839
  <div class="reader-view-controls" role="group" aria-label="Report view">
3443
- <button type="button" class="report-view-btn active" data-report-view="brief" data-copy="brief" aria-pressed="true">摘要</button>
3444
- <button type="button" class="report-view-btn" data-report-view="full" data-copy="full" aria-pressed="false">完整报告</button>
2840
+ <button type="button" class="report-view-btn active" data-report-view="brief" data-copy="brief" aria-pressed="true">{esc(ui_zh['toolbar_brief'])}</button>
2841
+ <button type="button" class="report-view-btn" data-report-view="full" data-copy="full" aria-pressed="false">{esc(ui_zh['toolbar_full'])}</button>
3445
2842
  </div>
3446
- <div class="reader-tools">{_lang_switcher(UI_ZH, UI_EN)}<button class="reader-print" type="button" data-copy="print">打印</button></div>
2843
+ <div class="reader-tools">{_lang_switcher(ui_zh, ui_en)}<button class="reader-print" type="button" data-copy="print">{esc(ui_zh['toolbar_print'])}</button></div>
3447
2844
  </div>
3448
2845
  {body_zh}
3449
2846
  {body_en}
@@ -3540,6 +2937,13 @@ def main() -> int:
3540
2937
  return 2
3541
2938
  result_zh = json.loads(zh_path.read_text(encoding="utf-8"))
3542
2939
 
2940
+ # 0. Domain copy pack(result.meta.domain / frame.extensions.domain)
2941
+ try:
2942
+ activate_copy_pack(result_en)
2943
+ except AssertionError as exc:
2944
+ print(f"REPORT_INVALID — domain copy pack rejected:\n{exc}")
2945
+ return 2
2946
+
3543
2947
  # 1. Contract validation(两份数据分别校验)
3544
2948
  for label, data in (("result.json", result_en), ("result.zh.json", result_zh)):
3545
2949
  problems = validate_contract(data)
@@ -3569,8 +2973,8 @@ def main() -> int:
3569
2973
  # 3. Adapters(两份数据分别生成 spec / 信息图 / 学术图;数字同构)
3570
2974
  charts_zh = build_chart_specs(result_zh, lang="zh")
3571
2975
  charts_en = build_chart_specs(result_en, lang="en")
3572
- infographics_zh = build_infographics(result_zh, lang="zh")
3573
- infographics_en = build_infographics(result_en, lang="en")
2976
+ infographics_zh = build_infographics(result_zh, lang="zh", ui=build_ui("zh"))
2977
+ infographics_en = build_infographics(result_en, lang="en", ui=build_ui("en"))
3574
2978
  figure_data = build_figure_data(result_en)
3575
2979
  figures_zh = render_figures(figure_data, theme=args.theme, lang="zh")
3576
2980
  figures_en = render_figures(figure_data, theme=args.theme, lang="en")
@@ -3647,5 +3051,14 @@ def main() -> int:
3647
3051
  return 0
3648
3052
 
3649
3053
 
3054
+ def __getattr__(name: str):
3055
+ """Back-compat UI dicts for tests: br.UI_ZH / br.UI_EN."""
3056
+ if name == "UI_ZH":
3057
+ return build_ui("zh")
3058
+ if name == "UI_EN":
3059
+ return build_ui("en")
3060
+ raise AttributeError(name)
3061
+
3062
+
3650
3063
  if __name__ == "__main__":
3651
3064
  sys.exit(main())