eduevidence 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +506 -0
  3. package/README.zh-CN.md +434 -0
  4. package/SKILL.md +195 -0
  5. package/bin/eduevidence.js +127 -0
  6. package/domains/education/manifest.json +183 -0
  7. package/domains/education/outcome_taxonomy.json +127 -0
  8. package/domains/manifest.json +26 -0
  9. package/domains/policy/frame.schema.json +234 -0
  10. package/domains/policy/manifest.json +10 -0
  11. package/domains/policy/methodology_checklist.json +109 -0
  12. package/domains/policy/outcome_taxonomy.json +53 -0
  13. package/domains/policy/references/causal-identification.md +45 -0
  14. package/domains/policy/references/cost-evidence.md +44 -0
  15. package/domains/policy/references/equity.md +42 -0
  16. package/domains/policy/references/evidence-hierarchy.md +41 -0
  17. package/domains/policy/references/implementation-evidence.md +47 -0
  18. package/eduevidence_cli.py +26 -0
  19. package/engine/__init__.py +11 -0
  20. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  22. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  23. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  24. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  25. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  26. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  27. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  28. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  29. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  30. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  31. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  32. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  33. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  34. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  35. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  36. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  37. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  38. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  39. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  40. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  41. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  42. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  43. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  44. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  45. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  46. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  47. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  48. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  49. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  50. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  51. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  52. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  53. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  54. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  55. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  56. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  57. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  58. package/engine/analysis.py +308 -0
  59. package/engine/bias.py +178 -0
  60. package/engine/briefs.py +106 -0
  61. package/engine/capabilities.py +99 -0
  62. package/engine/citation_check.py +192 -0
  63. package/engine/contracts.py +117 -0
  64. package/engine/datasets.py +165 -0
  65. package/engine/events.py +67 -0
  66. package/engine/evidence_graph.py +571 -0
  67. package/engine/evidence_review.py +88 -0
  68. package/engine/evidencecore.py +182 -0
  69. package/engine/gap_lens.py +132 -0
  70. package/engine/gaps.py +169 -0
  71. package/engine/graph_store.py +335 -0
  72. package/engine/graph_validate.py +87 -0
  73. package/engine/ids.py +77 -0
  74. package/engine/library.py +268 -0
  75. package/engine/library_builtin.py +301 -0
  76. package/engine/living.py +671 -0
  77. package/engine/log.py +39 -0
  78. package/engine/meta_analysis.py +333 -0
  79. package/engine/meta_synthesis.py +111 -0
  80. package/engine/migration.py +397 -0
  81. package/engine/mode_router.py +72 -0
  82. package/engine/paths.py +15 -0
  83. package/engine/pilot.py +368 -0
  84. package/engine/planner.py +126 -0
  85. package/engine/project.py +118 -0
  86. package/engine/projections.py +240 -0
  87. package/engine/robustness.py +109 -0
  88. package/engine/run.py +85 -0
  89. package/engine/semantics.py +135 -0
  90. package/engine/study_design.py +87 -0
  91. package/engine/synthesis.py +187 -0
  92. package/engine/tribunal.py +408 -0
  93. package/engine/update.py +113 -0
  94. package/engine/versions.py +12 -0
  95. package/install.sh +510 -0
  96. package/integrations/__init__.py +1 -0
  97. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  98. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  99. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  100. package/integrations/agent_mcp.py +856 -0
  101. package/integrations/smart_web_fetch.py +59 -0
  102. package/package.json +50 -0
  103. package/pyproject.toml +55 -0
  104. package/references/applicability-policy.md +88 -0
  105. package/references/education-framing.md +132 -0
  106. package/references/effect_size_formulas.md +35 -0
  107. package/references/evaluation-design.md +111 -0
  108. package/references/evidence-quality.md +79 -0
  109. package/references/grade_framework.md +29 -0
  110. package/references/intervention-design.md +98 -0
  111. package/references/methodology-audit.md +103 -0
  112. package/references/outcome-taxonomy.md +106 -0
  113. package/references/retrieval-protocol.md +142 -0
  114. package/references/skeptic-protocol.md +93 -0
  115. package/references/social_science_pitfalls.md +48 -0
  116. package/references/source-validity.md +140 -0
  117. package/references/tribunal-policy.md +112 -0
  118. package/references/wwc_standards.md +29 -0
  119. package/retrieval/__init__.py +1 -0
  120. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  121. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  122. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  123. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  124. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  125. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  126. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  127. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  128. package/retrieval/corpus_store.py +181 -0
  129. package/retrieval/dedupe.py +127 -0
  130. package/retrieval/failures.py +90 -0
  131. package/retrieval/fetch.py +435 -0
  132. package/retrieval/search.py +493 -0
  133. package/retrieval/source.py +160 -0
  134. package/retrieval/validate.py +257 -0
  135. package/schemas/agent-mcp-approval.schema.json +57 -0
  136. package/schemas/chart-spec.schema.json +88 -0
  137. package/schemas/cross-model-review.schema.json +28 -0
  138. package/schemas/education-frame.schema.json +106 -0
  139. package/schemas/evaluation.schema.json +35 -0
  140. package/schemas/evidence.schema.json +81 -0
  141. package/schemas/fetch-result.schema.json +119 -0
  142. package/schemas/intervention.schema.json +46 -0
  143. package/schemas/methodology.schema.json +102 -0
  144. package/schemas/report-result.schema.json +381 -0
  145. package/schemas/report-spec.schema.json +130 -0
  146. package/schemas/source.schema.json +311 -0
  147. package/schemas/v2/analysis-plan.schema.json +28 -0
  148. package/schemas/v2/analysis-run.schema.json +33 -0
  149. package/schemas/v2/claim.schema.json +26 -0
  150. package/schemas/v2/dataset-asset.schema.json +40 -0
  151. package/schemas/v2/decision-snapshot.schema.json +53 -0
  152. package/schemas/v2/evidence-link.schema.json +38 -0
  153. package/schemas/v2/finding.schema.json +47 -0
  154. package/schemas/v2/graph-revision.schema.json +30 -0
  155. package/schemas/v2/knowledge-gap.schema.json +40 -0
  156. package/schemas/v2/methodology-audit.schema.json +30 -0
  157. package/schemas/v2/outcome.schema.json +18 -0
  158. package/schemas/v2/project.schema.json +31 -0
  159. package/schemas/v2/research-intent.schema.json +24 -0
  160. package/schemas/v2/run.schema.json +43 -0
  161. package/schemas/v2/source.schema.json +24 -0
  162. package/schemas/v2/study-design.schema.json +67 -0
  163. package/schemas/v2/study.schema.json +37 -0
  164. package/schemas/v3/pilot-outcome.schema.json +132 -0
  165. package/schemas/v3/run-manifest.schema.json +193 -0
  166. package/schemas/v3/synthesis.schema.json +86 -0
  167. package/schemas/v4/drift-report.schema.json +66 -0
  168. package/schemas/v4/evidence-library.schema.json +42 -0
  169. package/schemas/v4/living-subscription.schema.json +55 -0
  170. package/schemas/v4/meta-analysis.schema.json +152 -0
  171. package/schemas/verdict.schema.json +56 -0
  172. package/scripts/__init__.py +0 -0
  173. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  175. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  176. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  177. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  178. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  179. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  180. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  181. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  182. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  183. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  184. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  185. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  186. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  187. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  188. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  189. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  190. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  191. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  192. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  193. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  194. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  195. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  196. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  197. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  198. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  199. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  200. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  201. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  202. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  203. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  204. package/scripts/audit_dois.py +292 -0
  205. package/scripts/bake_pack.sh +37 -0
  206. package/scripts/benchmark.py +183 -0
  207. package/scripts/benchmark_evaluator.py +371 -0
  208. package/scripts/benchmark_judge.py +535 -0
  209. package/scripts/benchmark_routing.py +120 -0
  210. package/scripts/benchmark_v2.py +304 -0
  211. package/scripts/benchmark_v3.py +552 -0
  212. package/scripts/build_esl_artifacts.py +1921 -0
  213. package/scripts/build_evidence_library.py +307 -0
  214. package/scripts/build_killer_demo.py +295 -0
  215. package/scripts/build_result.py +311 -0
  216. package/scripts/check_version_consistency.py +96 -0
  217. package/scripts/citation_check.py +123 -0
  218. package/scripts/claim_audit.py +157 -0
  219. package/scripts/complexity_gate.py +180 -0
  220. package/scripts/compute_confidence.py +176 -0
  221. package/scripts/dashboard_server.py +536 -0
  222. package/scripts/did_regression.py +315 -0
  223. package/scripts/effect_calculator.py +99 -0
  224. package/scripts/enrich_projects_human_and_lieflat.py +315 -0
  225. package/scripts/evidence_matrix.py +129 -0
  226. package/scripts/evidence_score.py +234 -0
  227. package/scripts/evidence_semantics.py +87 -0
  228. package/scripts/fetch_benchmark.py +177 -0
  229. package/scripts/generate_metrics.py +99 -0
  230. package/scripts/generate_new_projects.py +686 -0
  231. package/scripts/generate_promo.py +192 -0
  232. package/scripts/lint_report_layout.py +182 -0
  233. package/scripts/orchestrator.py +1456 -0
  234. package/scripts/pre_verdict_gate.py +513 -0
  235. package/scripts/quickstart.py +121 -0
  236. package/scripts/rebake_all_5themes.py +88 -0
  237. package/scripts/recompute_demo_quality.py +205 -0
  238. package/scripts/render_report.py +270 -0
  239. package/scripts/render_report_html.py +356 -0
  240. package/scripts/retraction_watch.py +110 -0
  241. package/scripts/run_workspace.py +337 -0
  242. package/scripts/serve_web.py +54 -0
  243. package/scripts/skill_lint.py +150 -0
  244. package/scripts/startup_probe.py +265 -0
  245. package/scripts/sync_killer_demo_report.py +270 -0
  246. package/scripts/test_adversarial_empirical.py +541 -0
  247. package/scripts/validate_schema.py +256 -0
  248. package/skill/agents/education-planner.md +80 -0
  249. package/skill/agents/evaluation-designer.md +74 -0
  250. package/skill/agents/evidence-analyst.md +106 -0
  251. package/skill/agents/evidence-judge.md +111 -0
  252. package/skill/agents/evidence-retriever.md +80 -0
  253. package/skill/agents/intervention-designer.md +82 -0
  254. package/skill/agents/method-reviewer.md +104 -0
  255. package/skill/agents/skeptic.md +89 -0
  256. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
  257. package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
  258. package/skill/sub-skills/data-analysis/SKILL.md +23 -0
  259. package/skill/sub-skills/ethics-review/SKILL.md +25 -0
  260. package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
  261. package/skill/sub-skills/evidence-review/SKILL.md +18 -0
  262. package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
  263. package/skill/sub-skills/literature-review/SKILL.md +21 -0
  264. package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
  265. package/skill/sub-skills/report-generation/SKILL.md +51 -0
  266. package/skill/sub-skills/research-planning/SKILL.md +21 -0
  267. package/skill/sub-skills/study-design/SKILL.md +16 -0
  268. package/skill/task-briefs/adjudicate.md +17 -0
  269. package/skill/task-briefs/audit.md +15 -0
  270. package/skill/task-briefs/challenge.md +15 -0
  271. package/skill/task-briefs/evaluate.md +13 -0
  272. package/skill/task-briefs/extract.md +16 -0
  273. package/skill/task-briefs/frame.md +17 -0
  274. package/skill/task-briefs/intervene.md +14 -0
  275. package/skill/task-briefs/present.md +16 -0
  276. package/skill/task-briefs/retrieve.md +15 -0
  277. package/visualization/eduevidence-report/assets/base.css +337 -0
  278. package/visualization/eduevidence-report/motion/motion.css +157 -0
  279. package/visualization/eduevidence-report/motion/motion.js +107 -0
  280. package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
  281. package/visualization/eduevidence-report/references/component-catalog.md +145 -0
  282. package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
  283. package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
  284. package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
  285. package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
  286. package/visualization/eduevidence-report/references/motion-system.md +31 -0
  287. package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
  288. package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
  289. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  290. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  291. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  292. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  293. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  294. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  295. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  296. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  297. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
  298. package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
  299. package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
  300. package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
  301. package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
  302. package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
  303. package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
  304. package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
  305. package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
  306. package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
  307. package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
  308. package/visualization/eduevidence-report/themes/academic.css +94 -0
  309. package/visualization/eduevidence-report/themes/claude.css +96 -0
  310. package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
  311. package/visualization/eduevidence-report/themes/datalab.css +151 -0
  312. package/visualization/eduevidence-report/themes/presentation.css +140 -0
@@ -0,0 +1,268 @@
1
+ """Snapshot-based Shared Research Library.
2
+
3
+ Reusable research facts (Source/Study/Finding/MethodologyAudit) live in an
4
+ immutable library revision model mirroring the Project GraphStore's
5
+ crash-safe pattern. Projects import snapshots: a Project never evaluates
6
+ directly against library JSONL, and a later library change never silently
7
+ changes an existing Project's conclusions — only an explicit import/sync
8
+ advances the Project graph.
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ from engine.contracts import validate_record
18
+ from engine.graph_store import GraphStore, GraphMutation, GraphRevision, _atomic_write_text
19
+
20
+ LIBRARY_TABLES = ("sources", "studies", "findings", "audits")
21
+ _TABLE_SCHEMA = {
22
+ "sources": "source",
23
+ "studies": "study",
24
+ "findings": "finding",
25
+ "audits": "methodology-audit",
26
+ }
27
+ _TABLE_ID_KEY = {
28
+ "sources": "source_id",
29
+ "studies": "study_id",
30
+ "findings": "finding_id",
31
+ "audits": "audit_id",
32
+ }
33
+
34
+
35
+ def _now_iso() -> str:
36
+ return datetime.now(timezone.utc).isoformat()
37
+
38
+
39
+ class ResearchLibrary:
40
+ """Shared Research Library under `home/library/`."""
41
+
42
+ def __init__(self, home: Path):
43
+ self.home = Path(home).expanduser().resolve()
44
+ self.library_dir = self.home / "library"
45
+ self.revisions_dir = self.library_dir / "revisions"
46
+ self.head_path = self.library_dir / "HEAD"
47
+
48
+ @classmethod
49
+ def open(cls, home: Path) -> "ResearchLibrary":
50
+ lib = cls(home)
51
+ lib.revisions_dir.mkdir(parents=True, exist_ok=True)
52
+ if not lib.head_path.is_file():
53
+ _atomic_write_text(lib.head_path, "0")
54
+ return lib
55
+
56
+ def active_revision(self) -> int:
57
+ return int(self.head_path.read_text(encoding="utf-8").strip())
58
+
59
+ def _revision_dir(self, revision: int) -> Path:
60
+ return self.revisions_dir / f"rev-{revision:06d}"
61
+
62
+ def _read_snapshot(self, revision: int) -> dict[str, list[dict]]:
63
+ if revision == 0:
64
+ return {t: [] for t in LIBRARY_TABLES}
65
+ rev_dir = self._revision_dir(revision)
66
+ snapshot: dict[str, list[dict]] = {}
67
+ for table in LIBRARY_TABLES:
68
+ path = rev_dir / f"{table}.jsonl"
69
+ if not path.is_file():
70
+ raise FileNotFoundError(f"library revision {revision} missing {path.name}")
71
+ rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
72
+ snapshot[table] = rows
73
+ return snapshot
74
+
75
+ def read_table(self, table: str) -> list[dict]:
76
+ if table not in LIBRARY_TABLES:
77
+ raise ValueError(f"unknown library table {table!r}")
78
+ return self._read_snapshot(self.active_revision())[table]
79
+
80
+ # ---- mutation --------------------------------------------------------
81
+
82
+ def add_verified_bundle(self, *, sources: list[dict], studies: list[dict],
83
+ findings: list[dict], audits: list[dict]) -> int:
84
+ """Append verified facts as a new immutable library revision.
85
+
86
+ Returns the new revision number. Existing revisions are never
87
+ rewritten. Upserts are by entity id within each table.
88
+ """
89
+ before_rev = self.active_revision()
90
+ before = self._read_snapshot(before_rev)
91
+ after = {t: [dict(r) for r in rows] for t, rows in before.items()}
92
+ for table, incoming in (
93
+ ("sources", sources), ("studies", studies),
94
+ ("findings", findings), ("audits", audits),
95
+ ):
96
+ id_key = _TABLE_ID_KEY[table]
97
+ by_id = {r[id_key]: r for r in after[table]}
98
+ for rec in incoming:
99
+ errors = validate_record(_TABLE_SCHEMA[table], rec)
100
+ if errors:
101
+ raise ValueError(f"library {table} {rec.get(id_key, '?')} invalid: {errors}")
102
+ by_id[rec[id_key]] = dict(rec)
103
+ after[table] = list(by_id.values())
104
+
105
+ next_rev = before_rev + 1
106
+ tmp_dir = self.revisions_dir / f".tmp-{next_rev:06d}"
107
+ tmp_dir.mkdir(parents=True, exist_ok=True)
108
+ try:
109
+ for table in LIBRARY_TABLES:
110
+ lines = "".join(
111
+ json.dumps(r, sort_keys=True, separators=(",", ":")) + "\n"
112
+ for r in after[table]
113
+ )
114
+ (tmp_dir / f"{table}.jsonl").write_text(lines, encoding="utf-8")
115
+ manifest = {
116
+ "revision": next_rev,
117
+ "parent_revision": before_rev,
118
+ "created_at": _now_iso(),
119
+ "extensions": {},
120
+ }
121
+ (tmp_dir / "manifest.json").write_text(
122
+ json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
123
+ rev_dir = self._revision_dir(next_rev)
124
+ if rev_dir.exists():
125
+ raise FileExistsError(f"refusing to rewrite library revision {rev_dir}")
126
+ os.replace(tmp_dir, rev_dir)
127
+ _atomic_write_text(self.head_path, str(next_rev))
128
+ except Exception:
129
+ if tmp_dir.exists():
130
+ import shutil
131
+ shutil.rmtree(tmp_dir)
132
+ raise
133
+ return next_rev
134
+
135
+ # ---- find ------------------------------------------------------------
136
+
137
+ def find_source(self, canonical_locator: str) -> dict | None:
138
+ for src in self.read_table("sources"):
139
+ if src["canonical_locator"] == canonical_locator:
140
+ return src
141
+ return None
142
+
143
+ # ---- snapshot import -------------------------------------------------
144
+
145
+ def import_snapshot(self, *, project, source_ids: list[str],
146
+ run_id: str) -> GraphRevision:
147
+ """Import the selected library facts into a Project graph revision.
148
+
149
+ Imported entities get `extensions.origin` metadata recording the
150
+ library revision/entity id/content hash, so the Project never depends
151
+ on the live library.
152
+ """
153
+ snapshot = self._read_snapshot(self.active_revision())
154
+ requested_ids = set(source_ids)
155
+ sources = [s for s in snapshot["sources"] if s["source_id"] in requested_ids]
156
+ if len(sources) != len(requested_ids):
157
+ missing = requested_ids - {s["source_id"] for s in sources}
158
+ raise ValueError(f"library has no sources for: {sorted(missing)}")
159
+ src_ids = {s["source_id"] for s in sources}
160
+ # a Study citing multiple sources must import them all (transitive
161
+ # closure) so cross-entity validation never sees a dangling reference
162
+ studies = [s for s in snapshot["studies"] if set(s["source_ids"]) & src_ids]
163
+ for s in studies:
164
+ extra = set(s["source_ids"]) - src_ids
165
+ if extra:
166
+ sources.extend(
167
+ src for src in snapshot["sources"] if src["source_id"] in extra)
168
+ src_ids.update(extra)
169
+ study_ids = {s["study_id"] for s in studies}
170
+ findings = [f for f in snapshot["findings"] if f["study_id"] in study_ids]
171
+ audits = [a for a in snapshot["audits"] if a["study_id"] in study_ids]
172
+
173
+ lib_rev = self.active_revision()
174
+
175
+ # origin metadata per table: record library revision + entity id so the
176
+ def stamp(table: str, rec: dict) -> dict:
177
+ out = dict(rec)
178
+ ext = dict(out.get("extensions") or {})
179
+ ext["origin"] = {
180
+ "library_revision": lib_rev,
181
+ "library_entity_id": out[_TABLE_ID_KEY[table]],
182
+ "content_hash": hashlib.sha256(
183
+ json.dumps(out, sort_keys=True, separators=(",", ":"))
184
+ .encode("utf-8")).hexdigest(),
185
+ "imported_at": _now_iso(),
186
+ }
187
+ out["extensions"] = ext
188
+ return out
189
+ store = GraphStore.create(project)
190
+ existing_outcomes = {o["outcome_id"] for o in store.read_table("outcomes")}
191
+ outcomes: list[dict] = []
192
+ for f in findings:
193
+ oid = f["outcome_id"]
194
+ if oid in existing_outcomes:
195
+ continue
196
+ existing_outcomes.add(oid)
197
+ outcomes.append({
198
+ "outcome_id": oid,
199
+ "name": f.get("measure", oid),
200
+ "outcome_type": (f.get("extensions") or {}).get(
201
+ "outcome_type", "learning"),
202
+ "extensions": {
203
+ "auto_created_from_library_import": True,
204
+ "library_revision": lib_rev,
205
+ },
206
+ })
207
+ mutation = GraphMutation(
208
+ upserts={
209
+ "sources": [stamp("sources", s) for s in sources],
210
+ "studies": [stamp("studies", s) for s in studies],
211
+ "findings": [stamp("findings", f) for f in findings],
212
+ "audits": [stamp("audits", a) for a in audits],
213
+ "outcomes": outcomes,
214
+ },
215
+ retire_ids={},
216
+ )
217
+ return store.commit(run_id=run_id, reason="library snapshot import", mutation=mutation)
218
+
219
+ def diff_project_snapshot(self, *, project, source_ids: list[str]) -> dict:
220
+ """Diff imported facts vs the current library revision.
221
+
222
+ Compares every library entity reachable from the selected source_ids
223
+ (sources/studies/findings/audits) against the Project graph's copy
224
+ (ignoring import origin metadata). Returns added/changed/removed
225
+ entity ids so the caller can decide whether an explicit sync is
226
+ warranted.
227
+ """
228
+ snapshot = self._read_snapshot(self.active_revision())
229
+ src_ids = set(source_ids)
230
+ lib_sources = {s["source_id"]: s for s in snapshot["sources"] if s["source_id"] in src_ids}
231
+ lib_studies = {s["study_id"]: s for s in snapshot["studies"]
232
+ if set(s["source_ids"]) & src_ids}
233
+ lib_findings = {f["finding_id"]: f for f in snapshot["findings"]
234
+ if f["study_id"] in lib_studies}
235
+ lib_audits = {a["audit_id"]: a for a in snapshot["audits"]
236
+ if a["study_id"] in lib_studies}
237
+ lib_entities: dict[str, tuple[str, dict]] = {}
238
+ for table, ents in (("sources", lib_sources), ("studies", lib_studies),
239
+ ("findings", lib_findings), ("audits", lib_audits)):
240
+ for eid, rec in ents.items():
241
+ lib_entities[eid] = (table, rec)
242
+
243
+ store = GraphStore.create(project)
244
+ proj_by_table = {
245
+ t: {r[_TABLE_ID_KEY[t]]: r for r in store.read_table(t)}
246
+ for t in LIBRARY_TABLES
247
+ }
248
+ diff: dict = {"added": [], "changed": [], "removed": []}
249
+ for eid, (table, lib_rec) in lib_entities.items():
250
+ proj_rec = proj_by_table[table].get(eid)
251
+ if proj_rec is None:
252
+ diff["added"].append(eid)
253
+ continue
254
+ lib_content = {k: v for k, v in lib_rec.items() if k != "extensions"}
255
+ proj_content = {k: v for k, v in proj_rec.items() if k != "extensions"}
256
+ if lib_content != proj_content:
257
+ diff["changed"].append(eid)
258
+ for table in LIBRARY_TABLES:
259
+ for eid, rec in proj_by_table[table].items():
260
+ if eid in lib_entities:
261
+ continue
262
+ # only entities imported FROM this library may be reported
263
+ # removed; project-local entities are never "removed" by a
264
+ # library diff
265
+ origin = ((rec.get("extensions") or {}).get("origin") or {})
266
+ if origin.get("library_revision") is not None:
267
+ diff["removed"].append(eid)
268
+ return diff
@@ -0,0 +1,301 @@
1
+ """Builtin evidence library + offline preliminary verdict (v4).
2
+
3
+ Two entry points:
4
+
5
+ load_builtin_library() -> read benchmarks/evidence-library.json, validate
6
+ structurally, cache (lru_cache), return a deep
7
+ copy. Raises FileNotFoundError when the library
8
+ has not been built yet and ValueError when the
9
+ shape is invalid.
10
+
11
+ preliminary_verdict(question, *, top_k=10)
12
+ -> deterministic offline screening verdict. Matching
13
+ is Chinese-keyword / outcome-token based using
14
+ CJK bigram overlap (self-implemented; mirrors the
15
+ logic of scripts/benchmark_evaluator.py without
16
+ importing it): the question is tokenized into CJK
17
+ bigrams + English words, each library entry is
18
+ scored by bigram overlap over
19
+ claim_text + effect_summary + title, and the
20
+ top_k entries above MATCH_THRESHOLD with at least
21
+ MIN_SHARED_BIGRAMS shared tokens count as matched.
22
+
23
+ Conservative verdict rules (offline preliminary gate):
24
+ any matched contradict entry -> reject
25
+ else any matched support entry -> pilot
26
+ else -> insufficient_evidence
27
+ adopt is NEVER returned by the preliminary gate.
28
+
29
+ Output:
30
+ {"verdict": ..., "coverage": {"matched_entries": [...],
31
+ "matched_outcome_tokens": [...], "note": "..."},
32
+ "preliminary": True, "library_version": ...}
33
+
34
+ This module is stdlib-only (consistent with engine/ "Native Core" policy).
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import json
39
+ import re
40
+ from copy import deepcopy
41
+ from functools import lru_cache
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ ROOT = Path(__file__).resolve().parent.parent
46
+ def _resolve_library_path() -> Path:
47
+ """Repository layout first; wheel-installed share/ layout as fallback."""
48
+ repo = ROOT / "benchmarks" / "evidence-library.json"
49
+ if repo.is_file():
50
+ return repo
51
+ import sys
52
+ share = Path(sys.prefix) / "share" / "eduevidence" / "benchmarks" / "evidence-library.json"
53
+ if share.is_file():
54
+ return share
55
+ return repo
56
+
57
+
58
+ LIBRARY_PATH = _resolve_library_path()
59
+
60
+ # --- matching knobs (conservative) ---
61
+ MATCH_THRESHOLD = 0.30 # min bigram-overlap ratio (intersection / min sizes)
62
+ MIN_SHARED_BIGRAMS = 2 # min absolute shared tokens (blocks tiny-query over-match)
63
+
64
+ VERDICT_ORDER = ("adopt", "pilot", "reject", "insufficient_evidence")
65
+ DIRECTIONS = ("support", "contradict", "neutral")
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # tokenization (self-implemented, mirrors scripts/benchmark_evaluator.py)
69
+ # ---------------------------------------------------------------------------
70
+ _ID_RE = re.compile(r"\b([A-Za-z][A-Za-z-]{1,40})\b")
71
+
72
+
73
+ def _cjk_bigrams(text: str) -> set[str]:
74
+ chars = [c for c in text if "\u4e00" <= c <= "\u9fff"]
75
+ return {chars[i] + chars[i + 1] for i in range(len(chars) - 1)}
76
+
77
+
78
+ def _words(text: str) -> set[str]:
79
+ return {w.lower() for w in _ID_RE.findall(text) if len(w) > 2}
80
+
81
+
82
+ def _tokenize(text: str) -> set[str]:
83
+ return _cjk_bigrams(text) | _words(text)
84
+
85
+
86
+ def _overlap_tokens(ta: set[str], tb: set[str]) -> float:
87
+ if not ta or not tb:
88
+ return 0.0
89
+ return len(ta & tb) / max(1, min(len(ta), len(tb)))
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # outcome-token detection (Chinese terms + direct English taxonomy words)
94
+ # ---------------------------------------------------------------------------
95
+ _OUTCOME_TAXONOMY = {
96
+ "accuracy", "assignment_score", "retention", "transfer",
97
+ "independent_problem_solving", "completion_time", "cognitive_load",
98
+ "knowledge_gain", "concept_understanding", "engagement", "motivation",
99
+ "metacognition", "help_seeking", "code_quality", "ai_dependency",
100
+ "over_reliance", "reduced_effort", "reduced_transfer",
101
+ "academic_integrity_risk", "false_confidence",
102
+ }
103
+
104
+ _CN_OUTCOME_TERMS = {
105
+ "正确率": "accuracy", "准确率": "accuracy", "正确性": "accuracy",
106
+ "期末考试成绩": "assignment_score", "考试成绩": "assignment_score",
107
+ "作业得分": "assignment_score", "作业成绩": "assignment_score",
108
+ "成绩": "assignment_score", "得分": "assignment_score",
109
+ "记忆保持": "retention", "保持率": "retention", "保持": "retention",
110
+ "保留": "retention", "记忆": "retention",
111
+ "迁移能力": "transfer", "迁移": "transfer",
112
+ "独立问题解决": "independent_problem_solving",
113
+ "独立写作": "independent_problem_solving", "独立编程": "independent_problem_solving",
114
+ "独立解题": "independent_problem_solving", "脱离工具": "independent_problem_solving",
115
+ "无AI情境": "independent_problem_solving", "独立": "independent_problem_solving",
116
+ "任务完成时间": "completion_time", "完成时间": "completion_time",
117
+ "速度": "completion_time",
118
+ "认知负荷": "cognitive_load", "负荷": "cognitive_load",
119
+ "知识获得": "knowledge_gain", "知识": "knowledge_gain",
120
+ "概念理解": "concept_understanding", "概念": "concept_understanding",
121
+ "参与度": "engagement", "参与": "engagement", "投入": "engagement",
122
+ "学习动机": "motivation", "动机": "motivation", "兴趣": "motivation",
123
+ "元认知": "metacognition",
124
+ "求助行为": "help_seeking", "求助": "help_seeking",
125
+ "代码质量": "code_quality",
126
+ "过度依赖": "over_reliance", "AI依赖": "ai_dependency",
127
+ "依赖": "ai_dependency",
128
+ "减少努力": "reduced_effort", "努力": "reduced_effort",
129
+ "迁移受损": "reduced_transfer", "迁移下降": "reduced_transfer",
130
+ "学术诚信": "academic_integrity_risk", "诚信": "academic_integrity_risk",
131
+ "作弊": "academic_integrity_risk", "原创性": "academic_integrity_risk",
132
+ "虚假自信": "false_confidence",
133
+ }
134
+ # longest phrase first so "保持率" wins over "保持", "期末考试成绩" over "成绩", ...
135
+ _CN_OUTCOME_ORDERED = sorted(_CN_OUTCOME_TERMS.items(), key=lambda kv: -len(kv[0]))
136
+
137
+
138
+ def _detect_outcome_tokens(question: str) -> set[str]:
139
+ tokens: set[str] = set()
140
+ for phrase, outcome in _CN_OUTCOME_ORDERED:
141
+ if phrase in question:
142
+ tokens.add(outcome)
143
+ tokens |= {w for w in _words(question) if w in _OUTCOME_TAXONOMY}
144
+ return tokens
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # library loading: validate + cache
149
+ # ---------------------------------------------------------------------------
150
+ _TOP_REQUIRED = {"library_id", "version", "generated_at", "entries", "coverage_note"}
151
+ _ENTRY_REQUIRED = {
152
+ "entry_id", "source_id", "title", "year", "outcome_token", "direction",
153
+ "study_type", "claim_text", "effect_summary", "confidence_markers", "domains",
154
+ }
155
+
156
+
157
+ def _validate_library_shape(lib: dict[str, Any]) -> None:
158
+ """Structural validation of the builtin library (stdlib-only).
159
+
160
+ Keeps the engine dependency-free; the full JSON-Schema check lives in the
161
+ build script (scripts/build_evidence_library.py -> validate_schema.Validator).
162
+ """
163
+ if not isinstance(lib, dict):
164
+ raise ValueError("builtin library must be a JSON object")
165
+ missing = _TOP_REQUIRED - lib.keys()
166
+ if missing:
167
+ raise ValueError(f"builtin library missing required fields: {sorted(missing)}")
168
+ entries = lib.get("entries")
169
+ if not isinstance(entries, list) or not entries:
170
+ raise ValueError("builtin library entries must be a non-empty list")
171
+ for idx, entry in enumerate(entries):
172
+ if not isinstance(entry, dict):
173
+ raise ValueError(f"entry[{idx}] must be an object")
174
+ miss = _ENTRY_REQUIRED - entry.keys()
175
+ if miss:
176
+ raise ValueError(f"entry[{idx}] missing required fields: {sorted(miss)}")
177
+ if entry["direction"] not in DIRECTIONS:
178
+ raise ValueError(
179
+ f"entry[{idx}] direction {entry['direction']!r} not in {sorted(DIRECTIONS)}"
180
+ )
181
+ for field in ("entry_id", "source_id", "title", "outcome_token",
182
+ "claim_text", "effect_summary", "study_type"):
183
+ if not isinstance(entry[field], str) or not entry[field].strip():
184
+ raise ValueError(f"entry[{idx}] {field!r} must be a non-empty string")
185
+ if entry["year"] is not None and (
186
+ not isinstance(entry["year"], int) or isinstance(entry["year"], bool)
187
+ ):
188
+ raise ValueError(f"entry[{idx}] year must be integer or null")
189
+ if not isinstance(entry["confidence_markers"], list):
190
+ raise ValueError(f"entry[{idx}] confidence_markers must be a list")
191
+ if not isinstance(entry["domains"], list) or not entry["domains"]:
192
+ raise ValueError(f"entry[{idx}] domains must be a non-empty list")
193
+
194
+
195
+ @lru_cache(maxsize=1)
196
+ def _read_library() -> dict[str, Any]:
197
+ if not LIBRARY_PATH.is_file():
198
+ raise FileNotFoundError(
199
+ f"builtin evidence library not found: {LIBRARY_PATH}; "
200
+ "run 'python scripts/build_evidence_library.py' first"
201
+ )
202
+ lib = json.loads(LIBRARY_PATH.read_text(encoding="utf-8"))
203
+ _validate_library_shape(lib)
204
+ return lib
205
+
206
+
207
+ def load_builtin_library() -> dict[str, Any]:
208
+ """Load (validated + cached) builtin library; returns a deep copy."""
209
+ return deepcopy(_read_library())
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # preliminary verdict
214
+ # ---------------------------------------------------------------------------
215
+ def _entry_text(entry: dict[str, Any]) -> str:
216
+ return " ".join(
217
+ str(entry.get(k) or "")
218
+ for k in ("claim_text", "effect_summary", "title")
219
+ )
220
+
221
+
222
+ def preliminary_verdict(question: str, *, top_k: int = 10) -> dict[str, Any]:
223
+ """Offline conservative preliminary verdict for a (Chinese) education question.
224
+
225
+ Matching: CJK bigram overlap between the question and each entry's
226
+ claim_text + effect_summary + title; the top_k entries are considered and an
227
+ entry counts as matched when overlap >= MATCH_THRESHOLD and it shares at
228
+ least MIN_SHARED_BIGRAMS tokens. Verdict: contradict => reject, else
229
+ support => pilot, else insufficient_evidence. Never adopt. Never crashes on
230
+ empty/blank questions.
231
+ """
232
+ try:
233
+ top_k = int(top_k)
234
+ except (TypeError, ValueError):
235
+ top_k = 10
236
+ lib = _read_library()
237
+ question = (question or "").strip()
238
+ q_tokens = _tokenize(question)
239
+ q_outcomes = _detect_outcome_tokens(question)
240
+
241
+ scored: list[tuple[float, int, bool, dict[str, Any]]] = []
242
+ for entry in lib["entries"]:
243
+ e_tokens = _tokenize(_entry_text(entry))
244
+ base = _overlap_tokens(q_tokens, e_tokens)
245
+ shared = len(q_tokens & e_tokens)
246
+ # multi-outcome entries match when ANY of their tokens is in the question
247
+ entry_tokens = set(entry.get("outcome_tokens") or [entry.get("outcome_token")])
248
+ has_outcome = bool(entry_tokens & q_outcomes)
249
+ scored.append((base, shared, has_outcome, entry))
250
+
251
+ scored.sort(key=lambda t: (-t[0], -t[1], -int(t[2]), t[3].get("entry_id", "")))
252
+ top = scored[: max(0, int(top_k))]
253
+
254
+ matched = [
255
+ entry
256
+ for base, shared, _has_outcome, entry in top
257
+ if base >= MATCH_THRESHOLD and shared >= MIN_SHARED_BIGRAMS
258
+ ]
259
+
260
+ directions = {entry["direction"] for entry in matched}
261
+ if "contradict" in directions:
262
+ verdict = "reject"
263
+ elif "support" in directions:
264
+ verdict = "pilot"
265
+ else:
266
+ verdict = "insufficient_evidence"
267
+
268
+ note = _build_note(matched, directions, q_outcomes, top_k)
269
+ return {
270
+ "verdict": verdict,
271
+ "coverage": {
272
+ "matched_entries": [entry["entry_id"] for entry in matched],
273
+ "matched_outcome_tokens": sorted(q_outcomes),
274
+ "note": note,
275
+ },
276
+ "preliminary": True,
277
+ "library_version": lib.get("version", ""),
278
+ }
279
+
280
+
281
+ def _build_note(
282
+ matched: list[dict[str, Any]],
283
+ directions: set[str],
284
+ q_outcomes: set[str],
285
+ top_k: int,
286
+ ) -> str:
287
+ outcome_str = "、".join(sorted(q_outcomes)) or "无"
288
+ if not matched:
289
+ return (
290
+ f"离线初步裁决未匹配到内置证据(阈值:bigram overlap≥{MATCH_THRESHOLD} 且"
291
+ f"共享 bigram≥{MIN_SHARED_BIGRAMS});检测到结局词:{outcome_str}。"
292
+ "建议进入在线证据检索流程进一步核实。"
293
+ )
294
+ counts = {d: sum(1 for e in matched if e["direction"] == d) for d in DIRECTIONS}
295
+ return (
296
+ f"离线初步裁决在 top_k={top_k} 内匹配到 {len(matched)} 条内置证据:"
297
+ f"support={counts['support']}、contradict={counts['contradict']}、"
298
+ f"neutral={counts['neutral']};匹配结局词:{outcome_str}。"
299
+ "本裁决为初步(preliminary=true)且保守,从不直接给出 adopt,"
300
+ "建议结合完整证据库与在线检索复核。"
301
+ )