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.
- package/CHANGELOG.md +395 -0
- package/README.md +22 -13
- package/README.zh-CN.md +15 -8
- package/SKILL.md +10 -9
- package/benchmarks/evidence-library.json +277 -1
- package/docs/architecture.md +6 -3
- package/docs/j-ev-experimental.md +250 -0
- package/docs/reproducibility.md +138 -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/engine/capabilities.py +57 -5
- package/engine/decision_policy.py +88 -17
- package/engine/library_builtin.py +7 -4
- package/engine/tribunal.py +17 -23
- package/engine/versions.py +1 -1
- package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +4 -4
- package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
- package/examples/spaced-retrieval-practice/report.html +2522 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +4 -4
- package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +36 -36
- 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 +9 -1
- package/pyproject.toml +1 -1
- package/references/report-copy-style.md +43 -3
- package/schemas/v2/decision-snapshot.schema.json +20 -9
- package/schemas/v2/intake.schema.json +191 -0
- package/scripts/build_evidence_library.py +15 -5
- package/scripts/dashboard_server.py +13 -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 +68 -17
- package/scripts/pre_verdict_gate.py +21 -7
- package/scripts/skill_lint.py +11 -1
- package/scripts/skill_payload.py +3 -3
- package/scripts/test_adversarial_empirical.py +70 -6
- package/skill/agents/evidence-judge.md +49 -7
- package/skill/workflows/experimental-jev.md +170 -0
- package/skill/workflows/intake.md +120 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +32 -14
- package/visualization/eduevidence-report/scripts/build_report.py +75 -662
- 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 +61 -0
- 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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""report_copy_pack.py — domain copy-pack loader for the report layer.
|
|
3
|
+
|
|
4
|
+
Loads domains/<id>/copy/*.json and exposes display lookups so build_report.py
|
|
5
|
+
stays free of research-voice dictionaries.
|
|
6
|
+
Domain: result.meta.domain → frame.extensions.domain (default education).
|
|
7
|
+
Unknown domain: neutral fallback + warning; policy packs reject teaching words.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
#: Canonical six pack files (task contract).
|
|
17
|
+
COPY_PACK_FILES = (
|
|
18
|
+
"framing_lexicon.json",
|
|
19
|
+
"section_titles.json",
|
|
20
|
+
"module_labels.json",
|
|
21
|
+
"terminology.json",
|
|
22
|
+
"risk_constructs.json",
|
|
23
|
+
"few_shots.json",
|
|
24
|
+
)
|
|
25
|
+
#: Optional topic-split companions (merged into the canonical view on load).
|
|
26
|
+
COMPANION_FILES = (
|
|
27
|
+
"framing_enums.json",
|
|
28
|
+
"module_labels_modules.json",
|
|
29
|
+
"module_labels_nav.json",
|
|
30
|
+
"module_labels_summary.json",
|
|
31
|
+
"module_labels_tables.json",
|
|
32
|
+
"module_labels_footer.json",
|
|
33
|
+
"module_labels_v2.json",
|
|
34
|
+
)
|
|
35
|
+
NEUTRAL_DOMAIN = "_neutral"
|
|
36
|
+
DEFAULT_DOMAIN = "education"
|
|
37
|
+
|
|
38
|
+
#: Chapter skeleton is structure (not copy); titles/leads come from the pack.
|
|
39
|
+
DEFAULT_CHAPTER_SKELETON = (
|
|
40
|
+
{"key": "decision", "modules": ("decision", "scope")},
|
|
41
|
+
{"key": "evidence", "modules": ("retrieval", "outcomes", "evidence")},
|
|
42
|
+
{"key": "quality", "modules": ("quality", "conflicts", "trace")},
|
|
43
|
+
{"key": "action", "modules": ("applicability", "intervention")},
|
|
44
|
+
{"key": "evaluation", "modules": ("evaluation",)},
|
|
45
|
+
{"key": "sources", "modules": ("sources",)},
|
|
46
|
+
)
|
|
47
|
+
FULL_REPORT_MODULES = (
|
|
48
|
+
"decision", "scope", "retrieval", "outcomes", "evidence", "quality", "conflicts",
|
|
49
|
+
"trace", "applicability", "intervention", "evaluation", "sources",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from report_copy_policy_guard import assert_policy_copy_clean # noqa: E402
|
|
53
|
+
|
|
54
|
+
_ACTIVE_PACK: dict[str, Any] = {}
|
|
55
|
+
_COPY_WARNINGS: list[str] = []
|
|
56
|
+
|
|
57
|
+
def _repo_root() -> Path:
|
|
58
|
+
return Path(__file__).resolve().parents[3]
|
|
59
|
+
|
|
60
|
+
def domains_root() -> Path:
|
|
61
|
+
repo = _repo_root() / "domains"
|
|
62
|
+
if (repo / "manifest.json").is_file():
|
|
63
|
+
return repo
|
|
64
|
+
share = Path(sys.prefix) / "share" / "eduevidence" / "domains"
|
|
65
|
+
return share if (share / "manifest.json").is_file() else repo
|
|
66
|
+
|
|
67
|
+
def resolve_domain(result: dict) -> tuple[str, list[str]]:
|
|
68
|
+
warnings: list[str] = []
|
|
69
|
+
meta = result.get("meta") or {}
|
|
70
|
+
frame = result.get("research_frame") or {}
|
|
71
|
+
ext = frame.get("extensions") or {}
|
|
72
|
+
domain = str(meta.get("domain") or ext.get("domain") or "").strip().lower()
|
|
73
|
+
if not domain:
|
|
74
|
+
warnings.append(
|
|
75
|
+
"result carries no meta.domain / frame.extensions.domain; "
|
|
76
|
+
f"defaulting to {DEFAULT_DOMAIN}"
|
|
77
|
+
)
|
|
78
|
+
return DEFAULT_DOMAIN, warnings
|
|
79
|
+
return domain, warnings
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_copy_pack(domain: str, *, strict: bool = False) -> dict[str, Any]:
|
|
83
|
+
warnings: list[str] = []
|
|
84
|
+
root = domains_root() / domain / "copy"
|
|
85
|
+
pack: dict[str, Any] = {"domain": domain, "files": {}, "warnings": warnings}
|
|
86
|
+
if not root.is_dir():
|
|
87
|
+
known = sorted(
|
|
88
|
+
p.parent.parent.name for p in domains_root().glob("*/copy/module_labels.json")
|
|
89
|
+
if p.parent.parent.name != NEUTRAL_DOMAIN
|
|
90
|
+
)
|
|
91
|
+
msg = f"unknown domain {domain!r} (copy packs: {', '.join(known) or 'none'})"
|
|
92
|
+
if strict:
|
|
93
|
+
raise KeyError(msg)
|
|
94
|
+
warnings.append(msg + "; using neutral fallback copy")
|
|
95
|
+
pack["domain"] = NEUTRAL_DOMAIN
|
|
96
|
+
pack["files"] = load_copy_pack(NEUTRAL_DOMAIN, strict=True)["files"]
|
|
97
|
+
return pack
|
|
98
|
+
# optional topic-split companions first (module_labels_*.json, framing_enums.json)
|
|
99
|
+
for extra in sorted(root.glob("*.json")):
|
|
100
|
+
pack["files"][extra.name] = json.loads(extra.read_text(encoding="utf-8"))
|
|
101
|
+
missing = [name for name in COPY_PACK_FILES if name not in pack["files"]]
|
|
102
|
+
if missing:
|
|
103
|
+
msg = f"domain {domain!r} copy pack incomplete: missing {', '.join(missing)}"
|
|
104
|
+
if strict:
|
|
105
|
+
raise FileNotFoundError(msg)
|
|
106
|
+
warnings.append(msg + "; merging neutral fallback for gaps")
|
|
107
|
+
neutral = load_copy_pack(NEUTRAL_DOMAIN, strict=True)["files"]
|
|
108
|
+
for name in missing:
|
|
109
|
+
pack["files"][name] = neutral[name]
|
|
110
|
+
_merge_companions(pack["files"])
|
|
111
|
+
if domain == "policy":
|
|
112
|
+
assert_policy_copy_clean(pack["files"])
|
|
113
|
+
return pack
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _merge_companions(files: dict[str, Any]) -> None:
|
|
117
|
+
"""Fold topic-split companions into the canonical six-file view."""
|
|
118
|
+
enums = files.get("framing_enums.json") or {}
|
|
119
|
+
lex = files.setdefault("framing_lexicon.json", {})
|
|
120
|
+
if enums.get("frame_enums"):
|
|
121
|
+
lex["frame_enums"] = enums["frame_enums"]
|
|
122
|
+
base = files.setdefault("module_labels.json", {})
|
|
123
|
+
base_labels = base.setdefault("labels", {})
|
|
124
|
+
for name, payload in list(files.items()):
|
|
125
|
+
if name.startswith("module_labels") and name != "module_labels.json":
|
|
126
|
+
base_labels.update((payload or {}).get("labels") or {})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def activate_copy_pack(result: dict, *, strict: bool = False) -> dict[str, Any]:
|
|
130
|
+
global _ACTIVE_PACK, _COPY_WARNINGS
|
|
131
|
+
domain, warnings = resolve_domain(result)
|
|
132
|
+
pack = load_copy_pack(domain, strict=strict)
|
|
133
|
+
_COPY_WARNINGS = list(warnings) + list(pack.get("warnings") or [])
|
|
134
|
+
for message in _COPY_WARNINGS:
|
|
135
|
+
print(f"WARNING: {message}", file=sys.stderr)
|
|
136
|
+
_ACTIVE_PACK = pack
|
|
137
|
+
return pack
|
|
138
|
+
|
|
139
|
+
def active_copy_pack() -> dict[str, Any]:
|
|
140
|
+
# Prefer the most recently activated pack; fall back to neutral.
|
|
141
|
+
return _ACTIVE_PACK or load_copy_pack(NEUTRAL_DOMAIN)
|
|
142
|
+
|
|
143
|
+
def copy_warnings() -> list[str]:
|
|
144
|
+
return list(_COPY_WARNINGS)
|
|
145
|
+
|
|
146
|
+
def _bilingual(files: dict, file_name: str, *keys, lang: str, default=None):
|
|
147
|
+
node = files.get(file_name) or {}
|
|
148
|
+
for key in keys:
|
|
149
|
+
node = node.get(key) if isinstance(node, dict) else None
|
|
150
|
+
if node is None:
|
|
151
|
+
return default if default is not None else {}
|
|
152
|
+
if isinstance(node, dict) and ("zh" in node or "en" in node):
|
|
153
|
+
return node.get(lang, default)
|
|
154
|
+
return node if node is not None else default
|
|
155
|
+
|
|
156
|
+
def build_ui(lang: str, pack: dict | None = None) -> dict:
|
|
157
|
+
from zh_labels import merge_ui
|
|
158
|
+
|
|
159
|
+
pack = pack or active_copy_pack()
|
|
160
|
+
files = pack.get("files") or {}
|
|
161
|
+
ui = merge_ui(lang, _bilingual(files, "module_labels.json", "labels", lang=lang, default={}))
|
|
162
|
+
titles = _bilingual(files, "section_titles.json", "section_titles", lang=lang, default={}) or {}
|
|
163
|
+
leads = _bilingual(files, "section_titles.json", "section_leads", lang=lang, default={}) or {}
|
|
164
|
+
if titles:
|
|
165
|
+
ui["section_titles"] = titles
|
|
166
|
+
if leads:
|
|
167
|
+
ui["section_leads"] = leads
|
|
168
|
+
risk = files.get("risk_constructs.json") or {}
|
|
169
|
+
for ui_key, risk_key in (
|
|
170
|
+
("outcome_separation_title", "separation_title"),
|
|
171
|
+
("outcome_sep_note", "separation_note"),
|
|
172
|
+
("method_guard", "method_guard"),
|
|
173
|
+
):
|
|
174
|
+
value = risk.get(risk_key) or {}
|
|
175
|
+
picked = value.get(lang) if isinstance(value, dict) else value
|
|
176
|
+
if picked:
|
|
177
|
+
ui[ui_key] = picked
|
|
178
|
+
principle = (risk.get("guard_principle") or {}).get(lang)
|
|
179
|
+
if principle:
|
|
180
|
+
ui["guard_principle"] = principle
|
|
181
|
+
return ui
|
|
182
|
+
|
|
183
|
+
def frame_enum_table(lang: str) -> dict[str, str]:
|
|
184
|
+
files = active_copy_pack().get("files") or {}
|
|
185
|
+
merged = dict(_bilingual(files, "framing_enums.json", "frame_enums", lang=lang, default={}) or {})
|
|
186
|
+
merged.update(_bilingual(files, "framing_lexicon.json", "frame_enums", lang=lang, default={}) or {})
|
|
187
|
+
return merged
|
|
188
|
+
|
|
189
|
+
def methodology_label_table(lang: str) -> dict[str, str]:
|
|
190
|
+
files = active_copy_pack().get("files") or {}
|
|
191
|
+
return dict(_bilingual(files, "terminology.json", "methodology_labels", lang=lang, default={}) or {})
|
|
192
|
+
|
|
193
|
+
def scope_field_labels(lang: str) -> dict[str, str]:
|
|
194
|
+
files = active_copy_pack().get("files") or {}
|
|
195
|
+
return dict(_bilingual(files, "framing_lexicon.json", "frame_fields", lang=lang, default={}) or {})
|
|
196
|
+
|
|
197
|
+
def scope_subfield_labels(lang: str, section: str) -> dict[str, str]:
|
|
198
|
+
files = active_copy_pack().get("files") or {}
|
|
199
|
+
sub = _bilingual(files, "framing_lexicon.json", "frame_subfields", lang=lang, default={}) or {}
|
|
200
|
+
return dict(sub.get(section) or {})
|
|
201
|
+
|
|
202
|
+
def evidence_detail_label_table(lang: str) -> dict[str, str]:
|
|
203
|
+
files = active_copy_pack().get("files") or {}
|
|
204
|
+
return dict(_bilingual(files, "framing_lexicon.json", "evidence_detail_labels", lang=lang, default={}) or {})
|
|
205
|
+
|
|
206
|
+
def outcome_group_map() -> dict[str, list[str]]:
|
|
207
|
+
files = active_copy_pack().get("files") or {}
|
|
208
|
+
groups = (files.get("risk_constructs.json") or {}).get("outcome_groups") or {}
|
|
209
|
+
return {key: list(values or []) for key, values in groups.items()}
|
|
210
|
+
|
|
211
|
+
def default_full_report_plan() -> list[dict[str, Any]]:
|
|
212
|
+
files = active_copy_pack().get("files") or {}
|
|
213
|
+
chapters = (files.get("section_titles.json") or {}).get("default_chapters") or []
|
|
214
|
+
by_key = {str(c.get("key") or ""): c for c in chapters if isinstance(c, dict)}
|
|
215
|
+
plan = []
|
|
216
|
+
for skeleton in DEFAULT_CHAPTER_SKELETON:
|
|
217
|
+
chapter = dict(skeleton)
|
|
218
|
+
source = by_key.get(skeleton["key"]) or {}
|
|
219
|
+
chapter["title_zh"] = source.get("title_zh") or skeleton["key"]
|
|
220
|
+
chapter["title_en"] = source.get("title_en") or skeleton["key"]
|
|
221
|
+
chapter["lead_zh"] = source.get("lead_zh") or ""
|
|
222
|
+
chapter["lead_en"] = source.get("lead_en") or ""
|
|
223
|
+
if source.get("modules"):
|
|
224
|
+
chapter["modules"] = tuple(
|
|
225
|
+
m for m in source["modules"] if m in FULL_REPORT_MODULES
|
|
226
|
+
) or skeleton["modules"]
|
|
227
|
+
plan.append(chapter)
|
|
228
|
+
return plan
|
|
229
|
+
|
|
230
|
+
def default_leads(lang: str) -> dict[str, str]:
|
|
231
|
+
key = "lead_zh" if lang == "zh" else "lead_en"
|
|
232
|
+
return {chapter["key"]: chapter.get(key) or "" for chapter in default_full_report_plan()}
|
|
233
|
+
|
|
234
|
+
def brief_block_titles(lang: str) -> dict[str, tuple[str, str]]:
|
|
235
|
+
files = active_copy_pack().get("files") or {}
|
|
236
|
+
raw = _bilingual(files, "section_titles.json", "brief_blocks", lang=lang, default={}) or {}
|
|
237
|
+
out: dict[str, tuple[str, str]] = {}
|
|
238
|
+
for key, value in raw.items():
|
|
239
|
+
if isinstance(value, (list, tuple)) and len(value) >= 2:
|
|
240
|
+
out[key] = (str(value[0]), str(value[1]))
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
# Infographic SVG titles/keywords now resolve through build_infographics(ui=...).
|
|
244
|
+
# retitle_infographics remains a thin residual for callers that still render
|
|
245
|
+
# with the adapter's built-in education defaults and no ui.
|
|
246
|
+
_SVG_TITLE_FIXES = {
|
|
247
|
+
"zh": {
|
|
248
|
+
"教学干预时间线": "intervention",
|
|
249
|
+
"评价设计流程": "evaluation",
|
|
250
|
+
},
|
|
251
|
+
"en": {
|
|
252
|
+
"Intervention Timeline": "intervention",
|
|
253
|
+
"Evaluation Design Flow": "evaluation",
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def retitle_infographics(infographics: dict, lang: str, ui: dict) -> dict:
|
|
259
|
+
"""Replace any leftover education-hardcoded SVG strings with domain copy.
|
|
260
|
+
|
|
261
|
+
Prefer build_infographics(..., ui=build_ui(lang)); this residual only
|
|
262
|
+
patches titles/keywords that still carry the built-in education defaults.
|
|
263
|
+
"""
|
|
264
|
+
ui_keys = {
|
|
265
|
+
"intervention": ("svg_intervention_title", "Intervention Timeline"),
|
|
266
|
+
"evaluation": ("svg_evaluation_title", "Evaluation Design Flow"),
|
|
267
|
+
}
|
|
268
|
+
measures = list(ui.get("evaluation_measures") or [])
|
|
269
|
+
# EVAL_NODES in build_infographics: baseline/pre, post, retention, transfer.
|
|
270
|
+
node_fixes = {
|
|
271
|
+
"zh": {
|
|
272
|
+
"保持测试": measures[2] if len(measures) > 2 else "持续监测",
|
|
273
|
+
"迁移测试(无 AI)": measures[3] if len(measures) > 3 else "扩展评估",
|
|
274
|
+
"迁移测试": measures[3] if len(measures) > 3 else "扩展评估",
|
|
275
|
+
},
|
|
276
|
+
"en": {
|
|
277
|
+
"retention test": measures[2] if len(measures) > 2 else "Durability",
|
|
278
|
+
"transfer test (no AI)": measures[3] if len(measures) > 3 else "Scale-out",
|
|
279
|
+
"transfer test": measures[3] if len(measures) > 3 else "Scale-out",
|
|
280
|
+
},
|
|
281
|
+
}.get(lang, {})
|
|
282
|
+
out = {}
|
|
283
|
+
for key, svg in (infographics or {}).items():
|
|
284
|
+
text = svg or ""
|
|
285
|
+
for old, slot in _SVG_TITLE_FIXES.get(lang, {}).items():
|
|
286
|
+
if old not in text:
|
|
287
|
+
continue
|
|
288
|
+
ui_key, fallback = ui_keys.get(slot, (None, None))
|
|
289
|
+
replacement = (ui.get(ui_key) if ui_key else None) or fallback or old
|
|
290
|
+
if replacement and replacement != old:
|
|
291
|
+
text = text.replace(old, replacement)
|
|
292
|
+
for old, replacement in node_fixes.items():
|
|
293
|
+
if replacement and replacement != old:
|
|
294
|
+
text = text.replace(old, replacement)
|
|
295
|
+
out[key] = text
|
|
296
|
+
return out
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""report_copy_policy_guard.py — teaching-vocabulary ban for the policy copy pack."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
#: Teaching / education-research vocabulary banned from the policy pack.
|
|
8
|
+
TEACHING_WORDS_ZH = (
|
|
9
|
+
"教学", "学习者", "目标学习者", "学习指标", "学习效果", "学习结果", "学习过程",
|
|
10
|
+
"学习能力", "学习收获", "学习动机", "学习 /", "AI 干预", "AI干预", "教师",
|
|
11
|
+
"课程情境", "教育阶段", "教学决策", "教学行动", "教学干预",
|
|
12
|
+
"任务表现 ≠ 学习效果", "任务表现≠学习效果", "保持测试", "迁移测试", "独立学习",
|
|
13
|
+
)
|
|
14
|
+
TEACHING_WORDS_EN = (
|
|
15
|
+
"AI intervention", "Target learners", "Learning metrics", "Learning effect",
|
|
16
|
+
"Learning outcome", "Teaching intervention", "Teaching decision", "Teaching action",
|
|
17
|
+
"learning gain", "student learning",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def iter_strings(node, path="$"):
|
|
22
|
+
if isinstance(node, dict):
|
|
23
|
+
for key, value in node.items():
|
|
24
|
+
yield from iter_strings(value, f"{path}.{key}")
|
|
25
|
+
elif isinstance(node, list):
|
|
26
|
+
for index, value in enumerate(node):
|
|
27
|
+
yield from iter_strings(value, f"{path}[{index}]")
|
|
28
|
+
elif isinstance(node, str):
|
|
29
|
+
yield path, node
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def assert_policy_copy_clean(files: dict[str, Any]) -> None:
|
|
33
|
+
"""Policy pack must not contain teaching vocabulary (fail-clear)."""
|
|
34
|
+
hits = []
|
|
35
|
+
for name, data in files.items():
|
|
36
|
+
for path, text in iter_strings(data):
|
|
37
|
+
for word in TEACHING_WORDS_ZH:
|
|
38
|
+
if word in text:
|
|
39
|
+
hits.append(f"{name} {path}: {word!r} in {text!r}")
|
|
40
|
+
lowered = text.lower()
|
|
41
|
+
for phrase in TEACHING_WORDS_EN:
|
|
42
|
+
if phrase.lower() in lowered:
|
|
43
|
+
hits.append(f"{name} {path}: {phrase!r} in {text!r}")
|
|
44
|
+
if hits:
|
|
45
|
+
raise AssertionError(
|
|
46
|
+
"policy copy pack contains teaching vocabulary:\n " + "\n ".join(hits)
|
|
47
|
+
)
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
12
16
|
OUTCOME_ZH = {
|
|
13
17
|
"knowledge_gain": "知识获得",
|
|
14
18
|
"concept_understanding": "概念理解",
|
|
@@ -34,6 +38,12 @@ OUTCOME_ZH = {
|
|
|
34
38
|
"task_performance": "任务表现",
|
|
35
39
|
"programming_skill": "编程技能",
|
|
36
40
|
"writing_skill": "写作技能",
|
|
41
|
+
# policy-domain outcome tokens (domains/policy/outcome_taxonomy.json)
|
|
42
|
+
"policy_effectiveness": "政策有效性",
|
|
43
|
+
"cost_effectiveness": "成本效益",
|
|
44
|
+
"equity": "公平性",
|
|
45
|
+
"feasibility": "可行性",
|
|
46
|
+
"implementation_risk": "实施风险",
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
STUDY_ZH = {
|
|
@@ -145,6 +155,12 @@ OUTCOME_EN = {
|
|
|
145
155
|
"task_performance": "Task performance",
|
|
146
156
|
"programming_skill": "Programming skill",
|
|
147
157
|
"writing_skill": "Writing skill",
|
|
158
|
+
# policy-domain outcome tokens (domains/policy/outcome_taxonomy.json)
|
|
159
|
+
"policy_effectiveness": "Policy effectiveness",
|
|
160
|
+
"cost_effectiveness": "Cost-effectiveness",
|
|
161
|
+
"equity": "Equity",
|
|
162
|
+
"feasibility": "Feasibility",
|
|
163
|
+
"implementation_risk": "Implementation risk",
|
|
148
164
|
}
|
|
149
165
|
STUDY_EN = {
|
|
150
166
|
"rct": "Randomized controlled trial",
|
|
@@ -322,3 +338,48 @@ def humanize_identifier(value: str, lang: str = "en") -> str:
|
|
|
322
338
|
out.append(word[:1].upper() + word[1:])
|
|
323
339
|
return " ".join(out)
|
|
324
340
|
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Neutral UI dictionary lives in domains/_neutral/copy/module_labels.json.
|
|
344
|
+
# Domain packs overlay it via merge_ui(); this module stays enum-label only.
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
_NEUTRAL_UI_PATH = Path(__file__).resolve().parents[3] / "domains" / "_neutral" / "copy" / "module_labels.json"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def load_neutral_ui() -> dict[str, dict]:
|
|
350
|
+
"""Return {key: {"zh": ..., "en": ...}} for domain-neutral UI labels.
|
|
351
|
+
|
|
352
|
+
Neutral labels may be topic-split across module_labels*.json.
|
|
353
|
+
"""
|
|
354
|
+
labels: dict[str, dict] = {}
|
|
355
|
+
root = _NEUTRAL_UI_PATH.parent
|
|
356
|
+
if not root.is_dir():
|
|
357
|
+
return labels
|
|
358
|
+
for path in sorted(root.glob("module_labels*.json")):
|
|
359
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
360
|
+
labels.update(data.get("labels") or {})
|
|
361
|
+
return labels
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def merge_ui(lang: str, domain_labels: dict | None = None) -> dict:
|
|
365
|
+
"""Neutral UI overlaid with domain module_labels for `lang`."""
|
|
366
|
+
base: dict[str, Any] = {}
|
|
367
|
+
for key, value in load_neutral_ui().items():
|
|
368
|
+
if isinstance(value, dict) and ("zh" in value or "en" in value):
|
|
369
|
+
picked = value.get(lang)
|
|
370
|
+
if picked is not None:
|
|
371
|
+
base[key] = picked
|
|
372
|
+
else:
|
|
373
|
+
base[key] = value
|
|
374
|
+
if not domain_labels:
|
|
375
|
+
return base
|
|
376
|
+
for key, value in domain_labels.items():
|
|
377
|
+
if key == "section_key_note":
|
|
378
|
+
continue
|
|
379
|
+
if isinstance(value, dict) and ("zh" in value or "en" in value):
|
|
380
|
+
picked = value.get(lang)
|
|
381
|
+
if picked is not None:
|
|
382
|
+
base[key] = picked
|
|
383
|
+
else:
|
|
384
|
+
base[key] = value
|
|
385
|
+
return base
|