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.
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- 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())
|