eduevidence 6.0.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.
- package/CHANGELOG.md +395 -0
- package/CONTRIBUTING.md +105 -0
- package/README.md +113 -49
- package/README.zh-CN.md +39 -12
- package/SKILL.md +15 -5
- package/assets/readme/landing-tour.gif +0 -0
- package/assets/readme/studio-tour.gif +0 -0
- package/benchmarks/evidence-library.json +277 -1
- package/bin/eduevidence.js +2 -1
- package/docs/architecture.md +325 -46
- package/docs/demo-workplace-ai.md +1 -1
- package/docs/install-guide.md +1 -1
- package/docs/j-ev-experimental.md +250 -0
- package/docs/orchestration-role-model.md +1 -1
- package/docs/release-closeout/README.md +1 -1
- package/docs/reproducibility.md +138 -0
- package/docs/sciverse-api.md +125 -0
- package/domains/_neutral/copy/few_shots.json +21 -0
- package/domains/_neutral/copy/framing_lexicon.json +19 -0
- package/domains/_neutral/copy/module_labels.json +5 -0
- package/domains/_neutral/copy/module_labels_footer.json +102 -0
- package/domains/_neutral/copy/module_labels_modules.json +204 -0
- package/domains/_neutral/copy/module_labels_nav.json +126 -0
- package/domains/_neutral/copy/module_labels_summary.json +98 -0
- package/domains/_neutral/copy/module_labels_tables.json +164 -0
- package/domains/_neutral/copy/module_labels_v2.json +90 -0
- package/domains/_neutral/copy/risk_constructs.json +20 -0
- package/domains/_neutral/copy/section_titles.json +66 -0
- package/domains/_neutral/copy/terminology.json +11 -0
- package/domains/check_copy_packs.py +103 -0
- package/domains/education/copy/few_shots.json +22 -0
- package/domains/education/copy/framing_enums.json +167 -0
- package/domains/education/copy/framing_lexicon.json +166 -0
- package/domains/education/copy/module_labels.json +169 -0
- package/domains/education/copy/risk_constructs.json +48 -0
- package/domains/education/copy/section_titles.json +186 -0
- package/domains/education/copy/terminology.json +70 -0
- package/domains/education/manifest.json +1 -1
- package/domains/education/outcome_taxonomy.json +2 -2
- package/domains/manifest.json +1 -1
- package/domains/policy/copy/few_shots.json +22 -0
- package/domains/policy/copy/framing_enums.json +94 -0
- package/domains/policy/copy/framing_lexicon.json +174 -0
- package/domains/policy/copy/module_labels.json +168 -0
- package/domains/policy/copy/risk_constructs.json +33 -0
- package/domains/policy/copy/section_titles.json +186 -0
- package/domains/policy/copy/terminology.json +64 -0
- package/eduevidence_cli.py +10 -0
- package/engine/capabilities.py +57 -5
- package/engine/decision_policy.py +167 -0
- package/engine/evidence_graph.py +14 -10
- package/engine/gaps.py +42 -22
- package/engine/ids.py +2 -0
- package/engine/library.py +6 -2
- package/engine/library_builtin.py +7 -4
- package/engine/living.py +34 -4
- package/engine/migration.py +88 -3
- package/engine/orchestration.py +5 -5
- package/engine/paths.py +2 -0
- package/engine/pilot.py +34 -32
- package/engine/taxonomy.py +211 -0
- package/engine/tribunal.py +49 -43
- package/engine/versions.py +1 -1
- package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +1361 -147
- package/examples/ai-coding-assistant-evidence/artifact_manifest.json +3 -3
- package/examples/ai-coding-assistant-evidence/citation_check.json +1 -1
- package/examples/ai-coding-assistant-evidence/final_verdict.json +107 -0
- package/examples/ai-coding-assistant-evidence/gate_report.json +101 -0
- package/examples/ai-coding-assistant-evidence/report_spec.json +23 -12
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +1360 -146
- package/examples/ai-coding-assistant-evidence/result.json +13 -9
- package/examples/ai-coding-assistant-evidence/result.zh.json +45 -41
- package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
- package/examples/ai-coding-assistant-evidence/verdict.json +6 -2
- package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
- package/examples/spaced-retrieval-practice/applicability.json +14 -0
- package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
- package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
- package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
- package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
- package/examples/spaced-retrieval-practice/frame.json +58 -0
- package/examples/spaced-retrieval-practice/gate_report.json +101 -0
- package/examples/spaced-retrieval-practice/methodology.json +78 -0
- package/examples/spaced-retrieval-practice/report.html +2522 -0
- package/examples/spaced-retrieval-practice/report_spec.json +212 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
- package/examples/spaced-retrieval-practice/result.json +942 -0
- package/examples/spaced-retrieval-practice/result.zh.json +942 -0
- package/examples/spaced-retrieval-practice/skeptic.json +70 -0
- package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
- package/examples/spaced-retrieval-practice/verdict.json +93 -0
- package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
- package/examples/workplace-ai-assistant/artifact_manifest.json +15 -0
- package/examples/workplace-ai-assistant/claims.jsonl +4 -4
- package/examples/workplace-ai-assistant/evidence.jsonl +4 -4
- package/examples/workplace-ai-assistant/evidence_graph.json +15 -15
- package/examples/workplace-ai-assistant/final_verdict.json +78 -0
- package/examples/workplace-ai-assistant/gate_report.json +101 -0
- package/examples/workplace-ai-assistant/report_spec.json +209 -40
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
- package/examples/workplace-ai-assistant/result.json +82 -20
- package/examples/workplace-ai-assistant/result.zh.json +82 -20
- package/examples/workplace-ai-assistant/skeptic.json +72 -0
- package/examples/workplace-ai-assistant/verdict.json +36 -10
- package/integrations/agent_mcp.py +2 -2
- package/integrations/jev/__init__.py +115 -0
- package/integrations/jev/approval.py +212 -0
- package/integrations/jev/cli.py +84 -0
- package/integrations/jev/config.py +112 -0
- package/integrations/jev/gateway.py +128 -0
- package/integrations/jev/modes.py +38 -0
- package/integrations/jev/tools_classify.py +88 -0
- package/integrations/jev/tools_extract.py +111 -0
- package/integrations/jev/tools_rerank.py +71 -0
- package/integrations/jev/tools_screen.py +87 -0
- package/integrations/jev/tools_verify.py +95 -0
- package/integrations/jev_mcp.py +22 -0
- package/integrations/semantic_decide.py +286 -0
- package/integrations/semdecide_cli.py +55 -0
- package/package.json +19 -2
- package/pyproject.toml +4 -3
- package/references/report-copy-style.md +107 -0
- package/references/retrieval-compliance.md +75 -0
- package/references/retrieval-protocol.md +20 -0
- package/retrieval/audit.py +27 -3
- package/retrieval/fetch.py +96 -0
- package/retrieval/sciverse.py +398 -0
- package/retrieval/search.py +47 -7
- package/schemas/applicability.schema.json +94 -0
- package/schemas/chart-spec.schema.json +10 -3
- package/schemas/evidence.schema.json +316 -43
- package/schemas/fetch-result.schema.json +2 -1
- package/schemas/report-result.schema.json +3 -3
- package/schemas/report-spec.schema.json +98 -100
- package/schemas/skeptic.schema.json +86 -0
- package/schemas/source.schema.json +21 -2
- package/schemas/v2/decision-snapshot.schema.json +20 -9
- package/schemas/v2/finding.schema.json +5 -1
- package/schemas/v2/intake.schema.json +191 -0
- package/schemas/v2/methodology-audit.schema.json +5 -1
- package/schemas/v2/outcome.schema.json +28 -5
- package/schemas/v2/study.schema.json +5 -1
- package/schemas/vNext/autoevolve-session.schema.json +34 -1
- package/schemas/vNext/eval-snapshot.schema.json +77 -1
- package/schemas/vNext/execution-plan.schema.json +50 -1
- package/schemas/vNext/gap-priority.schema.json +54 -1
- package/schemas/vNext/negative-search-record.schema.json +68 -1
- package/schemas/vNext/research-iteration.schema.json +87 -1
- package/schemas/vNext/research-strategy.schema.json +62 -1
- package/schemas/vNext/skill-experiment.schema.json +90 -1
- package/schemas/vNext/task-spec.schema.json +156 -1
- package/schemas/vNext/worker-result.schema.json +60 -1
- package/schemas/verdict.schema.json +164 -28
- package/scripts/build_evidence_library.py +15 -5
- package/scripts/build_report_variants.py +18 -2
- package/scripts/build_result.py +74 -9
- package/scripts/check_package_parity.py +85 -0
- package/scripts/check_protocol_alignment.py +375 -0
- package/scripts/check_versioned_schemas.py +254 -0
- package/scripts/claim_audit.py +13 -8
- package/scripts/compute_confidence.py +10 -0
- package/scripts/dashboard_server.py +13 -2
- package/scripts/did_regression.py +12 -2
- package/scripts/evidence_score.py +5 -2
- package/scripts/intake/__init__.py +31 -0
- package/scripts/intake/__main__.py +18 -0
- package/scripts/intake/background.py +78 -0
- package/scripts/intake/browser.py +79 -0
- package/scripts/intake/cli.py +57 -0
- package/scripts/intake/constants.py +57 -0
- package/scripts/intake/depth.py +53 -0
- package/scripts/intake/enhancements.py +106 -0
- package/scripts/intake/hooks.py +90 -0
- package/scripts/intake/prefs.py +76 -0
- package/scripts/intake/prompts.py +85 -0
- package/scripts/intake/session.py +152 -0
- package/scripts/lint_file_layers.py +126 -0
- package/scripts/orchestrator.py +187 -40
- package/scripts/pre_verdict_gate.py +241 -29
- package/scripts/quickstart.py +18 -2
- package/scripts/run_workspace.py +7 -1
- package/scripts/skill_lint.py +11 -1
- package/scripts/skill_payload.py +6 -3
- package/scripts/test_adversarial_empirical.py +96 -25
- package/scripts/validate_schema.py +31 -1
- package/skill/agents/evaluation-designer.md +20 -4
- package/skill/agents/evidence-analyst.md +19 -3
- package/skill/agents/evidence-judge.md +98 -8
- package/skill/agents/evidence-retriever.md +20 -3
- package/skill/agents/intervention-designer.md +20 -4
- package/skill/agents/method-reviewer.md +18 -2
- package/skill/agents/{education-planner.md → research-planner.md} +19 -3
- package/skill/agents/skeptic.md +18 -2
- package/skill/roles/registry.yaml +11 -11
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
- package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
- package/skill/sub-skills/data-analysis/SKILL.md +34 -15
- package/skill/sub-skills/ethics-review/SKILL.md +33 -10
- package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
- package/skill/sub-skills/evidence-review/SKILL.md +31 -12
- package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
- package/skill/sub-skills/literature-review/SKILL.md +35 -14
- package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
- package/skill/sub-skills/report-generation/SKILL.md +28 -0
- package/skill/sub-skills/research-planning/SKILL.md +41 -14
- package/skill/sub-skills/study-design/SKILL.md +30 -9
- package/skill/task-briefs/adjudicate.md +32 -7
- package/skill/task-briefs/applicability.md +37 -2
- package/skill/task-briefs/audit.md +32 -7
- package/skill/task-briefs/challenge.md +34 -5
- package/skill/task-briefs/evaluate.md +30 -5
- package/skill/task-briefs/extract.md +31 -8
- package/skill/task-briefs/frame.md +39 -10
- package/skill/task-briefs/intervene.md +32 -6
- package/skill/task-briefs/present.md +32 -8
- package/skill/task-briefs/projection.md +36 -2
- package/skill/task-briefs/retrieve.md +36 -6
- package/skill/workflows/decision-and-pilot.md +76 -1
- package/skill/workflows/evaluate-and-update.md +83 -0
- package/skill/workflows/evidence-review.md +104 -0
- package/skill/workflows/experimental-jev.md +170 -0
- package/skill/workflows/intake.md +120 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +25 -3
- package/visualization/eduevidence-report/scripts/build_infographics.py +37 -15
- package/visualization/eduevidence-report/scripts/build_report.py +435 -575
- package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +349 -38
- package/visualization/eduevidence-report/scripts/report_copy_pack.py +296 -0
- package/visualization/eduevidence-report/scripts/report_copy_policy_guard.py +47 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +141 -1
- package/web/architecture.html +14885 -0
- package/web/studio/assets/index-B8tkF44Q.css +1 -0
- package/web/studio/index.html +2 -2
- package/scripts/build_esl_artifacts.py +0 -1921
- package/scripts/build_killer_demo.py +0 -295
- package/scripts/enrich_projects_human_and_lieflat.py +0 -315
- package/scripts/generate_new_projects.py +0 -686
- package/scripts/sync_killer_demo_report.py +0 -270
- package/web/studio/assets/index-CzXocaGv.css +0 -1
- /package/web/studio/assets/{index-pa7jD7n4.js → index-CQ6Keoyc.js} +0 -0
|
@@ -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,
|
|
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,56 +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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
"undergraduate_year_1": "大学一年级",
|
|
100
|
-
"computer_science": "计算机科学与技术",
|
|
101
|
-
"C_programming": "C 语言程序设计",
|
|
102
|
-
"compulsory_core_course": "必修核心课程",
|
|
103
|
-
"primary": "主要结果",
|
|
104
|
-
"secondary": "次要结果",
|
|
105
|
-
"risk": "风险结果",
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
FRAME_ENUM_EN = {
|
|
109
|
-
"teaching_decision": "Teaching decision",
|
|
110
|
-
"undergraduate_year_1": "First-year undergraduate",
|
|
111
|
-
"computer_science": "Computer science",
|
|
112
|
-
"C_programming": "C programming",
|
|
113
|
-
"compulsory_core_course": "Compulsory core course",
|
|
114
|
-
"primary": "Primary outcomes",
|
|
115
|
-
"secondary": "Secondary outcomes",
|
|
116
|
-
"risk": "Risk outcomes",
|
|
117
|
-
}
|
|
118
83
|
|
|
119
84
|
DIR_LABEL = {"support": "支持", "contradict": "反驳", "neutral": "中性"}
|
|
120
85
|
DIR_CLASS = {"support": "pos", "contradict": "neg", "neutral": "neu"}
|
|
@@ -125,359 +90,6 @@ EFFECT_CLASS = {"positive": "pos", "negative": "neg", "null": "neu", "neutral":
|
|
|
125
90
|
# 双语 UI 文案
|
|
126
91
|
# ---------------------------------------------------------------------------
|
|
127
92
|
|
|
128
|
-
UI_ZH = {
|
|
129
|
-
"theme_label": "主题",
|
|
130
|
-
"lang_label": "语言",
|
|
131
|
-
"zh": "中文",
|
|
132
|
-
"en": "EN",
|
|
133
|
-
"visual_brief": "可视化摘要",
|
|
134
|
-
"full_report": "完整报告",
|
|
135
|
-
"contents": "目录",
|
|
136
|
-
"collapse_contents": "收起目录",
|
|
137
|
-
"expand_contents": "展开目录",
|
|
138
|
-
"expand_evidence": "查看完整证据",
|
|
139
|
-
"expand_methodology": "查看审计依据",
|
|
140
|
-
"expand_source": "查看来源与溯源",
|
|
141
|
-
"expand_details": "展开完整说明",
|
|
142
|
-
"what_this_means": "这意味着什么",
|
|
143
|
-
"original_title": "原文标题",
|
|
144
|
-
"original_text": "原文",
|
|
145
|
-
"full_report_intro": "结论前置:全部可追溯证据与方法学细节都在这里,关键论证位置穿插有意义的可视化,每个数字都能回查到 result.json。",
|
|
146
|
-
"section_titles": {
|
|
147
|
-
"01": "01 执行决策", "02": "02 结果证据概览", "03": "03 证据矩阵",
|
|
148
|
-
"04": "04 证据裁决", "05": "05 方法学审计", "06": "06 冲突分析",
|
|
149
|
-
"07": "07 主张-证据追溯", "08": "08 适用性", "09": "09 教学干预",
|
|
150
|
-
"10": "10 评价方案", "11": "11 基准测试", "12": "12 来源与溯源",
|
|
151
|
-
},
|
|
152
|
-
"section_leads": {
|
|
153
|
-
"01": "本节先给结论:最终怎么裁决、置信度多高、靠哪几条证据。",
|
|
154
|
-
"02": "一图看清:哪些学习结果有支持证据、哪些被反驳。",
|
|
155
|
-
"03": "每条证据来自哪项研究、测了什么、方向与质量如何;可筛选、可搜索。",
|
|
156
|
-
"04": "证据允许主张什么、不允许主张什么;缺失的关键证据是什么。",
|
|
157
|
-
"05": "研究质量可靠吗?哪些方法学问题让结论打折。",
|
|
158
|
-
"06": "不同研究为何结论不同;分歧出在哪一环。",
|
|
159
|
-
"07": "从结论到证据到原始来源,每一步都可追查。",
|
|
160
|
-
"08": "结论适用于谁、什么课程与结果、需要什么条件。",
|
|
161
|
-
"09": "试点怎么分阶段放开 AI 规则;什么情况必须叫停。",
|
|
162
|
-
"10": "如何验证效果:指标、对照、成功阈值。",
|
|
163
|
-
"11": "EduEvidence 自身基准表现:引用精度与成本。",
|
|
164
|
-
"12": "每篇文献是谁、出自哪里、如何获取。",
|
|
165
|
-
},
|
|
166
|
-
"decision_kpi": ["决策", "置信度", "证据最充分的结果", "最不确定的结果", "主要风险", "来源数量"],
|
|
167
|
-
"summary_title": "一句话结论",
|
|
168
|
-
"summary_question": "问题",
|
|
169
|
-
"summary_evidence": "依据",
|
|
170
|
-
"summary_action": "行动",
|
|
171
|
-
"outcome_table": ["结果类型", "正向效应", "负向效应", "零效应", "证据"],
|
|
172
|
-
"figure1_caption": "图 1. 各结果类型的正向 / 负向 / 零效应证据数量(基于 effect_direction,不等同于 Claim 是否被支持)。",
|
|
173
|
-
"matrix_filter": "筛选 / 搜索",
|
|
174
|
-
"matrix_search_ph": "搜索证据…",
|
|
175
|
-
"matrix_all_dir": "全部效应",
|
|
176
|
-
"matrix_all_outcome": "全部结果",
|
|
177
|
-
"matrix_heads": ["ID", "结果", "效应", "质量", "主张", "来源"],
|
|
178
|
-
"matrix_details": "查看完整证据",
|
|
179
|
-
"matrix_detail_labels": ["研究标题", "研究设计", "人群", "干预", "直接性"],
|
|
180
|
-
"matrix_source_missing": "无可验证来源",
|
|
181
|
-
"hero_action": "建议决策",
|
|
182
|
-
"hero_confidence": "置信度",
|
|
183
|
-
"hero_supported": "最强支持结论",
|
|
184
|
-
"hero_uncertain": "关键不确定性 / 反例",
|
|
185
|
-
"hero_risk": "主要风险",
|
|
186
|
-
"hero_next": "下一步",
|
|
187
|
-
"hero_provenance": "证据 / 来源",
|
|
188
|
-
"outcome_separation_title": "结果分离 · 任务表现 ≠ 学习效果",
|
|
189
|
-
"effect_positive": "正向效应",
|
|
190
|
-
"effect_negative": "负向效应",
|
|
191
|
-
"effect_null": "零效应",
|
|
192
|
-
"outcome_group_task": "任务 / 近端表现",
|
|
193
|
-
"outcome_group_learning": "学习 / 保持 / 迁移",
|
|
194
|
-
"outcome_group_risk": "风险 / 依赖",
|
|
195
|
-
"outcome_group_other": "其他结果",
|
|
196
|
-
"outcome_sep_note": "将不同结果类型分开裁决,避免把训练时更快、更高分直接等同于真正学会。",
|
|
197
|
-
"tribunal_decision": "决策",
|
|
198
|
-
"tribunal_confidence": "置信度",
|
|
199
|
-
"tribunal_can": "可以主张",
|
|
200
|
-
"tribunal_uncertain": "尚不能主张",
|
|
201
|
-
"tribunal_cannot": "被反驳的主张",
|
|
202
|
-
"tribunal_missing": "缺失证据",
|
|
203
|
-
"tribunal_flow": "EvidenceFlow 协议",
|
|
204
|
-
"tribunal_figure": "裁决信息图",
|
|
205
|
-
"method_audit_heads": ["检查项", "状态", "说明"],
|
|
206
|
-
"method_guard": "任务 vs 学习护栏",
|
|
207
|
-
"conflict_verdict": "裁决说明",
|
|
208
|
-
"trace_decision": "决策",
|
|
209
|
-
"trace_claim_prefix": "主张",
|
|
210
|
-
"trace_no_source": "无来源",
|
|
211
|
-
"applicability": ["适用于谁", "适用课程", "适用结果", "适用条件", "目标人群", "目标情境"],
|
|
212
|
-
"intervention_learners": "目标学习者",
|
|
213
|
-
"intervention_duration": "试点时长",
|
|
214
|
-
"intervention_policy": "AI 使用规则",
|
|
215
|
-
"intervention_rule": "AI 规则",
|
|
216
|
-
"intervention_activities": "活动",
|
|
217
|
-
"intervention_check": "结果检查",
|
|
218
|
-
"intervention_stop": "停止条件",
|
|
219
|
-
"intervention_timeline": "干预时间线信息图",
|
|
220
|
-
"evaluation_question": "研究问题",
|
|
221
|
-
"evaluation_measures": ["基线", "后测", "保持测试", "迁移测试"],
|
|
222
|
-
"evaluation_metrics": ["过程指标", "学习指标", "风险指标"],
|
|
223
|
-
"evaluation_threshold": "成功阈值",
|
|
224
|
-
"evaluation_plan": "分析计划",
|
|
225
|
-
"evaluation_figure": "评价设计信息图",
|
|
226
|
-
"benchmark_note": "result.json 未携带 benchmark.baselines,本图不绘制;基准表现见独立基准报告。",
|
|
227
|
-
"sources_title": "来源列表",
|
|
228
|
-
"sources_heads": ["ID", "标题", "年份", "权威级别", "可验证位置"],
|
|
229
|
-
"provenance_title": "Fetch 溯源",
|
|
230
|
-
"provenance_heads": ["来源", "Fetch 方式", "状态", "降级", "时间"],
|
|
231
|
-
"provenance_search": "搜索提供方",
|
|
232
|
-
"provenance_time": "检索时间",
|
|
233
|
-
"provenance_empty": "无逐条 fetch 记录(来源由研究管线直接提供)。",
|
|
234
|
-
"no_data": "无数据。",
|
|
235
|
-
"header_evidence": "证据 ",
|
|
236
|
-
"header_sources": "来源 ",
|
|
237
|
-
"header_mode": "模式:",
|
|
238
|
-
"header_generated": "生成时间:",
|
|
239
|
-
"header_evidence_suffix": " 条",
|
|
240
|
-
"header_sources_suffix": " 个",
|
|
241
|
-
"footer_schema": "Schema",
|
|
242
|
-
"footer_claims": "Claim Binding",
|
|
243
|
-
"footer_numbers": "Numeric Consistency",
|
|
244
|
-
"footer_bilingual": "Bilingual Structure",
|
|
245
|
-
"footer_language": "语言人话化",
|
|
246
|
-
"footer_no_false_precision": "无伪精度",
|
|
247
|
-
"footer_lieflat_bound": "Lieflat 数据溯源",
|
|
248
|
-
"footer_no_axis_distortion": "坐标轴无失真",
|
|
249
|
-
"footer_colorblind_safe": "色盲安全",
|
|
250
|
-
"footer": "EduEvidence 证据报告 · {integrity} · 单文件离线可打开 · 数据源:result.json",
|
|
251
|
-
"matrix_search_label": "筛选 / 搜索证据",
|
|
252
|
-
"matrix_dir_filter": "按效应方向筛选",
|
|
253
|
-
"matrix_outcome_filter": "按结果类型筛选",
|
|
254
|
-
"svg_balance_title": "各结果类型证据效应分布",
|
|
255
|
-
"svg_balance_desc": "各结果类型的正向 / 负向 / 零效应证据条数(基于 effect_direction,不等同于 Claim 是否被支持)。",
|
|
256
|
-
"svg_figure1_title": "各结果类型效应方向分布(出版级学术图)",
|
|
257
|
-
"svg_figure1_desc": "各结果类型的正向 / 负向 / 零效应证据条数;计数轴整数刻度,不随主题变化。来源:EduEvidence result.json。",
|
|
258
|
-
"svg_benchmark_title": "基准对比:各基线引用支持精度",
|
|
259
|
-
"svg_benchmark_desc": "各基线的引用支持精度(Citation support precision)对比;无基准数据时不绘制。",
|
|
260
|
-
"svg_workflow_title": "EvidenceFlow 协议流程",
|
|
261
|
-
"svg_workflow_desc": "从问题框架、检索、抓取验证、证据抽取、反方质疑、方法审计、裁决到适用性与干预评价的完整流程。",
|
|
262
|
-
"svg_tribunal_title": "证据裁决信息图",
|
|
263
|
-
"svg_tribunal_desc": "可以主张与不可主张的证据 ID 与建议决策徽章;完整主张文本见下方裁决卡片。",
|
|
264
|
-
"svg_intervention_title": "教学干预时间线",
|
|
265
|
-
"svg_intervention_desc": "各试点阶段的短名称与活动数量;完整 AI 使用规则见阶段说明块。",
|
|
266
|
-
"svg_evaluation_title": "评价设计流程",
|
|
267
|
-
"svg_evaluation_desc": "基线、后测、保持测试与迁移测试的评价流程;完整指标与分析计划见评估章节。",
|
|
268
|
-
"raw_tag_title": "原始标识",
|
|
269
|
-
"summary_tag_support": "支持",
|
|
270
|
-
"summary_tag_contradict": "反驳",
|
|
271
|
-
"summary_confidence_prefix": "(置信度:",
|
|
272
|
-
"summary_confidence_suffix": ")",
|
|
273
|
-
"method_target": "审查目标",
|
|
274
|
-
"applicability_not_suitable": "不适用于",
|
|
275
|
-
"applicability_conditions": "适用条件",
|
|
276
|
-
"trace_claim_sep": ":",
|
|
277
|
-
"colon": ":",
|
|
278
|
-
"lang_switcher_aria": "语言切换 / Language switch",
|
|
279
|
-
"theme_switcher_aria": "主题 / Theme",
|
|
280
|
-
"v2_project_title": "项目与研究历史",
|
|
281
|
-
"v2_project_id": "项目 ID",
|
|
282
|
-
"v2_graph_revision": "证据图版本",
|
|
283
|
-
"v2_decision_snapshot": "决策快照",
|
|
284
|
-
"v2_timeline": "项目时间线",
|
|
285
|
-
"v2_gaps_title": "知识缺口",
|
|
286
|
-
"v2_gap_type": "缺口类型",
|
|
287
|
-
"v2_gap_priority": "优先级",
|
|
288
|
-
"v2_gap_reasoning": "依据",
|
|
289
|
-
"v2_design_title": "研究设计",
|
|
290
|
-
"v2_design_type": "设计类型",
|
|
291
|
-
"v2_design_question": "研究问题",
|
|
292
|
-
"v2_provenance_title": "数据集与分析溯源",
|
|
293
|
-
"v2_diff_title": "决策变更",
|
|
294
|
-
"v2_diff_action": "决策动作",
|
|
295
|
-
"v2_diff_confidence": "置信度",
|
|
296
|
-
"v2_diff_claims": "变更的主张",
|
|
297
|
-
"v2_diff_gaps": "已解决/新增缺口",
|
|
298
|
-
"v2_revision": "版本",
|
|
299
|
-
"v2_decision": "决策",
|
|
300
|
-
"v2_no_v2_data": "(无 V2 项目数据)",
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
UI_EN = {
|
|
304
|
-
"theme_label": "Theme",
|
|
305
|
-
"lang_label": "Language",
|
|
306
|
-
"zh": "中文",
|
|
307
|
-
"en": "EN",
|
|
308
|
-
"visual_brief": "Visual Brief",
|
|
309
|
-
"full_report": "Full Report",
|
|
310
|
-
"contents": "Contents",
|
|
311
|
-
"collapse_contents": "Collapse contents",
|
|
312
|
-
"expand_contents": "Expand contents",
|
|
313
|
-
"expand_evidence": "View full evidence",
|
|
314
|
-
"expand_methodology": "View audit rationale",
|
|
315
|
-
"expand_source": "View source & provenance",
|
|
316
|
-
"expand_details": "Expand full explanation",
|
|
317
|
-
"what_this_means": "What this means",
|
|
318
|
-
"original_title": "Original title",
|
|
319
|
-
"original_text": "Original text",
|
|
320
|
-
"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.",
|
|
321
|
-
"section_titles": {
|
|
322
|
-
"01": "01 Executive Decision", "02": "02 Outcome Evidence Overview",
|
|
323
|
-
"03": "03 Evidence Matrix", "04": "04 Evidence Tribunal",
|
|
324
|
-
"05": "05 Methodology Audit", "06": "06 Conflict Analysis",
|
|
325
|
-
"07": "07 Claim-Evidence Trace", "08": "08 Applicability",
|
|
326
|
-
"09": "09 Teaching Intervention", "10": "10 Evaluation Plan",
|
|
327
|
-
"11": "11 Benchmark", "12": "12 Sources & Provenance",
|
|
328
|
-
},
|
|
329
|
-
"section_leads": {
|
|
330
|
-
"01": "The verdict first: what we decide, at what confidence, on which evidence.",
|
|
331
|
-
"02": "At a glance: which learning outcomes have supporting evidence, which are contradicted.",
|
|
332
|
-
"03": "Where each piece of evidence comes from, what it measures, its direction and quality — filterable and searchable.",
|
|
333
|
-
"04": "What the evidence lets us claim, what it does not, and what is still missing.",
|
|
334
|
-
"05": "How reliable are these studies, and which methodological concerns discount the conclusions.",
|
|
335
|
-
"06": "Why studies disagree — and where exactly they diverge.",
|
|
336
|
-
"07": "Every step from conclusion to evidence to source stays traceable.",
|
|
337
|
-
"08": "Who the conclusion applies to, for which course and outcomes, under what conditions.",
|
|
338
|
-
"09": "How AI usage rules phase in during a pilot, and when we must stop.",
|
|
339
|
-
"10": "How we verify real effects: metrics, comparison, success threshold.",
|
|
340
|
-
"11": "How EduEvidence itself performs: citation precision and cost.",
|
|
341
|
-
"12": "Who wrote each cited study, where it came from, how it was fetched.",
|
|
342
|
-
},
|
|
343
|
-
"decision_kpi": ["Decision", "Confidence", "Best-supported outcome", "Most uncertain outcome", "Main risk", "Sources"],
|
|
344
|
-
"summary_title": "Bottom line",
|
|
345
|
-
"summary_question": "Question",
|
|
346
|
-
"summary_evidence": "Evidence",
|
|
347
|
-
"summary_action": "Action",
|
|
348
|
-
"outcome_table": ["Outcome", "Positive effect", "Negative effect", "Null effect", "Evidence"],
|
|
349
|
-
"figure1_caption": "Fig. 1. Positive / negative / null effect counts per outcome (based on effect_direction, not claim support).",
|
|
350
|
-
"matrix_filter": "Filter / Search",
|
|
351
|
-
"matrix_search_ph": "Search evidence…",
|
|
352
|
-
"matrix_all_dir": "All effects",
|
|
353
|
-
"matrix_all_outcome": "All outcomes",
|
|
354
|
-
"matrix_heads": ["ID", "Outcome", "Effect", "Quality", "Claim", "Source"],
|
|
355
|
-
"matrix_details": "View full evidence",
|
|
356
|
-
"matrix_detail_labels": ["Study title", "Design", "Population", "Intervention", "Directness"],
|
|
357
|
-
"matrix_source_missing": "No verifiable source",
|
|
358
|
-
"hero_action": "Recommended decision",
|
|
359
|
-
"hero_confidence": "Confidence",
|
|
360
|
-
"hero_supported": "Strongest supported conclusion",
|
|
361
|
-
"hero_uncertain": "Key uncertainty / contradiction",
|
|
362
|
-
"hero_risk": "Main risk",
|
|
363
|
-
"hero_next": "Next action",
|
|
364
|
-
"hero_provenance": "Evidence / sources",
|
|
365
|
-
"outcome_separation_title": "Outcome Separation · Task performance ≠ learning",
|
|
366
|
-
"effect_positive": "Positive effect",
|
|
367
|
-
"effect_negative": "Negative effect",
|
|
368
|
-
"effect_null": "Null effect",
|
|
369
|
-
"outcome_group_task": "Task / proximal performance",
|
|
370
|
-
"outcome_group_learning": "Learning / retention / transfer",
|
|
371
|
-
"outcome_group_risk": "Risk / dependency",
|
|
372
|
-
"outcome_group_other": "Other outcomes",
|
|
373
|
-
"outcome_sep_note": "Outcomes are adjudicated separately so faster training performance is not silently treated as evidence of learning.",
|
|
374
|
-
"tribunal_decision": "Decision",
|
|
375
|
-
"tribunal_confidence": "Confidence",
|
|
376
|
-
"tribunal_can": "Can claim",
|
|
377
|
-
"tribunal_uncertain": "Cannot yet claim",
|
|
378
|
-
"tribunal_cannot": "Contradicted claims",
|
|
379
|
-
"tribunal_missing": "Missing evidence",
|
|
380
|
-
"tribunal_flow": "EvidenceFlow Protocol",
|
|
381
|
-
"tribunal_figure": "Tribunal infographic",
|
|
382
|
-
"method_audit_heads": ["Item", "Status", "Note"],
|
|
383
|
-
"method_guard": "Task vs learning guard",
|
|
384
|
-
"conflict_verdict": "Tribunal note",
|
|
385
|
-
"trace_decision": "Decision",
|
|
386
|
-
"trace_claim_prefix": "Claim",
|
|
387
|
-
"trace_no_source": "No source",
|
|
388
|
-
"applicability": ["Suitable for", "Course", "Outcomes", "Conditions", "Target population", "Target context"],
|
|
389
|
-
"intervention_learners": "Target learners",
|
|
390
|
-
"intervention_duration": "Pilot duration",
|
|
391
|
-
"intervention_policy": "AI usage policy",
|
|
392
|
-
"intervention_rule": "AI rule",
|
|
393
|
-
"intervention_activities": "Activities",
|
|
394
|
-
"intervention_check": "Outcome check",
|
|
395
|
-
"intervention_stop": "Stop conditions",
|
|
396
|
-
"intervention_timeline": "Intervention timeline infographic",
|
|
397
|
-
"evaluation_question": "Research question",
|
|
398
|
-
"evaluation_measures": ["Baseline", "Post test", "Retention", "Transfer"],
|
|
399
|
-
"evaluation_metrics": ["Process metrics", "Learning metrics", "Risk metrics"],
|
|
400
|
-
"evaluation_threshold": "Success threshold",
|
|
401
|
-
"evaluation_plan": "Analysis plan",
|
|
402
|
-
"evaluation_figure": "Evaluation design infographic",
|
|
403
|
-
"benchmark_note": "result.json carries no benchmark.baselines, so this visual is omitted; see the standalone benchmark report.",
|
|
404
|
-
"sources_title": "Source list",
|
|
405
|
-
"sources_heads": ["ID", "Title", "Year", "Authority", "Verifiable location"],
|
|
406
|
-
"provenance_title": "Fetch provenance",
|
|
407
|
-
"provenance_heads": ["Source", "Fetch method", "Status", "Fallback", "Time"],
|
|
408
|
-
"provenance_search": "Search provider",
|
|
409
|
-
"provenance_time": "Fetched at",
|
|
410
|
-
"provenance_empty": "No per-source fetch records (sources provided directly by the research pipeline).",
|
|
411
|
-
"no_data": "No data.",
|
|
412
|
-
"header_evidence": "Evidence: ",
|
|
413
|
-
"header_sources": "Sources: ",
|
|
414
|
-
"header_mode": "Mode: ",
|
|
415
|
-
"header_generated": "Generated: ",
|
|
416
|
-
"header_evidence_suffix": "",
|
|
417
|
-
"header_sources_suffix": "",
|
|
418
|
-
"footer_schema": "Schema",
|
|
419
|
-
"footer_claims": "Claim Binding",
|
|
420
|
-
"footer_numbers": "Numeric Consistency",
|
|
421
|
-
"footer_bilingual": "Bilingual Structure",
|
|
422
|
-
"footer_language": "Human Language",
|
|
423
|
-
"footer_no_false_precision": "False Precision",
|
|
424
|
-
"footer_lieflat_bound": "Lieflat Data Bound",
|
|
425
|
-
"footer_no_axis_distortion": "Axis Distortion",
|
|
426
|
-
"footer_colorblind_safe": "Colorblind Safe",
|
|
427
|
-
"footer": "EduEvidence Evidence Report · {integrity} · single-file offline · source: result.json",
|
|
428
|
-
"matrix_search_label": "Filter / search evidence",
|
|
429
|
-
"matrix_dir_filter": "Filter by effect direction",
|
|
430
|
-
"matrix_outcome_filter": "Filter by outcome type",
|
|
431
|
-
"svg_balance_title": "Outcome evidence effect balance",
|
|
432
|
-
"svg_balance_desc": "Positive / negative / null effect counts per outcome (based on effect_direction, not claim support).",
|
|
433
|
-
"svg_figure1_title": "Effect direction by outcome type (publication figure)",
|
|
434
|
-
"svg_figure1_desc": "Counts of positive / negative / null effects per outcome type with an integer count axis, theme-independent. Source: EduEvidence result.json.",
|
|
435
|
-
"svg_benchmark_title": "Benchmark: citation support precision by baseline",
|
|
436
|
-
"svg_benchmark_desc": "Citation support precision per baseline; not drawn when no baseline data exists.",
|
|
437
|
-
"svg_workflow_title": "EvidenceFlow Protocol",
|
|
438
|
-
"svg_workflow_desc": "Research flow from framing, retrieval, fetch/verify, extraction, challenge, method audit and adjudication to applicability and intervention evaluation.",
|
|
439
|
-
"svg_tribunal_title": "Evidence Tribunal infographic",
|
|
440
|
-
"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.",
|
|
441
|
-
"svg_intervention_title": "Teaching intervention timeline",
|
|
442
|
-
"svg_intervention_desc": "Short phase names and activity counts; full AI usage rules are in the phase blocks.",
|
|
443
|
-
"svg_evaluation_title": "Evaluation design flow",
|
|
444
|
-
"svg_evaluation_desc": "Evaluation flow across baseline, post test, retention and transfer; full metrics and analysis plan are in the evaluation section.",
|
|
445
|
-
"raw_tag_title": "raw id",
|
|
446
|
-
"summary_tag_support": "Support",
|
|
447
|
-
"summary_tag_contradict": "Contradict",
|
|
448
|
-
"summary_confidence_prefix": " (confidence: ",
|
|
449
|
-
"summary_confidence_suffix": ")",
|
|
450
|
-
"method_target": "Audit target",
|
|
451
|
-
"applicability_not_suitable": "Not suitable for",
|
|
452
|
-
"applicability_conditions": "Conditions",
|
|
453
|
-
"trace_claim_sep": ": ",
|
|
454
|
-
"colon": ": ",
|
|
455
|
-
"lang_switcher_aria": "语言切换 / Language switch",
|
|
456
|
-
"theme_switcher_aria": "主题 / Theme",
|
|
457
|
-
"v2_project_title": "Project & Research History",
|
|
458
|
-
"v2_project_id": "Project ID",
|
|
459
|
-
"v2_graph_revision": "Graph revision",
|
|
460
|
-
"v2_decision_snapshot": "Decision snapshot",
|
|
461
|
-
"v2_timeline": "Project timeline",
|
|
462
|
-
"v2_gaps_title": "Knowledge gaps",
|
|
463
|
-
"v2_gap_type": "Gap type",
|
|
464
|
-
"v2_gap_priority": "Priority",
|
|
465
|
-
"v2_gap_reasoning": "Reasoning",
|
|
466
|
-
"v2_design_title": "Study design",
|
|
467
|
-
"v2_design_type": "Design type",
|
|
468
|
-
"v2_design_question": "Research question",
|
|
469
|
-
"v2_provenance_title": "Dataset & analysis provenance",
|
|
470
|
-
"v2_diff_title": "Decision diff",
|
|
471
|
-
"v2_diff_action": "Decision action",
|
|
472
|
-
"v2_diff_confidence": "Confidence",
|
|
473
|
-
"v2_diff_claims": "Changed claims",
|
|
474
|
-
"v2_diff_gaps": "Resolved/new gaps",
|
|
475
|
-
"v2_revision": "Revision",
|
|
476
|
-
"v2_decision": "Decision",
|
|
477
|
-
"v2_no_v2_data": "(no V2 project data)",
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
93
|
class ReportInvalid(Exception):
|
|
482
94
|
"""Scientific Integrity Gate failure (§27/§60): report must not be published."""
|
|
483
95
|
|
|
@@ -489,7 +101,7 @@ def esc(text: Any) -> str:
|
|
|
489
101
|
def _svg_a11y(svg: str, title: str, desc: str) -> str:
|
|
490
102
|
"""6.5: 为嵌入的 SVG 注入双语 <title>/<desc>,并把 aria-label 换成 UI 字典文案。
|
|
491
103
|
|
|
492
|
-
title/desc 由调用方按当前语言从
|
|
104
|
+
title/desc 由调用方按当前语言从 build_ui() 取;aria-label 已存在时覆盖,
|
|
493
105
|
不存在时补上,保证英文模式下不残留中文 aria-label。
|
|
494
106
|
"""
|
|
495
107
|
if not svg:
|
|
@@ -640,39 +252,135 @@ def visualization_decisions(result: dict, charts: dict) -> dict[str, dict[str, A
|
|
|
640
252
|
# 0b. Lieflat gallery composition — visual_layout contract (§三)
|
|
641
253
|
# ---------------------------------------------------------------------------
|
|
642
254
|
|
|
643
|
-
# Deterministic
|
|
644
|
-
#
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
255
|
+
# Deterministic fallback when visual_layout is missing or all entries are
|
|
256
|
+
# invalid. The composition is DATA-DRIVEN: instead of a fixed quartet that
|
|
257
|
+
# silently suppresses most charts on an evidence set without numeric effect
|
|
258
|
+
# sizes, candidates are probed against the extractors and only the shapes the
|
|
259
|
+
# data actually supports are kept. Numbers still come exclusively from
|
|
260
|
+
# scripts/charts_data.py, so this remains an honest gallery.
|
|
261
|
+
FALLBACK_CANDIDATES = (
|
|
262
|
+
("forest_plot", ("证据效应量森林图", "Effect-size forest plot",
|
|
263
|
+
"Hedges' g 与 95% 置信区间 · 一行一篇研究",
|
|
264
|
+
"Hedges' g with 95% CI · one row per study",
|
|
265
|
+
"仅当证据集携带数值效应量时绘制;无 g/CI 数据时不画假图。",
|
|
266
|
+
"Drawn only when numeric effect sizes exist in the evidence set.")),
|
|
267
|
+
("dot_cascade", ("证据效应量梯队级联", "Ranked effect-size cascade",
|
|
268
|
+
"按效应量由高到低排序 · 圆点高度 = Hedges' g",
|
|
269
|
+
"Sorted by effect size · dot height = Hedges' g",
|
|
270
|
+
"仅当存在逐研究数值效应量时绘制。",
|
|
271
|
+
"Drawn only when per-study numeric effect sizes exist.")),
|
|
272
|
+
("bubble_almanac", ("发表年份 × 结果维度文献年历", "Year × dimension evidence almanac",
|
|
273
|
+
"气泡面积 ∝ 该格研究数 · 实心圆 = 有显著结果",
|
|
274
|
+
"Bubble area ∝ study count · solid core = significant results",
|
|
275
|
+
"仅当证据集携带发表年份与结果维度时绘制。",
|
|
276
|
+
"Drawn only when years and outcome dimensions exist.")),
|
|
277
|
+
("matrix_heat", ("年份 × 结果维度证据密度", "Year × outcome evidence density",
|
|
278
|
+
"每格数字 = 该年份该结果维度的证据条数",
|
|
279
|
+
"Each cell counts evidence items for that year and outcome",
|
|
280
|
+
"当证据跨多个年份与结果维度时,展示研究密度的分布。",
|
|
281
|
+
"Shows where the evidence sits across years and outcomes.")),
|
|
282
|
+
("tick_rows", ("各结果类型效应方向分布", "Effect direction by outcome",
|
|
283
|
+
"每 1 个圆点 = 1 条证据 · 绿 = 正向 · 灰 = 零效应 · 橙 = 负向",
|
|
284
|
+
"One dot = one evidence item · green = positive · grey = null · orange = negative",
|
|
285
|
+
"基于 effect_direction 计数,全部数值来自 result.json。",
|
|
286
|
+
"Based on effect_direction counts; all numbers come from result.json.")),
|
|
287
|
+
("paired_rungs", ("各结果类型的正负证据对照", "Positive vs negative evidence by outcome",
|
|
288
|
+
"左右两列分别汇总正向与负向证据条数",
|
|
289
|
+
"Two columns summarise positive and negative evidence counts",
|
|
290
|
+
"当同一结果同时存在正向与负向证据时,分列呈现避免相互抵消。",
|
|
291
|
+
"Splits positive and negative evidence so they never cancel out.")),
|
|
292
|
+
("brand_spectrum", ("各结果类型的净效应倾向", "Net effect direction by outcome",
|
|
293
|
+
"位置 =(正向 − 负向)÷ 方向计数 · 中点为中性",
|
|
294
|
+
"Position = (positive - negative) / directional count · centre is neutral",
|
|
295
|
+
"双极展示各结果构念整体偏向支持还是反对。",
|
|
296
|
+
"Bipolar view of whether each outcome leans supportive or against.")),
|
|
297
|
+
("hundred_field", ("研究设计构成", "Study-design composition",
|
|
298
|
+
"每格 = 1 篇研究 · 显示证据来自哪些研究设计",
|
|
299
|
+
"One cell = one study · shows which designs produced the evidence",
|
|
300
|
+
"当证据包含多种研究设计时,构成图比表格更快暴露设计偏斜。",
|
|
301
|
+
"Reveals design skew faster than a table when several designs are present.")),
|
|
302
|
+
("tick_donut", ("方法学评级构成", "Methodology rating composition",
|
|
303
|
+
"每 tick ≈ 1% 构成 · 按 WWC 评级汇总",
|
|
304
|
+
"One tick ~ 1% share · aggregated by WWC rating",
|
|
305
|
+
"仅当证据集记录 WWC 评级时绘制。",
|
|
306
|
+
"Drawn only when WWC ratings are recorded.")),
|
|
307
|
+
("tick_gauge", ("决策置信度", "Decision confidence",
|
|
308
|
+
"0–100% 单值仪表 · 由确定性置信度公式给出",
|
|
309
|
+
"Single 0-100% gauge from the deterministic confidence formula",
|
|
310
|
+
"置信度值来自 result.json 的决策字段,不由模型自评。",
|
|
311
|
+
"The value comes from the result's decision field, never self-rated.")),
|
|
312
|
+
("ballot_tally", ("方法学检查项计票", "Methodology checklist tally",
|
|
313
|
+
"每 tick = 1 条审计结论 · 按检查项统计通过与否",
|
|
314
|
+
"One tick = one audit verdict · grouped by checklist item",
|
|
315
|
+
"让读者一眼看出方法学短板集中在哪些检查项。",
|
|
316
|
+
"Shows which checklist items concentrate the weaknesses.")),
|
|
317
|
+
("launch_fan", ("干预阶段与活动权重", "Intervention phases and activity weight",
|
|
318
|
+
"每段 = 一个试点阶段 · 宽度 = 活动条数",
|
|
319
|
+
"Each segment is one pilot phase · width = number of activities",
|
|
320
|
+
"展示试点在时间上的铺开方式。",
|
|
321
|
+
"Shows how the pilot is spread across its phases.")),
|
|
322
|
+
("barcode_lollipop", ("试点阶段周次分布", "Pilot phase week spans",
|
|
323
|
+
"每根 = 一个阶段 · 高度 = 阶段序号",
|
|
324
|
+
"One bar per phase · height = phase index",
|
|
325
|
+
"仅当阶段定义包含可解析的周次区间时绘制。",
|
|
326
|
+
"Drawn only when phases carry parseable week ranges.")),
|
|
327
|
+
("dotty_matrix", ("阶段 × 活动矩阵", "Phase × activity matrix",
|
|
328
|
+
"每点 = 1 项活动 · 行 = 阶段",
|
|
329
|
+
"One dot = one activity · rows are phases",
|
|
330
|
+
"当阶段内含多项活动时,矩阵比列表更易比较。",
|
|
331
|
+
"Compares activity load across phases better than a list.")),
|
|
332
|
+
("jitter_strip", ("分组效应量分布", "Grouped effect-size distribution",
|
|
333
|
+
"每点 = 一篇研究 · 按结果维度分组",
|
|
334
|
+
"One point = one study · grouped by outcome dimension",
|
|
335
|
+
"仅当多个结果维度各有 3 篇以上带效应量的研究时绘制。",
|
|
336
|
+
"Drawn only when several dimensions each carry 3+ numeric effects.")),
|
|
337
|
+
("parallel_coordinates", ("跨维度研究画像", "Studies across dimensions",
|
|
338
|
+
"同一批研究在效应量 / 样本量 / 质量 / 年份上的走势",
|
|
339
|
+
"The same studies traced across effect size / N / quality / year",
|
|
340
|
+
"仅当至少 3 篇研究同时具备四项数值时绘制。",
|
|
341
|
+
"Drawn only when at least 3 studies carry all four values.")),
|
|
674
342
|
)
|
|
675
343
|
|
|
344
|
+
|
|
345
|
+
def _fallback_layout_for(result: dict, lang: str) -> list[dict]:
|
|
346
|
+
"""Probe the registry and keep only the chart shapes this result supports.
|
|
347
|
+
|
|
348
|
+
``visual_layout`` is the AI's job; when it is absent this keeps the gallery
|
|
349
|
+
useful without inventing anything: every candidate runs through its own
|
|
350
|
+
extractor, and a candidate whose data is missing is simply not offered.
|
|
351
|
+
One chart per data source avoids repeating the same shape twice.
|
|
352
|
+
"""
|
|
353
|
+
from lieflat_engine import REGISTRY
|
|
354
|
+
|
|
355
|
+
chosen: list[dict] = []
|
|
356
|
+
used_sources: set[str] = set()
|
|
357
|
+
for fig_type, copy in FALLBACK_CANDIDATES:
|
|
358
|
+
reg = REGISTRY.get(fig_type)
|
|
359
|
+
if reg is None or reg["source"] in used_sources:
|
|
360
|
+
continue
|
|
361
|
+
try:
|
|
362
|
+
bundle, _reason = reg["extractor"](result, {}, lang)
|
|
363
|
+
except Exception:
|
|
364
|
+
bundle = None
|
|
365
|
+
if bundle is None:
|
|
366
|
+
continue
|
|
367
|
+
used_sources.add(reg["source"])
|
|
368
|
+
title_zh, title_en, sub_zh, sub_en, cap_zh, cap_en = copy
|
|
369
|
+
chosen.append({
|
|
370
|
+
"chart_id": "lieflat-" + fig_type.replace("_", "-") + ".svg",
|
|
371
|
+
"type": fig_type,
|
|
372
|
+
"catalog_ref": reg["catalog_ref"],
|
|
373
|
+
"source": reg["source"],
|
|
374
|
+
"params": {},
|
|
375
|
+
"title_zh": title_zh, "title_en": title_en,
|
|
376
|
+
"subtitle_zh": sub_zh, "subtitle_en": sub_en,
|
|
377
|
+
"caption_zh": cap_zh, "caption_en": cap_en,
|
|
378
|
+
})
|
|
379
|
+
if len(chosen) >= 6:
|
|
380
|
+
break
|
|
381
|
+
return chosen
|
|
382
|
+
|
|
383
|
+
|
|
676
384
|
LIEFLAT_PARAM_TYPES = {"int": int, "list": list}
|
|
677
385
|
|
|
678
386
|
|
|
@@ -794,9 +502,18 @@ def resolve_visual_layout(result: dict) -> dict[str, Any]:
|
|
|
794
502
|
if entries:
|
|
795
503
|
return {"entries": entries, "fallback": False, "warnings": warnings, "rejected": rejected}
|
|
796
504
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
505
|
+
# Data-driven fallback: probe every candidate shape and keep the ones this
|
|
506
|
+
# result can honestly support, instead of a fixed quartet that suppresses
|
|
507
|
+
# most charts when numeric effect sizes are absent.
|
|
508
|
+
lang_hint = "zh" if result.get("meta", {}).get("lang") == "zh" else "en"
|
|
509
|
+
fallback_entries = _fallback_layout_for(result, lang_hint)
|
|
510
|
+
if fallback_entries:
|
|
511
|
+
warnings.append(
|
|
512
|
+
"visual_layout missing or fully invalid — data-driven fallback selected "
|
|
513
|
+
+ ", ".join(e["type"] for e in fallback_entries))
|
|
514
|
+
else:
|
|
515
|
+
warnings.append("visual_layout missing or fully invalid — no chart shape had "
|
|
516
|
+
"sufficient data; gallery suppressed rather than drawn empty")
|
|
800
517
|
return {"entries": fallback_entries, "fallback": True, "warnings": warnings, "rejected": rejected}
|
|
801
518
|
|
|
802
519
|
|
|
@@ -948,6 +665,9 @@ TEXT_LEAF_KEYS = {
|
|
|
948
665
|
"population", "intervention", "comparison", "outcome_measure", "effect", "method",
|
|
949
666
|
"ai_usage_policy", "target_learners", "ai_usage_rule", "outcome_check",
|
|
950
667
|
"research_question", "treatment", "baseline", "post_test", "retention_test",
|
|
668
|
+
# Evaluation milestone fields name a measurement point in the reader
|
|
669
|
+
# language; they are prose, not structural identifiers.
|
|
670
|
+
"immediate_post", "retention", "transfer",
|
|
951
671
|
"transfer_test", "success_threshold", "analysis_plan",
|
|
952
672
|
"suitable_for", "not_suitable_for", "search_provider",
|
|
953
673
|
"learner_match", "subject_match", "tool_match", "scope",
|
|
@@ -963,6 +683,10 @@ PROSE_LIST_KEYS = {
|
|
|
963
683
|
"stop_conditions", "strengths", "limitations", "confounders", "required_conditions",
|
|
964
684
|
"process_metrics", "learning_metrics", "risk_metrics", "suggestions", "risk_control",
|
|
965
685
|
"evidence_alignment",
|
|
686
|
+
# Evaluation prose: criteria are sentences, and the milestone fields
|
|
687
|
+
# name a measurement point in the reader language.
|
|
688
|
+
"success_criteria", "stop_criteria", "immediate_post", "retention",
|
|
689
|
+
"transfer", "baseline",
|
|
966
690
|
}
|
|
967
691
|
|
|
968
692
|
|
|
@@ -1091,7 +815,34 @@ _ZH_RESIDUE_RE = [
|
|
|
1091
815
|
(re.compile(r"\bCONCERN\b|\bPASS\b|\bFAIL\b"), "unmapped English audit code in zh narrative"),
|
|
1092
816
|
]
|
|
1093
817
|
|
|
818
|
+
#: Raw storage identifiers inside a sentence. A single-token value may be an
|
|
819
|
+
#: enum rendered through frame_enum_table(), but an underscore inside a multi-word
|
|
820
|
+
#: string or inside a zh sentence is a copy defect the reader would see.
|
|
821
|
+
_RAW_IDENTIFIER_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+")
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _scan_raw_identifiers(problems: list[str], path: str, text: str) -> None:
|
|
825
|
+
"""Flag snake_case storage identifiers that leaked into visible prose."""
|
|
826
|
+
if not text or not isinstance(text, str):
|
|
827
|
+
return
|
|
828
|
+
cleaned = text.strip()
|
|
829
|
+
has_cjk = bool(_HAS_CJK.search(cleaned))
|
|
830
|
+
is_sentence = len(cleaned.split()) > 1
|
|
831
|
+
if not (has_cjk or is_sentence):
|
|
832
|
+
return
|
|
833
|
+
for match in _RAW_IDENTIFIER_RE.finditer(cleaned):
|
|
834
|
+
token = match.group(0)
|
|
835
|
+
enum_tokens = set(frame_enum_table("zh")) | set(frame_enum_table("en"))
|
|
836
|
+
if token in enum_tokens:
|
|
837
|
+
continue
|
|
838
|
+
problems.append(path + ": raw identifier " + repr(token) + " leaked into prose")
|
|
839
|
+
return
|
|
840
|
+
|
|
841
|
+
|
|
1094
842
|
STRICT_NARRATIVE_PATHS = [
|
|
843
|
+
# Reader-facing decision prose written by the adjudicator.
|
|
844
|
+
"decision.strongest_support", "decision.key_uncertainty",
|
|
845
|
+
"decision.main_risk", "decision.next_action",
|
|
1095
846
|
"decision.decision_rationale", "decision.rationale", "decision.strongest_support",
|
|
1096
847
|
"decision.key_uncertainty", "decision.main_risk", "decision.next_action",
|
|
1097
848
|
"decision.next_steps", "decision.what_can_be_claimed", "decision.what_cannot_be_claimed",
|
|
@@ -1108,6 +859,21 @@ _SKIP_HINTS = ("id", "url", "doi", "author", "venue", "year", "status", "score",
|
|
|
1108
859
|
"verdict", "target", "decision")
|
|
1109
860
|
|
|
1110
861
|
|
|
862
|
+
COPY_LIMITS_ZH = {
|
|
863
|
+
"decision.strongest_support": 60,
|
|
864
|
+
"decision.key_uncertainty": 70,
|
|
865
|
+
"decision.main_risk": 60,
|
|
866
|
+
"decision.next_action": 80,
|
|
867
|
+
"decision.decision_rationale": 160,
|
|
868
|
+
"decision.rationale": 160,
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
#: Structural tokens that look like prose to a language check but are parsed
|
|
873
|
+
#: They stay identical across languages by contract.
|
|
874
|
+
NON_NARRATIVE_KEYS = ("weeks", "pilot_duration")
|
|
875
|
+
|
|
876
|
+
|
|
1111
877
|
def _scan_narrative(problems, path, en_text, zh_text, strict=False, kind=""):
|
|
1112
878
|
en_text = en_text or ""
|
|
1113
879
|
zh_text = zh_text or ""
|
|
@@ -1130,6 +896,10 @@ def _scan_narrative(problems, path, en_text, zh_text, strict=False, kind=""):
|
|
|
1130
896
|
problems.append(path + ": 含" + label)
|
|
1131
897
|
if strict and kind in ("rationale", "reason") and len(zh_text) < 40:
|
|
1132
898
|
problems.append(path + ": 决策理由过短(<40 字)")
|
|
899
|
+
ceiling = COPY_LIMITS_ZH.get(path)
|
|
900
|
+
if ceiling and len(zh_text) > ceiling:
|
|
901
|
+
problems.append(
|
|
902
|
+
f"{path}: 叙述超出 {ceiling} 字上限({len(zh_text)} 字)")
|
|
1133
903
|
|
|
1134
904
|
|
|
1135
905
|
def check_language_parallel(result_en: dict, result_zh: dict) -> list[str]:
|
|
@@ -1154,6 +924,9 @@ def check_language_parallel(result_en: dict, result_zh: dict) -> list[str]:
|
|
|
1154
924
|
_walk_strings(result_zh.get(root), "", zh_nodes, _SKIP_HINTS)
|
|
1155
925
|
by_path_en = {p: t for p, t in en_nodes}
|
|
1156
926
|
for p, z in zh_nodes:
|
|
927
|
+
leaf = p.rsplit(".", 1)[-1]
|
|
928
|
+
if leaf in NON_NARRATIVE_KEYS:
|
|
929
|
+
continue
|
|
1157
930
|
_scan_narrative(problems, root + p, by_path_en.get(p, ""), z, strict=False)
|
|
1158
931
|
for key in ("claims", "evidence"):
|
|
1159
932
|
en_items = result_en.get(key) or []
|
|
@@ -1163,6 +936,37 @@ def check_language_parallel(result_en: dict, result_zh: dict) -> list[str]:
|
|
|
1163
936
|
z_text = z_item.get("claim") if isinstance(z_item, dict) else None
|
|
1164
937
|
if z_text:
|
|
1165
938
|
_scan_narrative(problems, key + "[" + str(i) + "].claim", e_text, z_text, strict=False)
|
|
939
|
+
# Frame prose: field values may be enums (rendered via frame_enum_table), but a
|
|
940
|
+
# storage identifier must never reach the reader inside a sentence. Both
|
|
941
|
+
# language versions are checked because the leak is an authoring defect.
|
|
942
|
+
# Weeks and pilot duration are structural tokens (W1, W2-W13, one_semester):
|
|
943
|
+
|
|
944
|
+
for label, root in (("research_frame", result_en), ("research_frame.zh", result_zh)):
|
|
945
|
+
nodes: list = []
|
|
946
|
+
_walk_strings(root.get("research_frame"), "", nodes, ())
|
|
947
|
+
for node_path, text_value in nodes:
|
|
948
|
+
_scan_raw_identifiers(problems, label + node_path, text_value)
|
|
949
|
+
# Structural parallel: ids, source/study/sample keys, sample sizes and the
|
|
950
|
+
# direction fields must match item by item. Free text may differ between
|
|
951
|
+
# languages; these may not, or the zh report describes a different study.
|
|
952
|
+
structural_fields = ("evidence_id", "source_id", "study_id", "sample_id",
|
|
953
|
+
"sample_size", "outcome_type", "effect_direction",
|
|
954
|
+
"relation_to_claim")
|
|
955
|
+
en_ev = result_en.get("evidence")
|
|
956
|
+
zh_ev = result_zh.get("evidence")
|
|
957
|
+
if isinstance(en_ev, list) and isinstance(zh_ev, list):
|
|
958
|
+
if len(en_ev) != len(zh_ev):
|
|
959
|
+
problems.append(
|
|
960
|
+
"evidence: en and zh rows differ in count "
|
|
961
|
+
+ f"({len(en_ev)} vs {len(zh_ev)})")
|
|
962
|
+
for i, (a, b) in enumerate(zip(en_ev, zh_ev)):
|
|
963
|
+
if not (isinstance(a, dict) and isinstance(b, dict)):
|
|
964
|
+
continue
|
|
965
|
+
for field_name in structural_fields:
|
|
966
|
+
if a.get(field_name) != b.get(field_name):
|
|
967
|
+
problems.append(
|
|
968
|
+
f"evidence[{i}].{field_name}: en={a.get(field_name)!r} "
|
|
969
|
+
f"!= zh={b.get(field_name)!r} (structural fields must agree)")
|
|
1166
970
|
return problems
|
|
1167
971
|
|
|
1168
972
|
|
|
@@ -1352,13 +1156,21 @@ def diverging_bar_svg(option: dict, width: int = 720, height: int = 300,
|
|
|
1352
1156
|
"""真 diverging 静态图(P0-10):support 从中心向右、contradict 从中心向左,
|
|
1353
1157
|
neutral 走独立的细条道(第二网格),三系列互不覆盖。计数轴整数刻度(P0-11)。
|
|
1354
1158
|
6.5: aria-label / title / desc 从 UI 字典按语言取。"""
|
|
1355
|
-
ui = ui or
|
|
1159
|
+
ui = ui or build_ui("zh")
|
|
1356
1160
|
cats = option.get("yAxis", [{}])[0].get("data", []) if isinstance(option.get("yAxis"), list) \
|
|
1357
1161
|
else option.get("yAxis", {}).get("data", [])
|
|
1358
1162
|
series = option.get("series", [])
|
|
1359
1163
|
if not cats:
|
|
1360
1164
|
return ""
|
|
1361
|
-
|
|
1165
|
+
# Reserve enough gutter for the longest y-axis label. A fixed 150px gutter
|
|
1166
|
+
# pushed a long outcome name off the left edge (x0 = -42), so the reader
|
|
1167
|
+
# saw a clipped label instead of the outcome it names.
|
|
1168
|
+
def _label_w(text: str) -> float:
|
|
1169
|
+
cjk = sum(1 for ch in str(text) if "\u4e00" <= ch <= "\u9fff")
|
|
1170
|
+
return cjk * 11 + (len(str(text)) - cjk) * 6.6
|
|
1171
|
+
|
|
1172
|
+
left = max(150, int(max((_label_w(c) for c in cats), default=0)) + 20)
|
|
1173
|
+
right = 40
|
|
1362
1174
|
top, bottom = 46, 34
|
|
1363
1175
|
main_h = int((height - top - bottom) * 0.62)
|
|
1364
1176
|
neutral_h = height - top - bottom - main_h - 14
|
|
@@ -1432,7 +1244,7 @@ def diverging_bar_svg(option: dict, width: int = 720, height: int = 300,
|
|
|
1432
1244
|
|
|
1433
1245
|
def grouped_bar_svg(option: dict, width: int = 720, height: int = 260,
|
|
1434
1246
|
note: str = "", lang: str = "zh", ui: dict | None = None) -> str:
|
|
1435
|
-
ui = ui or
|
|
1247
|
+
ui = ui or build_ui("zh")
|
|
1436
1248
|
cats = option.get("xAxis", {}).get("data", [])
|
|
1437
1249
|
series = option.get("series", [])
|
|
1438
1250
|
parts = [f'<rect x="0" y="0" width="{width}" height="{height}" fill="#FFFFFF"/>']
|
|
@@ -1544,26 +1356,21 @@ def _outcome_support_score(evidence: list[dict], outcome: dict) -> float:
|
|
|
1544
1356
|
def first_screen(result: dict, lang: str, ui: dict) -> str:
|
|
1545
1357
|
"""Decision-first hero: meaning before raw counts."""
|
|
1546
1358
|
decision = result.get("decision", {})
|
|
1547
|
-
outcomes = result.get("outcomes", [])
|
|
1548
1359
|
evidence = result.get("evidence", [])
|
|
1549
|
-
ranked = sorted(outcomes, key=lambda o: _outcome_support_score(evidence, o), reverse=True)
|
|
1550
|
-
best_type = next((o.get("outcome_type") for o in ranked if o.get("positive_count", 0) > 0), None)
|
|
1551
|
-
supported_claims = decision.get("supported_claims") or []
|
|
1552
|
-
can_claim = decision.get("what_can_be_claimed") or []
|
|
1553
|
-
uncertain_claims = decision.get("uncertain_claims") or []
|
|
1554
|
-
contradicted_claims = decision.get("contradicted_claims") or []
|
|
1555
1360
|
action = decision.get("recommended_action", "insufficient_evidence")
|
|
1556
1361
|
cls = {"adopt": "adopt", "pilot": "pilot", "reject": "reject"}.get(action, "")
|
|
1557
1362
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1363
|
+
# These four are written by the adjudicator (schemas/verdict.schema.json).
|
|
1364
|
+
# The renderer must not synthesise them from claim fragments: assembling a
|
|
1365
|
+
# sentence out of list items is what made the first screen read as patched
|
|
1366
|
+
# together rather than written. A missing field is reported as missing,
|
|
1367
|
+
# which is also what the copy gate looks for.
|
|
1368
|
+
missing = ("该字段未产出(应由裁决角色撰写)。" if lang == "zh"
|
|
1369
|
+
else "Not produced by the adjudicator.")
|
|
1370
|
+
strongest = decision.get("strongest_support") or missing
|
|
1371
|
+
uncertainty = decision.get("key_uncertainty") or missing
|
|
1372
|
+
risk = decision.get("main_risk") or missing
|
|
1373
|
+
next_action = decision.get("next_action") or missing
|
|
1567
1374
|
|
|
1568
1375
|
rationale = decision.get("decision_rationale") or decision.get("rationale") or ""
|
|
1569
1376
|
rationale_html = expandable_text(rationale, ui["expand_details"], 380, "hero-rationale")
|
|
@@ -1586,23 +1393,17 @@ def first_screen(result: dict, lang: str, ui: dict) -> str:
|
|
|
1586
1393
|
</div>"""
|
|
1587
1394
|
|
|
1588
1395
|
|
|
1589
|
-
OUTCOME_GROUPS = {
|
|
1590
|
-
"task": {"completion_time", "accuracy", "assignment_score", "task_performance", "code_quality"},
|
|
1591
|
-
"learning": {"knowledge_gain", "learning_gain", "concept_understanding", "retention", "transfer",
|
|
1592
|
-
"independent_problem_solving", "programming_skill", "writing_skill"},
|
|
1593
|
-
"risk": {"ai_dependency", "over_reliance", "reduced_effort", "reduced_transfer",
|
|
1594
|
-
"academic_integrity_risk", "false_confidence", "cognitive_load"},
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
1396
|
def render_outcome_separation(result: dict, lang: str, ui: dict) -> str:
|
|
1599
1397
|
outcomes = effect_outcomes(result)
|
|
1600
1398
|
if len(outcomes) < 2:
|
|
1601
1399
|
return ""
|
|
1602
1400
|
buckets: dict[str, list[dict]] = {"task": [], "learning": [], "risk": [], "other": []}
|
|
1401
|
+
group_map = outcome_group_map()
|
|
1603
1402
|
for outcome in outcomes:
|
|
1604
1403
|
kind = outcome.get("outcome_type") or ""
|
|
1605
|
-
group = next((name for name, values in
|
|
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"
|
|
1606
1407
|
buckets[group].append(outcome)
|
|
1607
1408
|
group_labels = {
|
|
1608
1409
|
"task": ui["outcome_group_task"], "learning": ui["outcome_group_learning"],
|
|
@@ -1722,33 +1523,33 @@ def effect_label(lang: str, value: Any) -> str:
|
|
|
1722
1523
|
"neutral": "Null effect"}.get(effect, effect)
|
|
1723
1524
|
|
|
1724
1525
|
|
|
1526
|
+
def _quality_dims_text(lang: str, dims: Any) -> str:
|
|
1527
|
+
"""Render quality dimensions as readable pairs (D1 Study design = 2)."""
|
|
1528
|
+
if not isinstance(dims, dict) or not dims:
|
|
1529
|
+
return ""
|
|
1530
|
+
DIM_LABELS_ZH = {
|
|
1531
|
+
"D1_study_design": "D1 研究设计", "D2_sample_quality": "D2 样本质量",
|
|
1532
|
+
"D3_measurement_validity": "D3 测量效度", "D4_temporal_strength": "D4 时间强度",
|
|
1533
|
+
"D5_directness": "D5 直接性",
|
|
1534
|
+
}
|
|
1535
|
+
DIM_LABELS_EN = {
|
|
1536
|
+
"D1_study_design": "D1 Study design", "D2_sample_quality": "D2 Sample quality",
|
|
1537
|
+
"D3_measurement_validity": "D3 Measurement validity",
|
|
1538
|
+
"D4_temporal_strength": "D4 Temporal strength", "D5_directness": "D5 Directness",
|
|
1539
|
+
}
|
|
1540
|
+
table = DIM_LABELS_ZH if lang == "zh" else DIM_LABELS_EN
|
|
1541
|
+
sep = " · " if lang == "zh" else " · "
|
|
1542
|
+
joiner = "=" if lang == "zh" else " = "
|
|
1543
|
+
parts = []
|
|
1544
|
+
for key, value in dims.items():
|
|
1545
|
+
name = table.get(key) or _humanize_identifier(str(key), lang)
|
|
1546
|
+
parts.append(f"{name}{joiner}{value}")
|
|
1547
|
+
return sep.join(parts)
|
|
1548
|
+
|
|
1549
|
+
|
|
1725
1550
|
def render_evidence_detail(ev: dict, source: dict, lang: str, ui: dict) -> str:
|
|
1726
1551
|
"""Render complete traceable evidence detail without inventing missing fields."""
|
|
1727
|
-
labels = (
|
|
1728
|
-
"study_id": "研究 ID", "sample_id": "样本 ID", "title": "研究标题",
|
|
1729
|
-
"year": "年份", "study_type": "研究设计", "education_level": "教育阶段",
|
|
1730
|
-
"population": "研究人群", "sample_size": "样本量", "intervention": "干预",
|
|
1731
|
-
"comparison": "对照 / 比较条件", "outcome_measure": "结果测量", "claim": "完整主张",
|
|
1732
|
-
"effect": "效应 / 结果", "effect_direction": "效应方向", "relation_to_claim": "与主张关系",
|
|
1733
|
-
"duration": "干预时长", "method": "方法", "strengths": "优势",
|
|
1734
|
-
"limitations": "局限", "confounders": "混杂因素", "quality_dimensions": "质量维度",
|
|
1735
|
-
"quality_score": "质量分", "evidence_level": "证据等级", "directness": "直接性",
|
|
1736
|
-
"applicability": "适用性", "confidence": "置信度", "status": "证据状态",
|
|
1737
|
-
"source_location": "来源位置", "source_title": "来源标题", "source_url": "可验证链接",
|
|
1738
|
-
"claim_id": "Claim ID",
|
|
1739
|
-
} if lang == "zh" else {
|
|
1740
|
-
"study_id": "Study ID", "sample_id": "Sample ID", "title": "Study title",
|
|
1741
|
-
"year": "Year", "study_type": "Study design", "education_level": "Education level",
|
|
1742
|
-
"population": "Population", "sample_size": "Sample size", "intervention": "Intervention",
|
|
1743
|
-
"comparison": "Comparison", "outcome_measure": "Outcome measure", "claim": "Full claim",
|
|
1744
|
-
"effect": "Effect / result", "effect_direction": "Effect direction", "relation_to_claim": "Relation to claim",
|
|
1745
|
-
"duration": "Duration", "method": "Method", "strengths": "Strengths",
|
|
1746
|
-
"limitations": "Limitations", "confounders": "Confounders", "quality_dimensions": "Quality dimensions",
|
|
1747
|
-
"quality_score": "Quality score", "evidence_level": "Evidence level", "directness": "Directness",
|
|
1748
|
-
"applicability": "Applicability", "confidence": "Confidence", "status": "Evidence status",
|
|
1749
|
-
"source_location": "Source location", "source_title": "Source title", "source_url": "Verifiable link",
|
|
1750
|
-
"claim_id": "Claim ID",
|
|
1751
|
-
})
|
|
1552
|
+
labels = evidence_detail_label_table(lang)
|
|
1752
1553
|
|
|
1753
1554
|
values: list[tuple[str, Any]] = []
|
|
1754
1555
|
source_title = source.get("title") or ev.get("title")
|
|
@@ -1758,14 +1559,20 @@ def render_evidence_detail(ev: dict, source: dict, lang: str, ui: dict) -> str:
|
|
|
1758
1559
|
("study_id", ev.get("study_id")), ("sample_id", ev.get("sample_id")),
|
|
1759
1560
|
("title", ev.get("title")), ("source_title", source_title), ("year", source_year),
|
|
1760
1561
|
("study_type", label(lang, "study", ev.get("study_type") or "")),
|
|
1761
|
-
("education_level",
|
|
1762
|
-
("
|
|
1763
|
-
("
|
|
1562
|
+
("education_level", _frame_value_label(lang, ev.get("education_level"))),
|
|
1563
|
+
("population", _frame_value_label(lang, ev.get("population"))),
|
|
1564
|
+
("sample_size", ev.get("sample_size")),
|
|
1565
|
+
("intervention", _frame_value_label(lang, ev.get("intervention"))),
|
|
1566
|
+
("comparison", _frame_value_label(lang, ev.get("comparison"))),
|
|
1567
|
+
("outcome_measure", _frame_value_label(lang, ev.get("outcome_measure"))),
|
|
1764
1568
|
("effect", ev.get("effect")), ("effect_direction", effect_label(lang, ev.get("effect_direction"))),
|
|
1765
1569
|
("relation_to_claim", label(lang, "dir", ev.get("relation_to_claim") or ev.get("direction") or "neutral")),
|
|
1766
|
-
("duration",
|
|
1570
|
+
("duration", _frame_value_label(lang, ev.get("duration"))),
|
|
1571
|
+
("method", _frame_value_label(lang, ev.get("method"))),
|
|
1767
1572
|
("strengths", ev.get("strengths")), ("limitations", ev.get("limitations")),
|
|
1768
|
-
("confounders",
|
|
1573
|
+
("confounders", [_frame_value_label(lang, c) for c in (ev.get("confounders") or [])]
|
|
1574
|
+
if isinstance(ev.get("confounders"), list) else _frame_value_label(lang, ev.get("confounders"))),
|
|
1575
|
+
("quality_dimensions", _quality_dims_text(lang, ev.get("quality_dimensions"))),
|
|
1769
1576
|
("quality_score", ev.get("quality_score")), ("evidence_level", ev.get("evidence_level")),
|
|
1770
1577
|
("directness", ev.get("directness")), ("applicability", ev.get("applicability")),
|
|
1771
1578
|
("confidence", ev.get("confidence")), ("status", label(lang, "status", ev.get("status") or "")),
|
|
@@ -2000,9 +1807,10 @@ def render_tribunal(result: dict, workflow_svg: str, tribunal_svg: str, lang: st
|
|
|
2000
1807
|
|
|
2001
1808
|
def methodology_item_label(lang: str, item: Any) -> str:
|
|
2002
1809
|
key = str(item or "")
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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("_", " ")
|
|
2006
1814
|
|
|
2007
1815
|
|
|
2008
1816
|
def render_methodology(result: dict, lang: str, ui: dict) -> str:
|
|
@@ -2086,12 +1894,32 @@ def render_applicability(result: dict, lang: str, ui: dict) -> str:
|
|
|
2086
1894
|
return "\n".join(out) or f"<p>{esc(ui['no_data'])}</p>"
|
|
2087
1895
|
|
|
2088
1896
|
|
|
1897
|
+
def _weeks_label(value: str, lang: str) -> str:
|
|
1898
|
+
"""Render a week-range token for the reader, keeping data identical.
|
|
1899
|
+
|
|
1900
|
+
The bilingual contract requires the stored value to match across
|
|
1901
|
+
languages, so the Chinese form is produced here rather than stored.
|
|
1902
|
+
"""
|
|
1903
|
+
text = str(value or "")
|
|
1904
|
+
if lang != "zh" or not text:
|
|
1905
|
+
return text
|
|
1906
|
+
import re as _re
|
|
1907
|
+
m = _re.fullmatch(r"W(\d+)(?:-W?(\d+))?", text)
|
|
1908
|
+
if not m:
|
|
1909
|
+
return text
|
|
1910
|
+
start, end = m.group(1), m.group(2)
|
|
1911
|
+
if end:
|
|
1912
|
+
return f"第 {start}-{end} 周"
|
|
1913
|
+
return f"第 {start} 周"
|
|
1914
|
+
|
|
1915
|
+
|
|
2089
1916
|
def render_intervention(result: dict, svg: str, lang: str, ui: dict) -> str:
|
|
2090
1917
|
intervention = result.get("intervention", {})
|
|
2091
1918
|
if not intervention:
|
|
2092
1919
|
return f"<p>{esc(ui['no_data'])}</p>"
|
|
2093
1920
|
population = intervention.get("target_population") or intervention.get("target_learners")
|
|
2094
|
-
|
|
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")
|
|
2095
1923
|
lines = [f"<p><strong>{esc(population_label)}{esc(ui['colon'])}</strong>{esc(population)} · "
|
|
2096
1924
|
f"<strong>{esc(ui['intervention_duration'])}{esc(ui['colon'])}</strong>{esc(intervention.get('pilot_duration'))}</p>"]
|
|
2097
1925
|
if intervention.get("ai_usage_policy"):
|
|
@@ -2258,16 +2086,16 @@ def resolve_full_report_plan(result: dict) -> list[dict[str, Any]]:
|
|
|
2258
2086
|
raw = result.get("report_outline") or result.get("report_structure") or {}
|
|
2259
2087
|
chapters = raw.get("chapters") if isinstance(raw, dict) else raw if isinstance(raw, list) else None
|
|
2260
2088
|
if not isinstance(chapters, list) or not 5 <= len(chapters) <= 7:
|
|
2261
|
-
return
|
|
2089
|
+
return default_full_report_plan()
|
|
2262
2090
|
|
|
2263
2091
|
normalized: list[dict[str, Any]] = []
|
|
2264
2092
|
seen_modules: list[str] = []
|
|
2265
2093
|
for index, chapter in enumerate(chapters, 1):
|
|
2266
2094
|
if not isinstance(chapter, dict):
|
|
2267
|
-
return
|
|
2095
|
+
return default_full_report_plan()
|
|
2268
2096
|
modules = [m for m in (chapter.get("modules") or []) if m in FULL_REPORT_MODULES]
|
|
2269
2097
|
if not modules or any(m in seen_modules for m in modules):
|
|
2270
|
-
return [dict(item) for item in
|
|
2098
|
+
return [dict(item) for item in default_full_report_plan()]
|
|
2271
2099
|
seen_modules.extend(modules)
|
|
2272
2100
|
key = re.sub(r"[^a-z0-9-]+", "-", str(chapter.get("key") or f"chapter-{index}").lower()).strip("-")
|
|
2273
2101
|
normalized.append({
|
|
@@ -2279,9 +2107,9 @@ def resolve_full_report_plan(result: dict) -> list[dict[str, Any]]:
|
|
|
2279
2107
|
"modules": tuple(modules),
|
|
2280
2108
|
})
|
|
2281
2109
|
if set(seen_modules) != set(FULL_REPORT_MODULES):
|
|
2282
|
-
return [dict(item) for item in
|
|
2110
|
+
return [dict(item) for item in default_full_report_plan()]
|
|
2283
2111
|
if "decision" not in normalized[0]["modules"] or "sources" not in normalized[-1]["modules"]:
|
|
2284
|
-
return [dict(item) for item in
|
|
2112
|
+
return [dict(item) for item in default_full_report_plan()]
|
|
2285
2113
|
return normalized
|
|
2286
2114
|
|
|
2287
2115
|
|
|
@@ -2303,8 +2131,31 @@ def render_full_chapter(chapter_id: str, title: str, content: str, lead: str = "
|
|
|
2303
2131
|
|
|
2304
2132
|
|
|
2305
2133
|
def frame_enum_label(lang: str, value: Any) -> str:
|
|
2134
|
+
"""Label a frame enum value for display.
|
|
2135
|
+
|
|
2136
|
+
Registered values use the curated label. Anything else must still read as
|
|
2137
|
+
prose: a raw snake_case identifier in the report is a copy defect, so the
|
|
2138
|
+
fallback humanises it instead of leaking the storage form to the reader.
|
|
2139
|
+
"""
|
|
2306
2140
|
text = str(value or "")
|
|
2307
|
-
|
|
2141
|
+
if not text:
|
|
2142
|
+
return ""
|
|
2143
|
+
table = frame_enum_table(lang)
|
|
2144
|
+
if text in table:
|
|
2145
|
+
return table[text]
|
|
2146
|
+
return _humanize_identifier(text, lang)
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def _humanize_identifier(value: str, lang: str = "en") -> str:
|
|
2150
|
+
"""Turn a snake_case / kebab-case token into a readable phrase.
|
|
2151
|
+
|
|
2152
|
+
Delegates to zh_labels.humanize_identifier so the report body and the
|
|
2153
|
+
charts share one display rule (curated label first, acronym-aware prose
|
|
2154
|
+
second) instead of leaking storage identifiers to the reader.
|
|
2155
|
+
"""
|
|
2156
|
+
from zh_labels import humanize_identifier
|
|
2157
|
+
|
|
2158
|
+
return humanize_identifier(value, lang)
|
|
2308
2159
|
|
|
2309
2160
|
|
|
2310
2161
|
def labeled_pairs(lang: str, data: dict, labels_zh: dict[str, str], labels_en: dict[str, str]) -> str:
|
|
@@ -2313,33 +2164,39 @@ def labeled_pairs(lang: str, data: dict, labels_zh: dict[str, str], labels_en: d
|
|
|
2313
2164
|
for key, value in data.items():
|
|
2314
2165
|
if value in (None, "", [], {}):
|
|
2315
2166
|
continue
|
|
2316
|
-
rendered =
|
|
2167
|
+
rendered = _frame_value_label(lang, value) if isinstance(value, str) else str(value)
|
|
2317
2168
|
parts.append(f"{labels.get(key, key)}:{rendered}" if lang == "zh" else f"{labels.get(key, key)}: {rendered}")
|
|
2318
2169
|
return ";".join(parts) if lang == "zh" else "; ".join(parts)
|
|
2319
2170
|
|
|
2320
2171
|
|
|
2172
|
+
def _frame_value_label(lang: str, value: Any) -> str:
|
|
2173
|
+
"""Render a frame value for display.
|
|
2174
|
+
|
|
2175
|
+
Free prose passes through; a bare storage identifier is humanised so the
|
|
2176
|
+
comparison/success cards never show snake_case. Multi-word prose that
|
|
2177
|
+
merely contains an underscore is returned unchanged.
|
|
2178
|
+
"""
|
|
2179
|
+
if value in (None, "", [], {}):
|
|
2180
|
+
return ""
|
|
2181
|
+
text_value = str(value)
|
|
2182
|
+
table = frame_enum_table(lang)
|
|
2183
|
+
if text_value in table:
|
|
2184
|
+
return table[text_value]
|
|
2185
|
+
if " " in text_value.strip() or _HAS_CJK.search(text_value):
|
|
2186
|
+
return text_value
|
|
2187
|
+
return _humanize_identifier(text_value, lang)
|
|
2188
|
+
|
|
2189
|
+
|
|
2321
2190
|
def render_research_scope(result: dict, lang: str, ui: dict) -> str:
|
|
2322
2191
|
frame = result.get("research_frame", {}) or {}
|
|
2323
2192
|
learner = frame.get("learner", {}) or {}
|
|
2324
2193
|
course = frame.get("course", {}) or {}
|
|
2325
2194
|
intervention = frame.get("intervention", {}) or {}
|
|
2326
2195
|
scope = frame.get("scope", {}) or {}
|
|
2327
|
-
labels = (
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
"question": "Research question", "learner": "Target learners", "course": "Course context", "intervention": "AI intervention",
|
|
2332
|
-
"comparison": "Comparison", "outcomes": "Outcome constructs", "scope": "Research scope", "success": "Decision success condition",
|
|
2333
|
-
})
|
|
2334
|
-
learner_text = labeled_pairs(lang, learner,
|
|
2335
|
-
{"education_level":"教育阶段", "major":"专业", "prior_knowledge":"先验知识", "special_characteristics":"学习者特征"},
|
|
2336
|
-
{"education_level":"Education level", "major":"Major", "prior_knowledge":"Prior knowledge", "special_characteristics":"Learner characteristics"})
|
|
2337
|
-
course_text = labeled_pairs(lang, course,
|
|
2338
|
-
{"subject":"课程", "course_type":"课程类型", "duration":"课程周期"},
|
|
2339
|
-
{"subject":"Subject", "course_type":"Course type", "duration":"Duration"})
|
|
2340
|
-
intervention_text = labeled_pairs(lang, intervention,
|
|
2341
|
-
{"ai_tool":"AI 工具", "allowed_usage":"允许使用", "frequency":"使用频率", "duration":"干预周期"},
|
|
2342
|
-
{"ai_tool":"AI tool", "allowed_usage":"Allowed usage", "frequency":"Frequency", "duration":"Duration"})
|
|
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"))
|
|
2343
2200
|
outcome_map = frame.get("outcomes", {}) or {}
|
|
2344
2201
|
outcome_parts = []
|
|
2345
2202
|
for group, values in outcome_map.items():
|
|
@@ -2347,24 +2204,34 @@ def render_research_scope(result: dict, lang: str, ui: dict) -> str:
|
|
|
2347
2204
|
rendered = "、".join(label(lang, "outcome", v) for v in values) if lang == "zh" else ", ".join(label(lang, "outcome", v) for v in values)
|
|
2348
2205
|
outcome_parts.append(f"{frame_enum_label(lang, group)}:{rendered}" if lang == "zh" else f"{frame_enum_label(lang, group)}: {rendered}")
|
|
2349
2206
|
scope_parts = []
|
|
2350
|
-
scope_labels_zh =
|
|
2351
|
-
scope_labels_en =
|
|
2207
|
+
scope_labels_zh = scope_subfield_labels("zh", "scope")
|
|
2208
|
+
scope_labels_en = scope_subfield_labels("en", "scope")
|
|
2352
2209
|
for key, value in scope.items():
|
|
2353
2210
|
if value in (None, "", [], {}):
|
|
2354
2211
|
continue
|
|
2355
|
-
if
|
|
2356
|
-
|
|
2212
|
+
if isinstance(value, list):
|
|
2213
|
+
# Any list-valued scope field renders as a joined list. Only
|
|
2214
|
+
# study_types used to be unpacked, so a list-valued evidence_types
|
|
2215
|
+
# fell through to str() and printed its Python repr
|
|
2216
|
+
# ("['quasi Experimental', 'rct']") straight into the card.
|
|
2217
|
+
kind = "study" if key == "study_types" else ""
|
|
2218
|
+
parts = [label(lang, kind, str(v)) if kind else _frame_value_label(lang, v)
|
|
2219
|
+
for v in value]
|
|
2220
|
+
rendered = ("、".join(parts) if lang == "zh" else ", ".join(parts))
|
|
2357
2221
|
else:
|
|
2358
2222
|
rendered = frame_enum_label(lang, value)
|
|
2359
|
-
field_label = (scope_labels_zh if lang == "zh" else scope_labels_en).get(
|
|
2223
|
+
field_label = (scope_labels_zh if lang == "zh" else scope_labels_en).get(
|
|
2224
|
+
key, _humanize_identifier(key, lang))
|
|
2360
2225
|
scope_parts.append(f"{field_label}:{rendered}" if lang == "zh" else f"{field_label}: {rendered}")
|
|
2361
2226
|
scope_text = ";".join(scope_parts) if lang == "zh" else "; ".join(scope_parts)
|
|
2362
2227
|
cards = [
|
|
2363
2228
|
(labels["question"], frame.get("question") or result.get("meta", {}).get("question")),
|
|
2364
2229
|
(labels["learner"], learner_text), (labels["course"], course_text),
|
|
2365
|
-
(labels["intervention"], intervention_text),
|
|
2230
|
+
(labels["intervention"], intervention_text),
|
|
2231
|
+
(labels["comparison"], _frame_value_label(lang, frame.get("comparison"))),
|
|
2366
2232
|
(labels["outcomes"], ";".join(outcome_parts) if lang == "zh" else "; ".join(outcome_parts)),
|
|
2367
|
-
(labels["scope"], scope_text),
|
|
2233
|
+
(labels["scope"], scope_text),
|
|
2234
|
+
(labels["success"], _frame_value_label(lang, frame.get("success_condition"))),
|
|
2368
2235
|
]
|
|
2369
2236
|
return '<div class="scope-grid">' + "".join(
|
|
2370
2237
|
f'<article class="scope-card"><h3>{esc(title)}</h3>{expandable_text(text, ui["expand_details"], 260, "scope-text")}</article>'
|
|
@@ -2373,8 +2240,8 @@ def render_research_scope(result: dict, lang: str, ui: dict) -> str:
|
|
|
2373
2240
|
|
|
2374
2241
|
def render_retrieval_context(result: dict, lang: str, ui: dict) -> str:
|
|
2375
2242
|
frame = result.get("research_frame", {}) or {}
|
|
2376
|
-
inclusion = frame.get("inclusion_criteria") or []
|
|
2377
|
-
exclusion = frame.get("exclusion_criteria") or []
|
|
2243
|
+
inclusion = [_frame_value_label(lang, v) for v in (frame.get("inclusion_criteria") or [])]
|
|
2244
|
+
exclusion = [_frame_value_label(lang, v) for v in (frame.get("exclusion_criteria") or [])]
|
|
2378
2245
|
provenance = result.get("provenance", {}) or {}
|
|
2379
2246
|
sources = result.get("sources", []) or []
|
|
2380
2247
|
source_ids = " ".join(f'<code>{esc(s.get("source_id"))}</code>' for s in sources)
|
|
@@ -2448,20 +2315,13 @@ def render_full_report(result: dict, lang: str, ui: dict, charts: dict, infograp
|
|
|
2448
2315
|
+ render_v2_history(result, lang, ui)),
|
|
2449
2316
|
}
|
|
2450
2317
|
|
|
2451
|
-
|
|
2452
|
-
"decision": ("先明确最终裁决与研究边界,再解释为什么。" if lang == "zh" else "State the final adjudication and research boundary before explaining why."),
|
|
2453
|
-
"evidence": ("把任务表现、真实学习、保持与风险放在同一证据地图中,但不混为一谈。" if lang == "zh" else "Place task performance, actual learning, retention and risk on one evidence map without conflating them."),
|
|
2454
|
-
"quality": ("检查证据为什么可信、哪里冲突,以及哪些结论必须降级。" if lang == "zh" else "Examine why evidence is credible, where it conflicts, and which conclusions require downgrading."),
|
|
2455
|
-
"action": ("把可外推范围、护栏和教学动作连接到具体证据。" if lang == "zh" else "Connect applicability, guardrails and teaching actions to specific evidence."),
|
|
2456
|
-
"evaluation": ("用独立学习结果验证试点,并预先写清停止条件。" if lang == "zh" else "Validate the pilot with independent learning outcomes and pre-specified stop conditions."),
|
|
2457
|
-
"sources": ("保留原始来源、URL、证据 ID 和获取信息,确保可回查。" if lang == "zh" else "Preserve original sources, URLs, evidence IDs and retrieval metadata for auditability."),
|
|
2458
|
-
}
|
|
2318
|
+
leads_by_key = default_leads(lang)
|
|
2459
2319
|
|
|
2460
2320
|
plan = resolve_full_report_plan(result)
|
|
2461
2321
|
rendered = []
|
|
2462
2322
|
for index, chapter in enumerate(plan, 1):
|
|
2463
2323
|
content = "".join(module_content.get(module, "") for module in chapter.get("modules", ()))
|
|
2464
|
-
lead = chapter.get("lead_zh" if lang == "zh" else "lead_en") or
|
|
2324
|
+
lead = chapter.get("lead_zh" if lang == "zh" else "lead_en") or leads_by_key.get(chapter.get("key"), "")
|
|
2465
2325
|
rendered.append(render_full_chapter(
|
|
2466
2326
|
chapter_dom_id(lang, str(chapter.get("key") or f"chapter-{index}"), index),
|
|
2467
2327
|
full_chapter_title(chapter, lang, index), content, str(lead or "")))
|
|
@@ -2640,12 +2500,6 @@ def _enhancer_js(charts_zh: dict, charts_en: dict, result_en: dict) -> str:
|
|
|
2640
2500
|
trace_en = next((c for c in charts_en.get("charts", [])
|
|
2641
2501
|
if c.get("chart_id") == "claim-evidence-trace"), None)
|
|
2642
2502
|
benchmark_en = charts_en.get("benchmark") or {}
|
|
2643
|
-
matrix_rows = [
|
|
2644
|
-
{"id": e.get("evidence_id"), "title": e.get("title", ""),
|
|
2645
|
-
"direction": e.get("direction", "neutral"),
|
|
2646
|
-
"outcome": e.get("outcome_type", ""), "quality": e.get("quality_score", 0)}
|
|
2647
|
-
for e in result_en.get("evidence", [])
|
|
2648
|
-
]
|
|
2649
2503
|
return f"""
|
|
2650
2504
|
(function () {{
|
|
2651
2505
|
'use strict';
|
|
@@ -2845,7 +2699,12 @@ def render_lieflat_gallery_brief(result: dict, figures: dict, layout: dict,
|
|
|
2845
2699
|
title = entry.get("title_zh" if zh else "title_en") or ""
|
|
2846
2700
|
subtitle = entry.get("subtitle_zh" if zh else "subtitle_en") or ""
|
|
2847
2701
|
caption = entry.get("caption_zh" if zh else "caption_en") or ""
|
|
2848
|
-
|
|
2702
|
+
# The caption names the catalogue figure and, in prose, what it was
|
|
2703
|
+
# drawn from. The raw bundle path ("evidence.year_x_outcome_counts") is
|
|
2704
|
+
# an internal pointer and must not reach the reader, so it goes through
|
|
2705
|
+
# the same humaniser as every other enum in the report.
|
|
2706
|
+
source_ref = _humanize_identifier(str(entry.get('source', '')), lang)
|
|
2707
|
+
source_line = f"{entry.get('catalog_ref', '')} · {source_ref}".strip(" ·")
|
|
2849
2708
|
fig_type = entry.get("type", "")
|
|
2850
2709
|
cards.append(
|
|
2851
2710
|
f'<figure class="lieflat-card" data-lieflat data-visual="lieflat-{esc(fig_type)}" '
|
|
@@ -2879,24 +2738,7 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
|
|
|
2879
2738
|
outcome_chart = next((c for c in charts.get("charts", [])
|
|
2880
2739
|
if c.get("chart_id") == "outcome-evidence-overview"), None)
|
|
2881
2740
|
|
|
2882
|
-
|
|
2883
|
-
brief_titles = {
|
|
2884
|
-
"decision": ("先看结论", "该不该做、置信度多高、最关键的证据边界在哪。"),
|
|
2885
|
-
"lieflat": ("Lieflat 实证手作画廊", "AI 按数据形状从 Lieflat 目录选型编排;每张图的数字都可溯源到 result.json。"),
|
|
2886
|
-
"outcomes": ("任务表现 ≠ 学习效果", "只展示真正有解释力的结果分离;正向、负向与零效应按 effect_direction 编码。"),
|
|
2887
|
-
"tribunal": ("证据裁决", "支持、不确定、被反驳与缺失证据分开放置,不把长段落平铺在同一层。"),
|
|
2888
|
-
"action": ("从证据到行动", "适用性、护栏、停止条件与评价连成一条可执行路径。"),
|
|
2889
|
-
"sources": ("关键来源", "摘要页只列最关键的来源;完整溯源在完整报告中展开。"),
|
|
2890
|
-
}
|
|
2891
|
-
else:
|
|
2892
|
-
brief_titles = {
|
|
2893
|
-
"decision": ("Decision first", "What to do, how confident we are, and the most important evidence boundary."),
|
|
2894
|
-
"lieflat": ("Lieflat Editorial Gallery", "Charts selected and composed by AI from the Lieflat catalog; every number traces back to result.json."),
|
|
2895
|
-
"outcomes": ("Task performance ≠ learning", "Only informative outcome separation; positive, negative and null effects use effect_direction."),
|
|
2896
|
-
"tribunal": ("Evidence tribunal", "Supported, uncertain, contradicted and missing evidence stay separated instead of flattened into long prose."),
|
|
2897
|
-
"action": ("Evidence to action", "Applicability, guardrails, stop conditions and evaluation form one executable path."),
|
|
2898
|
-
"sources": ("Key sources", "Only the key sources in the brief; full traceability expands in the full report."),
|
|
2899
|
-
}
|
|
2741
|
+
brief_titles = brief_block_titles(lang)
|
|
2900
2742
|
|
|
2901
2743
|
lieflat_layout = viz.get("lieflat_layout") or {"entries": []}
|
|
2902
2744
|
lieflat_meta = (viz.get("lieflat_meta") or {}).get(lang, {})
|
|
@@ -2922,7 +2764,7 @@ def render_body(result: dict, lang: str, ui: dict, charts: dict, infographics: d
|
|
|
2922
2764
|
nav_items = [("decision", brief_titles["decision"][0]), ("outcomes", brief_titles["outcomes"][0]),
|
|
2923
2765
|
("tribunal", brief_titles["tribunal"][0]), ("action", brief_titles["action"][0]),
|
|
2924
2766
|
("sources", brief_titles["sources"][0])]
|
|
2925
|
-
brief_nav = '<nav class="brief-navigation" aria-label="' + ("
|
|
2767
|
+
brief_nav = '<nav class="brief-navigation" aria-label="' + esc(ui["brief_nav_aria"]) + '">' + ''.join(
|
|
2926
2768
|
f'<a href="#brief-{key}-{lang}"><span>{i:02d}</span>{esc(title)}</a>'
|
|
2927
2769
|
for i, (key, title) in enumerate(nav_items, 1)) + '</nav>'
|
|
2928
2770
|
full_report = render_full_report(result, lang, ui, charts, infographics, figures, viz)
|
|
@@ -2967,9 +2809,11 @@ def render_html(result_en: dict, result_zh: dict, charts_zh: dict, charts_en: di
|
|
|
2967
2809
|
figures_en: dict, theme: str, viz: dict,
|
|
2968
2810
|
result_sha256: str = "", integrity: dict | None = None) -> str:
|
|
2969
2811
|
# Theme is fixed at generation time; language remains switchable in the HTML.
|
|
2970
|
-
|
|
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,
|
|
2971
2815
|
integrity=integrity)
|
|
2972
|
-
body_en = render_body(result_en, "en",
|
|
2816
|
+
body_en = render_body(result_en, "en", ui_en, charts_en, infographics_en, figures_en, viz, theme,
|
|
2973
2817
|
integrity=integrity)
|
|
2974
2818
|
hash_meta = (f'<meta name="eduevidence-result-sha256" content="{esc(result_sha256)}">\n'
|
|
2975
2819
|
if result_sha256 else "")
|
|
@@ -2993,10 +2837,10 @@ def render_html(result_en: dict, result_zh: dict, charts_zh: dict, charts_en: di
|
|
|
2993
2837
|
<div class="controls reader-toolbar">
|
|
2994
2838
|
<a class="reader-home" href="#" aria-label="Back to report start">EduEvidence<span class="generated-theme">{esc(THEME_DISPLAY[theme])}</span></a>
|
|
2995
2839
|
<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"
|
|
2997
|
-
<button type="button" class="report-view-btn" data-report-view="full" data-copy="full" aria-pressed="false"
|
|
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>
|
|
2998
2842
|
</div>
|
|
2999
|
-
<div class="reader-tools">{_lang_switcher(
|
|
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>
|
|
3000
2844
|
</div>
|
|
3001
2845
|
{body_zh}
|
|
3002
2846
|
{body_en}
|
|
@@ -3093,6 +2937,13 @@ def main() -> int:
|
|
|
3093
2937
|
return 2
|
|
3094
2938
|
result_zh = json.loads(zh_path.read_text(encoding="utf-8"))
|
|
3095
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
|
+
|
|
3096
2947
|
# 1. Contract validation(两份数据分别校验)
|
|
3097
2948
|
for label, data in (("result.json", result_en), ("result.zh.json", result_zh)):
|
|
3098
2949
|
problems = validate_contract(data)
|
|
@@ -3122,8 +2973,8 @@ def main() -> int:
|
|
|
3122
2973
|
# 3. Adapters(两份数据分别生成 spec / 信息图 / 学术图;数字同构)
|
|
3123
2974
|
charts_zh = build_chart_specs(result_zh, lang="zh")
|
|
3124
2975
|
charts_en = build_chart_specs(result_en, lang="en")
|
|
3125
|
-
infographics_zh = build_infographics(result_zh, lang="zh")
|
|
3126
|
-
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"))
|
|
3127
2978
|
figure_data = build_figure_data(result_en)
|
|
3128
2979
|
figures_zh = render_figures(figure_data, theme=args.theme, lang="zh")
|
|
3129
2980
|
figures_en = render_figures(figure_data, theme=args.theme, lang="en")
|
|
@@ -3200,5 +3051,14 @@ def main() -> int:
|
|
|
3200
3051
|
return 0
|
|
3201
3052
|
|
|
3202
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
|
+
|
|
3203
3063
|
if __name__ == "__main__":
|
|
3204
3064
|
sys.exit(main())
|