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,234 @@
1
+ #!/usr/bin/env python3
2
+ """evidence_score.py — Deterministic quality scoring and confidence computation.
3
+
4
+ Two rule-based engines:
5
+
6
+ 1. Quality score (five dimensions, each 0-2, total 0-10):
7
+ D1 Study Design, D2 Sample Quality, D3 Measurement Validity,
8
+ D4 Temporal Strength, D5 Directness.
9
+ Level mapping (references/evidence-quality.md): 8-10 strong, 5-7 moderate,
10
+ 2-4 weak, 0-1 very_weak.
11
+
12
+ 2. Confidence (rule-based, NOT model-generated):
13
+ Evidence Quality + Consistency + Directness + Evidence Count
14
+ - Conflict Penalty - Unsupported Penalty
15
+ -> High | Moderate | Low | Insufficient
16
+ v2 policy (2026-08-12.v2, D-1/D-2): consistency is computed over
17
+ decision_relation (support_adoption/oppose_adoption/conditional/neutral)
18
+ instead of relation_to_claim, and the evidence count term is
19
+ min(1.0, independent_studies / 4) — independent_samples is reported
20
+ separately instead of being added to the count term.
21
+
22
+ The authoritative pipeline entry for final verdicts is
23
+ scripts/compute_confidence.py (independent-studies/samples-weighted formula
24
+ with confidence_policy_version); the `confidence()` function below remains the
25
+ backward-compatible engine used by example reproducibility checks.
26
+
27
+ IMPORTANT: the confidence score is a rule-based index in [0, 1], NOT a
28
+ probability. Never present it as "85% confidence" or any probabilistic claim.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from typing import Any
33
+
34
+ from evidence_semantics import claim_relation, decision_relation
35
+
36
+ DIMENSIONS = ["D1_study_design", "D2_sample_quality", "D3_measurement_validity",
37
+ "D4_temporal_strength", "D5_directness"]
38
+
39
+ #: Version of the deterministic confidence policy (bump on any formula change).
40
+ CONFIDENCE_POLICY_VERSION = "2026-08-12.v2"
41
+
42
+
43
+ def quality_score(dimensions: dict[str, int]) -> float:
44
+ """Sum the five 0-2 dimension scores into a 0-10 quality score."""
45
+ total = 0.0
46
+ for dim in DIMENSIONS:
47
+ val = dimensions.get(dim, 0)
48
+ if not isinstance(val, (int, float)) or isinstance(val, bool):
49
+ raise ValueError(f"dimension {dim} must be numeric, got {val!r}")
50
+ total += max(0.0, min(2.0, float(val)))
51
+ return total
52
+
53
+
54
+ def quality_level(score: float) -> str:
55
+ """Map a 0-10 quality score to a level (references/evidence-quality.md)."""
56
+ if score >= 8:
57
+ return "strong"
58
+ if score >= 5:
59
+ return "moderate"
60
+ if score >= 2:
61
+ return "weak"
62
+ return "very_weak"
63
+
64
+
65
+ def direction_of(evidence: dict[str, Any]) -> str:
66
+ """Legacy wrapper for the claim relation of one evidence object.
67
+
68
+ Delegates to ``evidence_semantics.claim_relation`` so the interpretation
69
+ of ``relation_to_claim`` / legacy ``direction`` stays centralized (A-1).
70
+ Kept for backward-compatible callers; new code should use
71
+ ``evidence_semantics.claim_relation`` directly.
72
+ """
73
+ return claim_relation(evidence)
74
+
75
+
76
+ def independent_studies(evidence_list: list[dict[str, Any]]) -> int:
77
+ """Number of distinct studies behind the evidence.
78
+
79
+ Counts unique non-empty ``study_id`` values; falls back to unique
80
+ ``source_id`` values when ``study_id`` is absent.
81
+ """
82
+ ids = [e.get("study_id") or e.get("source_id") for e in evidence_list]
83
+ return len({i for i in ids if i})
84
+
85
+
86
+ def independent_samples(evidence_list: list[dict[str, Any]]) -> int:
87
+ """Number of distinct samples behind the evidence.
88
+
89
+ Counts unique non-empty ``sample_id`` values. When ``sample_id`` is missing
90
+ entirely, falls back to study-level ids (each study contributes at least one
91
+ sample), so the count stays a deterministic lower-bound estimate.
92
+ """
93
+ ids = [e.get("sample_id") or e.get("study_id") or e.get("source_id") for e in evidence_list]
94
+ return len({i for i in ids if i})
95
+
96
+
97
+ def consistency_score(directions: list[str]) -> float:
98
+ """Consistency in [0,1]: proportion of non-neutral evidence agreeing with the majority direction."""
99
+ non_neutral = [d for d in directions if d in ("support", "contradict")]
100
+ if not non_neutral:
101
+ return 0.0
102
+ majority = max(non_neutral.count("support"), non_neutral.count("contradict"))
103
+ return majority / len(non_neutral)
104
+
105
+
106
+ def decision_consistency_score(relations: list[str]) -> float:
107
+ """Decision-level consistency in [0,1] (D-2).
108
+
109
+ Computed over ``decision_relation`` values instead of ``relation_to_claim``:
110
+ claim-level evidence is usually extracted precisely because it supports its
111
+ claim, so relation-level consistency overstates agreement about the final
112
+ teaching decision. Decisive relations are support_adoption (for) and
113
+ oppose_adoption (against); conditional and neutral are non-committal.
114
+ """
115
+ decisive = [r for r in relations if r in ("support_adoption", "oppose_adoption")]
116
+ if not decisive:
117
+ return 0.0
118
+ majority = max(decisive.count("support_adoption"), decisive.count("oppose_adoption"))
119
+ return majority / len(decisive)
120
+
121
+
122
+ def directness_score(evidence_list: list[dict[str, Any]]) -> float:
123
+ """Average D5 Directness (0-2) across evidence; 0 if empty."""
124
+ if not evidence_list:
125
+ return 0.0
126
+ values = [e.get("quality_dimensions", {}).get("D5_directness", 0) for e in evidence_list]
127
+ return sum(float(v) for v in values) / len(values)
128
+
129
+
130
+ def confidence(evidence_list: list[dict[str, Any]], *, target_outcome: str | None = None) -> dict[str, Any]:
131
+ """Rule-based confidence computation per plan section 13.
132
+
133
+ Normalized implementation (weights sum to 1.0 so the score stays in [0, 1]):
134
+
135
+ score = 0.30*Evidence Quality + 0.25*Consistency + 0.20*Directness
136
+ + 0.25*Evidence Count - Conflict Penalty - Unsupported Penalty
137
+
138
+ where Evidence Count = min(1.0, independent_studies / 4) (D-1),
139
+ Conflict Penalty = 0.15 if any evidence opposes adoption
140
+ (decision_relation == oppose_adoption, D-2), and
141
+ Unsupported Penalty = min(0.20, 0.05 * n_unsupported). The raw quality /
142
+ consistency / directness terms are scaled to [0, 1] first.
143
+
144
+ Returns breakdown dict plus final label (High | Moderate | Low | Insufficient).
145
+ """
146
+ if not evidence_list:
147
+ return {"confidence": "Insufficient",
148
+ "confidence_policy_version": CONFIDENCE_POLICY_VERSION,
149
+ "confidence_breakdown": {"evidence_count": 0}}
150
+
151
+ # 1. Evidence Quality (mean of quality scores, scaled to 0-1 range)
152
+ quality_values = [e.get("quality_score") for e in evidence_list]
153
+ numeric = [q for q in quality_values if isinstance(q, (int, float))]
154
+ avg_quality = sum(numeric) / len(numeric) if numeric else 0.0
155
+ quality_term = avg_quality / 10.0 # 0-1
156
+
157
+ # 2. Consistency (decision_relation based, D-2): claim-level evidence is
158
+ # usually extracted to support its claim, so relation_to_claim would
159
+ # overstate agreement about the final teaching decision.
160
+ decisions = [decision_relation(e) for e in evidence_list]
161
+ consistency = decision_consistency_score(decisions)
162
+
163
+ # 3. Directness (0-2 -> 0-1)
164
+ directness = directness_score(evidence_list) / 2.0
165
+
166
+ # 4. Evidence count (capped at 1) — 独立研究计权(D-1):count term 只用
167
+ # independent_studies,不再与 independent_samples 相加(一个普通研究
168
+ # 通常 1 study + 1 sample,相加会让同一研究贡献两次)。independent_samples
169
+ # 单独展示在 Provenance / Evidence Summary。
170
+ n_studies = independent_studies(evidence_list)
171
+ n_samples = independent_samples(evidence_list)
172
+ count_term = min(1.0, n_studies / 4.0)
173
+
174
+ # 5. Conflict penalty (0.15 when any evidence opposes adoption)
175
+ conflict_penalty = 0.15 if "oppose_adoption" in decisions else 0.0
176
+
177
+ # 6. Unsupported penalty (capped at 0.20)
178
+ unsupported = [e for e in evidence_list if e.get("status") in ("UNSUPPORTED", "DOWNGRADE_CONFIDENCE")]
179
+ unsupported_penalty = min(0.20, len(unsupported) * 0.05)
180
+
181
+ score = (0.30 * quality_term + 0.25 * consistency + 0.20 * directness
182
+ + 0.25 * count_term - conflict_penalty - unsupported_penalty)
183
+ score = max(0.0, min(1.0, score))
184
+
185
+ if target_outcome:
186
+ relevant = [e for e in evidence_list if e.get("outcome_type") == target_outcome]
187
+ if relevant:
188
+ score *= 0.5 + 0.5 * (len(relevant) / max(1, len(evidence_list)))
189
+
190
+ label = _confidence_label(score)
191
+ return {
192
+ "confidence": label,
193
+ "confidence_score": round(score, 3),
194
+ "confidence_policy_version": CONFIDENCE_POLICY_VERSION,
195
+ "independent_studies": n_studies,
196
+ "independent_samples": n_samples,
197
+ "confidence_breakdown": {
198
+ "score": round(score, 3),
199
+ "evidence_quality": round(quality_term, 3),
200
+ "consistency": round(consistency, 3),
201
+ "directness": round(directness, 3),
202
+ "evidence_count": len(evidence_list),
203
+ "independent_studies": n_studies,
204
+ "independent_samples": n_samples,
205
+ "count_term": round(count_term, 3),
206
+ "conflict_penalty": conflict_penalty,
207
+ "unsupported_penalty": round(unsupported_penalty, 3),
208
+ },
209
+ }
210
+
211
+
212
+ def _confidence_label(score: float) -> str:
213
+ if score >= 0.72:
214
+ return "High"
215
+ if score >= 0.45:
216
+ return "Moderate"
217
+ if score >= 0.2:
218
+ return "Low"
219
+ return "Insufficient"
220
+
221
+
222
+ if __name__ == "__main__":
223
+ import json
224
+ import sys
225
+
226
+ if len(sys.argv) != 2:
227
+ print("usage: evidence_score.py <evidence.jsonl>", file=sys.stderr)
228
+ sys.exit(2)
229
+ with open(sys.argv[1], encoding="utf-8") as fh:
230
+ evs = [json.loads(line) for line in fh if line.strip()]
231
+ for ev in evs:
232
+ if ev.get("quality_dimensions") and ev.get("quality_score") is None:
233
+ ev["quality_score"] = quality_score(ev["quality_dimensions"])
234
+ print(json.dumps(confidence(evs), ensure_ascii=False, indent=2))
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """evidence_semantics.py — Centralized Evidence semantic helpers (A-1).
3
+
4
+ The three evidence "direction" semantics are deliberately distinct; every
5
+ consumer must go through these helpers instead of re-interpreting the raw
6
+ fields, so the semantics cannot drift across files again:
7
+
8
+ claim_relation(ev) support | contradict | neutral
9
+ Does this evidence support the specific claim it is bound to?
10
+ (field: relation_to_claim; legacy fallback: direction)
11
+
12
+ effect_direction(ev) positive | negative | null
13
+ What did the study observe? (field: effect_direction)
14
+
15
+ decision_relation(ev) support_adoption | oppose_adoption | conditional | neutral
16
+ What does this evidence mean for the final teaching decision?
17
+ (field: decision_relation; deterministic fallback derived from the
18
+ claim relation when the field is absent)
19
+
20
+ Consumers:
21
+
22
+ Claim Audit -> claim_relation
23
+ Outcome Visualization -> effect_direction
24
+ Cross-study Consistency -> decision_relation
25
+
26
+ V2 note: the Research Engine's V2 semantics live in `engine/semantics.py`
27
+ (finding_effect / claim_relation / decision_implication) and operate on the
28
+ versioned graph entities. This module keeps its legacy flat-evidence API for
29
+ V1 callers; the two layers are structurally different and must not be mixed.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ from typing import Any
34
+
35
+ CLAIM_RELATIONS = ("support", "contradict", "neutral")
36
+ EFFECT_DIRECTIONS = ("positive", "negative", "null")
37
+ DECISION_RELATIONS = ("support_adoption", "oppose_adoption", "conditional", "neutral")
38
+
39
+ _CLAIM_TO_DECISION = {
40
+ "support": "support_adoption",
41
+ "contradict": "oppose_adoption",
42
+ "neutral": "neutral",
43
+ }
44
+
45
+
46
+ def claim_relation(evidence: dict[str, Any]) -> str:
47
+ """Relation of one evidence object to the claim it is bound to.
48
+
49
+ Reads the new-contract field ``relation_to_claim`` (support | contradict |
50
+ neutral); falls back to the legacy ``direction`` field for backward
51
+ compatibility. Returns 'neutral' when neither is present/valid.
52
+ """
53
+ relation = evidence.get("relation_to_claim")
54
+ if relation in CLAIM_RELATIONS:
55
+ return relation
56
+ legacy = evidence.get("direction")
57
+ if legacy in CLAIM_RELATIONS:
58
+ return legacy
59
+ return "neutral"
60
+
61
+
62
+ def effect_direction(evidence: dict[str, Any]) -> str:
63
+ """Observed effect direction of one evidence object.
64
+
65
+ Reads ``effect_direction`` (positive | negative | null). Returns 'null'
66
+ when the field is absent or invalid — a missing measurement is treated as
67
+ no measurable effect, never as an invented one.
68
+ """
69
+ effect = evidence.get("effect_direction")
70
+ if effect in EFFECT_DIRECTIONS:
71
+ return effect
72
+ return "null"
73
+
74
+
75
+ def decision_relation(evidence: dict[str, Any]) -> str:
76
+ """Relation of one evidence object to the final teaching decision.
77
+
78
+ Reads ``decision_relation`` (support_adoption | oppose_adoption |
79
+ conditional | neutral). When the field is absent, derives a deterministic
80
+ fallback from the claim relation (support -> support_adoption,
81
+ contradict -> oppose_adoption, neutral -> neutral) so legacy data keeps
82
+ working without silent semantic drift.
83
+ """
84
+ decision = evidence.get("decision_relation")
85
+ if decision in DECISION_RELATIONS:
86
+ return decision
87
+ return _CLAIM_TO_DECISION[claim_relation(evidence)]
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ """fetch_benchmark.py — Fetch Reliability Benchmark (Smart Web Fetch v3 §19).
3
+
4
+ Compares built-in fetch vs Smart Web Fetch over benchmarks/fetch/urls.jsonl
5
+ (30 URLs covering paper landing / Chinese web / university sites / dynamic
6
+ pages / failure simulations).
7
+
8
+ Metrics:
9
+ Fetch Success Rate FETCH_VALID+PARTIAL proportion
10
+ Useful Content Recall content captured for expected-title URLs
11
+ Noise Ratio 1 - clean_size/raw_size average
12
+ Latency seconds per fetch
13
+ Fallback Rate proportion of attempts that used a fallback provider
14
+
15
+ Offline mode (--dry-run): uses a deterministic local simulator so the
16
+ benchmark runs without network — useful for CI and reproducibility. Live mode
17
+ (--live) actually hits the network.
18
+
19
+ Usage:
20
+ python3 scripts/fetch_benchmark.py --urls benchmarks/fetch/urls.jsonl --dry-run
21
+ python3 scripts/fetch_benchmark.py --urls benchmarks/fetch/urls.jsonl --live --out benchmarks/fetch/results/summary.json
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import random
28
+ import sys
29
+ import time
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+
34
+ def _simulate(url: str, category: str, expect_title: str, seed: int = 42) -> dict[str, Any]:
35
+ """Deterministic offline simulator mirroring real fetch outcomes."""
36
+ rng = random.Random(seed + hash(url) % 100000)
37
+
38
+ # service failure simulations
39
+ if category == "service_failure_sim":
40
+ return {"url": url, "fetch_status": "FETCH_FAILED", "fetch_provider": "builtin",
41
+ "latency_s": 0.05, "fallback_used": False, "fallback_chain": ["builtin:error"],
42
+ "raw_size": 0, "clean_size": 0, "compression_ratio": 0.0}
43
+
44
+ # login-walled / dynamic pages degrade to PARTIAL at best
45
+ if category in ("dynamic_page", "complex_nav"):
46
+ status = "FETCH_PARTIAL" if rng.random() < 0.6 else "FETCH_FAILED"
47
+ provider = "jina_reader" if status == "FETCH_PARTIAL" else "builtin"
48
+ return {"url": url, "fetch_status": status, "fetch_provider": provider,
49
+ "latency_s": round(0.3 + rng.random() * 0.5, 2), "fallback_used": status == "FETCH_PARTIAL",
50
+ "fallback_chain": ["builtin:error", "jina_reader:ok"] if status == "FETCH_PARTIAL" else ["builtin:error"],
51
+ "raw_size": 80_000, "clean_size": 6_000 if status == "FETCH_PARTIAL" else 0,
52
+ "compression_ratio": 0.9 if status == "FETCH_PARTIAL" else 0.0}
53
+
54
+ # short page
55
+ if category == "short_page":
56
+ return {"url": url, "fetch_status": "FETCH_PARTIAL", "fetch_provider": "builtin",
57
+ "latency_s": 0.1, "fallback_used": False, "fallback_chain": ["builtin"],
58
+ "raw_size": 1_200, "clean_size": 120, "compression_ratio": 0.9}
59
+
60
+ # paper landing / journal / institution: reliable
61
+ if category in ("paper_landing", "journal_article", "institution_org", "government_org",
62
+ "international_org", "report", "dataset_page", "wiki", "long_page",
63
+ "professional_institution"):
64
+ status = "FETCH_VALID" if rng.random() < 0.85 else "FETCH_PARTIAL"
65
+ provider = "builtin" if status == "FETCH_VALID" else "jina_reader"
66
+ clean = 18_000 if status == "FETCH_VALID" else 4_000
67
+ return {"url": url, "fetch_status": status, "fetch_provider": provider,
68
+ "latency_s": round(0.2 + rng.random() * 0.6, 2), "fallback_used": status == "FETCH_PARTIAL",
69
+ "fallback_chain": ["builtin"] if status == "FETCH_VALID" else ["builtin:error", "jina_reader:ok"],
70
+ "raw_size": 120_000, "clean_size": clean, "compression_ratio": 1 - clean / 120_000}
71
+
72
+ # university / chinese / news sites: mixed reliability
73
+ content_ok = rng.random() < 0.7
74
+ status = "FETCH_VALID" if content_ok else "FETCH_PARTIAL"
75
+ provider = "builtin" if status == "FETCH_VALID" else "markdown_new"
76
+ return {"url": url, "fetch_status": status, "fetch_provider": provider,
77
+ "latency_s": round(0.2 + rng.random() * 0.7, 2), "fallback_used": status == "FETCH_PARTIAL",
78
+ "fallback_chain": ["builtin"] if status == "FETCH_VALID" else ["builtin:error", "markdown_new:ok"],
79
+ "raw_size": 90_000, "clean_size": 10_000 if status == "FETCH_VALID" else 3_000,
80
+ "compression_ratio": 1 - (10_000 if status == "FETCH_VALID" else 3_000) / 90_000}
81
+
82
+
83
+ def _live_fetch(url: str, timeout: int = 20) -> dict[str, Any]:
84
+ """Live fetch through the Smart Web Fetch chain (network required)."""
85
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
86
+ from integrations.smart_web_fetch import smart_fetch
87
+
88
+ start = time.monotonic()
89
+ result = smart_fetch(url, timeout=timeout)
90
+ elapsed = time.monotonic() - start
91
+ return {
92
+ "url": url,
93
+ "fetch_status": result["fetch_status"],
94
+ "fetch_provider": result["fetch_provider"],
95
+ "latency_s": round(elapsed, 2),
96
+ "fallback_used": result["fallback_used"],
97
+ "fallback_chain": result["fallback_chain"],
98
+ "raw_size": result["raw_size"],
99
+ "clean_size": result["clean_size"],
100
+ "compression_ratio": result["compression_ratio"],
101
+ }
102
+
103
+
104
+ def run_benchmark(urls: list[dict[str, Any]], *, live: bool, timeout: int = 20) -> dict[str, Any]:
105
+ results: list[dict[str, Any]] = []
106
+ for row in urls:
107
+ if live:
108
+ res = _live_fetch(row["url"], timeout=timeout)
109
+ else:
110
+ res = _simulate(row["url"], row.get("category", ""), row.get("expect_title", ""))
111
+ res["id"] = row["id"]
112
+ res["category"] = row.get("category", "")
113
+ res["expect_title"] = row.get("expect_title", "")
114
+ results.append(res)
115
+
116
+ total = len(results)
117
+ valid = [r for r in results if r["fetch_status"] == "FETCH_VALID"]
118
+ partial = [r for r in results if r["fetch_status"] == "FETCH_PARTIAL"]
119
+ failed = [r for r in results if r["fetch_status"] == "FETCH_FAILED"]
120
+
121
+ latencies = [r["latency_s"] for r in results if r.get("latency_s")]
122
+ ratios = [r["compression_ratio"] for r in results if r.get("compression_ratio")]
123
+ fallback = [r for r in results if r.get("fallback_used")]
124
+
125
+ # Useful content recall: expected-title URLs that returned usable content
126
+ expect_title_urls = [r for r in results if r.get("expect_title")]
127
+ recall_hits = [r for r in expect_title_urls
128
+ if r["fetch_status"] in ("FETCH_VALID", "FETCH_PARTIAL")]
129
+ recall = len(recall_hits) / len(expect_title_urls) if expect_title_urls else 0.0
130
+
131
+ summary = {
132
+ "total_urls": total,
133
+ "fetch_success_rate": round(len(valid) / total, 3),
134
+ "partial_rate": round(len(partial) / total, 3),
135
+ "failure_rate": round(len(failed) / total, 3),
136
+ "useful_content_recall": round(recall, 3),
137
+ "avg_noise_ratio": round(sum(ratios) / len(ratios), 3) if ratios else 0.0,
138
+ "avg_latency_s": round(sum(latencies) / len(latencies), 3) if latencies else 0.0,
139
+ "fallback_rate": round(len(fallback) / total, 3),
140
+ "mode": "live" if live else "dry_run",
141
+ "results": results,
142
+ }
143
+ return summary
144
+
145
+
146
+ def main() -> int:
147
+ parser = argparse.ArgumentParser(description="EduEvidence Fetch Reliability Benchmark")
148
+ parser.add_argument("--urls", required=True, help="benchmarks/fetch/urls.jsonl")
149
+ parser.add_argument("--live", action="store_true", help="actually hit the network")
150
+ parser.add_argument("--dry-run", dest="dry_run", action="store_true",
151
+ help="explicitly run the offline simulator (default; mutually exclusive with --live)")
152
+ parser.add_argument("--timeout", type=int, default=20)
153
+ parser.add_argument("--out", help="optional JSON output path")
154
+ args = parser.parse_args()
155
+
156
+ if args.live and args.dry_run:
157
+ parser.error("--live and --dry-run are mutually exclusive")
158
+
159
+ urls = [json.loads(line) for line in Path(args.urls).read_text(encoding="utf-8").splitlines()
160
+ if line.strip()]
161
+ summary = run_benchmark(urls, live=args.live, timeout=args.timeout)
162
+
163
+ if args.out:
164
+ out_path = Path(args.out)
165
+ out_path.parent.mkdir(parents=True, exist_ok=True)
166
+ out_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
167
+ print(f"wrote {args.out}")
168
+
169
+ keys = ("total_urls", "fetch_success_rate", "partial_rate", "failure_rate",
170
+ "useful_content_recall", "avg_noise_ratio", "avg_latency_s", "fallback_rate", "mode")
171
+ for k in keys:
172
+ print(f"{k}: {summary[k]}")
173
+ return 0
174
+
175
+
176
+ if __name__ == "__main__":
177
+ sys.exit(main())
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """generate_metrics.py — Single source of truth for headline project metrics.
3
+
4
+ Docs must stop hard-coding counts that drift (plan R5). This script derives
5
+ the authoritative numbers from the repository itself and writes
6
+ docs/metrics.json. Run with --check (CI) to fail when a doc-facing metric
7
+ changed without regenerating this file.
8
+
9
+ Metrics:
10
+ - engine_version from engine/versions.py (the version authority)
11
+ - test_functions grep 'def test_' across tests/
12
+ - test_files number of collected test modules in tests/
13
+ - schema_count schemas/*.json at root + v2/ + v3/ + v4/
14
+ - reference_doc_count references/*.md
15
+ - gold_annotation_count benchmarks/annotations/gold-Q*.json
16
+ - example_packs examples/*/ directories shipping result.json
17
+
18
+ Usage:
19
+ python3 scripts/generate_metrics.py # regenerate docs/metrics.json
20
+ python3 scripts/generate_metrics.py --check # exit 1 if file is stale
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import re
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ REPO_ROOT = Path(__file__).resolve().parent.parent
32
+
33
+
34
+ def collect() -> dict:
35
+ versions_py = (REPO_ROOT / "engine" / "versions.py").read_text(encoding="utf-8")
36
+ m = re.search(r'^ENGINE_VERSION\s*=\s*"(\d+\.\d+\.\d+)"', versions_py, re.M)
37
+ engine_version = m.group(1) if m else "unknown"
38
+
39
+ test_functions = test_files = 0
40
+ tests_dir = REPO_ROOT / "tests"
41
+ if tests_dir.is_dir():
42
+ for path in tests_dir.glob("test_*.py"):
43
+ test_files += 1
44
+ test_functions += len(re.findall(r"^\s*def test_", path.read_text(encoding="utf-8"), re.M))
45
+
46
+ schema_count = 0
47
+ schemas_dir = REPO_ROOT / "schemas"
48
+ if schemas_dir.is_dir():
49
+ schema_count = len(list(schemas_dir.rglob("*.json")))
50
+
51
+ reference_doc_count = len(list((REPO_ROOT / "references").glob("*.md"))) \
52
+ if (REPO_ROOT / "references").is_dir() else 0
53
+
54
+ gold_annotation_count = len(list((REPO_ROOT / "benchmarks" / "annotations").glob("gold-Q*.json"))) \
55
+ if (REPO_ROOT / "benchmarks" / "annotations").is_dir() else 0
56
+
57
+ example_packs = sorted(
58
+ p.name for p in (REPO_ROOT / "examples").iterdir()
59
+ if p.is_dir() and (p / "result.json").exists()
60
+ ) if (REPO_ROOT / "examples").is_dir() else []
61
+
62
+ return {
63
+ "engine_version": engine_version,
64
+ "test_functions": test_functions,
65
+ "test_files": test_files,
66
+ "schema_count": schema_count,
67
+ "reference_doc_count": reference_doc_count,
68
+ "gold_annotation_count": gold_annotation_count,
69
+ "example_packs": example_packs,
70
+ }
71
+
72
+
73
+ def main() -> int:
74
+ parser = argparse.ArgumentParser(description=__doc__)
75
+ parser.add_argument("--check", action="store_true",
76
+ help="fail if docs/metrics.json is stale")
77
+ args = parser.parse_args()
78
+
79
+ metrics = collect()
80
+ out_path = REPO_ROOT / "docs" / "metrics.json"
81
+ current = json.loads(out_path.read_text(encoding="utf-8")) if out_path.exists() else None
82
+
83
+ if args.check:
84
+ if current != metrics:
85
+ print("FAIL: docs/metrics.json is stale; rerun scripts/generate_metrics.py")
86
+ print(f"expected: {json.dumps(metrics, ensure_ascii=False)}")
87
+ print(f"found: {json.dumps(current, ensure_ascii=False)}")
88
+ return 1
89
+ print(f"metrics OK ({metrics['engine_version']}, "
90
+ f"{metrics['test_functions']} tests, {metrics['schema_count']} schemas)")
91
+ return 0
92
+
93
+ out_path.write_text(json.dumps(metrics, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
94
+ print(json.dumps(metrics, ensure_ascii=False, indent=2))
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ sys.exit(main())