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,571 @@
|
|
|
1
|
+
"""engine/evidence_graph.py — Unified SSOT Evidence Graph for EduEvidence.
|
|
2
|
+
|
|
3
|
+
Provides a unified, strongly-typed graph data model holding all 7 core node entities:
|
|
4
|
+
1. PaperNode (Sources & Bibliographic records)
|
|
5
|
+
2. EvidenceNode (Empirical findings, Hedges' g effect sizes, sample sizes, WWC ratings)
|
|
6
|
+
3. OutcomeNode (Ontology-mapped outcome dimensions)
|
|
7
|
+
4. ClaimNode (Hypothesized assertions & pooled status)
|
|
8
|
+
5. RiskNode (Methodological & social science pitfalls, e.g. Scaffolding Dependency)
|
|
9
|
+
6. GapNode (Identified research gaps & future trial recommendations)
|
|
10
|
+
7. DecisionNode (Adjudicated four-state verdict, confidence, boundaries & protocols)
|
|
11
|
+
|
|
12
|
+
All downstream modules (Tribunal, GapLens, Meta-Analysis, Visual Reports, Web Dashboard)
|
|
13
|
+
consume and update this unified graph model.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import asdict, dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
23
|
+
|
|
24
|
+
from engine.events import event_bus
|
|
25
|
+
from engine.semantics import OutcomeClassifier, OutcomeDimension
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# 1. Standard Node Data Models
|
|
30
|
+
# ============================================================================
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class PaperNode:
|
|
34
|
+
paper_id: str
|
|
35
|
+
title: str
|
|
36
|
+
authors: List[str] = field(default_factory=list)
|
|
37
|
+
year: int = 2024
|
|
38
|
+
venue: str = "Academic Publication"
|
|
39
|
+
doi: Optional[str] = None
|
|
40
|
+
url: Optional[str] = None
|
|
41
|
+
authority_tier: int = 1 # Tier 1 (peer-reviewed / gold) to Tier 5 (grey/web)
|
|
42
|
+
peer_reviewed: bool = True
|
|
43
|
+
summary: str = ""
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
return asdict(self)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class EvidenceNode:
|
|
51
|
+
evidence_id: str
|
|
52
|
+
paper_id: str
|
|
53
|
+
outcome_metric: str
|
|
54
|
+
outcome_dimension: str = OutcomeDimension.GENERAL_MEASURE
|
|
55
|
+
claim_id: Optional[str] = None
|
|
56
|
+
outcome_id: Optional[str] = None
|
|
57
|
+
effect_size: Dict[str, Any] = field(default_factory=lambda: {"metric": "Hedges g", "value": 0.0, "ci_lower": 0.0, "ci_upper": 0.0, "p_value": 0.05})
|
|
58
|
+
sample_size: int = 0
|
|
59
|
+
sample_description: str = ""
|
|
60
|
+
study_design: str = "Quasi-Experimental" # RCT, Quasi-Experimental DID, Meta-Analysis, Observational
|
|
61
|
+
direction: str = "NEUTRAL" # SUPPORTS, CONTRADICTS, NEUTRAL, MIXED
|
|
62
|
+
confidence_score: float = 0.85
|
|
63
|
+
wwc_rating: str = "Meets Standards with Reservations" # WWC 5.0
|
|
64
|
+
key_quote: str = ""
|
|
65
|
+
calibrated_weight: float = 1.0
|
|
66
|
+
bias_flag: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
if self.outcome_dimension == OutcomeDimension.GENERAL_MEASURE and self.outcome_metric:
|
|
70
|
+
self.outcome_dimension = OutcomeClassifier.classify(self.outcome_metric)
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
return asdict(self)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class OutcomeNode:
|
|
78
|
+
outcome_id: str
|
|
79
|
+
name: str
|
|
80
|
+
dimension: str = OutcomeDimension.GENERAL_MEASURE
|
|
81
|
+
category: str = "Learning" # Learning, Task, Process, Risk, Policy
|
|
82
|
+
description: str = ""
|
|
83
|
+
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
if self.dimension == OutcomeDimension.GENERAL_MEASURE and self.name:
|
|
86
|
+
self.dimension = OutcomeClassifier.classify(self.name)
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
89
|
+
return asdict(self)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ClaimNode:
|
|
94
|
+
claim_id: str
|
|
95
|
+
statement: str
|
|
96
|
+
outcome_dimension: str = OutcomeDimension.GENERAL_MEASURE
|
|
97
|
+
outcome_metric: str = ""
|
|
98
|
+
status: str = "UNCERTAIN" # SUPPORTED, CONTRADICTS, MIXED, UNCERTAIN, INSUFFICIENT_EVIDENCE
|
|
99
|
+
pooled_effect_g: float = 0.0
|
|
100
|
+
evidence_ids: List[str] = field(default_factory=list)
|
|
101
|
+
bias_warning: str = ""
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
104
|
+
return asdict(self)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class RiskNode:
|
|
109
|
+
risk_id: str
|
|
110
|
+
risk_type: str # Scaffolding Dependency Trap, Novice Illusion, Academic Integrity, Transfer Deficit, Equity Gap
|
|
111
|
+
severity: str = "MODERATE" # HIGH, MODERATE, LOW
|
|
112
|
+
description: str = ""
|
|
113
|
+
mitigation: str = ""
|
|
114
|
+
triggered_by_evidence_ids: List[str] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
117
|
+
return asdict(self)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class GapNode:
|
|
122
|
+
gap_id: str
|
|
123
|
+
gap_type: str # Population Gap, Measurement/Retention Gap, Methodological Gap, Contradiction Lens
|
|
124
|
+
description: str = ""
|
|
125
|
+
target_outcome: str = ""
|
|
126
|
+
existing_evidence_summary: str = ""
|
|
127
|
+
recommended_trial_design: str = ""
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
130
|
+
return asdict(self)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class DecisionNode:
|
|
135
|
+
decision_id: str
|
|
136
|
+
verdict: str # ADOPT, PILOT, REJECT, INSUFFICIENT_EVIDENCE
|
|
137
|
+
confidence_score: float = 0.85
|
|
138
|
+
rationale: str = ""
|
|
139
|
+
applicability_boundary: str = ""
|
|
140
|
+
intervention_plan: Dict[str, Any] = field(default_factory=dict)
|
|
141
|
+
evaluation_plan: Dict[str, Any] = field(default_factory=dict)
|
|
142
|
+
stop_conditions: List[str] = field(default_factory=list)
|
|
143
|
+
timestamp: str = ""
|
|
144
|
+
|
|
145
|
+
def __post_init__(self):
|
|
146
|
+
if not self.timestamp:
|
|
147
|
+
self.timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
148
|
+
|
|
149
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
150
|
+
return asdict(self)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class GraphEdge:
|
|
155
|
+
source_id: str
|
|
156
|
+
target_id: str
|
|
157
|
+
relation: str # EXTRACTED_FROM, EVALUATES, SUPPORTS, CONTRADICTS, EXPOSES_RISK,
|
|
158
|
+
# IDENTIFIES_GAP, GROUNDS_DECISION, MEASURES
|
|
159
|
+
weight: float = 1.0
|
|
160
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
163
|
+
return asdict(self)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ============================================================================
|
|
167
|
+
# 2. Unified EvidenceGraph Single Source of Truth (SSOT)
|
|
168
|
+
# ============================================================================
|
|
169
|
+
|
|
170
|
+
class EvidenceGraph:
|
|
171
|
+
"""The central Single Source of Truth (SSOT) evidence graph."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, project_id: str = "default_project"):
|
|
174
|
+
self.project_id = project_id
|
|
175
|
+
self.revision_id: int = 1
|
|
176
|
+
self.intent: Dict[str, Any] = {}
|
|
177
|
+
|
|
178
|
+
# 7 Core Node Storage Tables
|
|
179
|
+
self.papers: Dict[str, PaperNode] = {}
|
|
180
|
+
self.evidence: Dict[str, EvidenceNode] = {}
|
|
181
|
+
self.outcomes: Dict[str, OutcomeNode] = {}
|
|
182
|
+
self.claims: Dict[str, ClaimNode] = {}
|
|
183
|
+
self.risks: Dict[str, RiskNode] = {}
|
|
184
|
+
self.gaps: Dict[str, GapNode] = {}
|
|
185
|
+
self.decision: Optional[DecisionNode] = None
|
|
186
|
+
|
|
187
|
+
# Edges Table
|
|
188
|
+
self.edges: List[GraphEdge] = []
|
|
189
|
+
self.audit_warnings: List[str] = []
|
|
190
|
+
self.created_at: str = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
191
|
+
self.updated_at: str = self.created_at
|
|
192
|
+
|
|
193
|
+
# --- Node Add / Upsert Helpers ---
|
|
194
|
+
|
|
195
|
+
def add_paper(self, paper: PaperNode) -> PaperNode:
|
|
196
|
+
self.papers[paper.paper_id] = paper
|
|
197
|
+
self._touch()
|
|
198
|
+
return paper
|
|
199
|
+
|
|
200
|
+
def _ensure_measures_edge(self, evidence_id: str, outcome_id: str) -> None:
|
|
201
|
+
"""Idempotently connect an Evidence node to the Outcome it measures.
|
|
202
|
+
|
|
203
|
+
add_edge already dedupes on (source, target, relation), so repeated
|
|
204
|
+
upserts in either order produce exactly one MEASURES edge.
|
|
205
|
+
"""
|
|
206
|
+
if evidence_id in self.evidence and outcome_id in self.outcomes:
|
|
207
|
+
self.add_edge(evidence_id, outcome_id, "MEASURES")
|
|
208
|
+
self._touch()
|
|
209
|
+
|
|
210
|
+
def add_evidence(self, ev: EvidenceNode) -> EvidenceNode:
|
|
211
|
+
self.evidence[ev.evidence_id] = ev
|
|
212
|
+
# Auto-create edge to paper if paper exists
|
|
213
|
+
if ev.paper_id in self.papers:
|
|
214
|
+
self.add_edge(ev.evidence_id, ev.paper_id, "EXTRACTED_FROM", weight=ev.calibrated_weight)
|
|
215
|
+
if ev.outcome_id:
|
|
216
|
+
self._ensure_measures_edge(ev.evidence_id, ev.outcome_id)
|
|
217
|
+
self._touch()
|
|
218
|
+
return ev
|
|
219
|
+
|
|
220
|
+
def add_outcome(self, outcome: OutcomeNode) -> OutcomeNode:
|
|
221
|
+
self.outcomes[outcome.outcome_id] = outcome
|
|
222
|
+
# Back-fill MEASURES edges for evidence already in the graph
|
|
223
|
+
for ev in self.evidence.values():
|
|
224
|
+
if ev.outcome_id == outcome.outcome_id:
|
|
225
|
+
self._ensure_measures_edge(ev.evidence_id, outcome.outcome_id)
|
|
226
|
+
self._touch()
|
|
227
|
+
return outcome
|
|
228
|
+
|
|
229
|
+
def add_claim(self, claim: ClaimNode) -> ClaimNode:
|
|
230
|
+
self.claims[claim.claim_id] = claim
|
|
231
|
+
for ev_id in claim.evidence_ids:
|
|
232
|
+
if ev_id in self.evidence:
|
|
233
|
+
rel = "SUPPORTS" if self.evidence[ev_id].direction == "SUPPORTS" else (
|
|
234
|
+
"CONTRADICTS" if self.evidence[ev_id].direction == "CONTRADICTS" else "EVALUATES"
|
|
235
|
+
)
|
|
236
|
+
self.add_edge(ev_id, claim.claim_id, rel)
|
|
237
|
+
self._touch()
|
|
238
|
+
return claim
|
|
239
|
+
|
|
240
|
+
def add_risk(self, risk: RiskNode) -> RiskNode:
|
|
241
|
+
self.risks[risk.risk_id] = risk
|
|
242
|
+
for ev_id in risk.triggered_by_evidence_ids:
|
|
243
|
+
if ev_id in self.evidence:
|
|
244
|
+
self.add_edge(ev_id, risk.risk_id, "EXPOSES_RISK")
|
|
245
|
+
self._touch()
|
|
246
|
+
return risk
|
|
247
|
+
|
|
248
|
+
def add_gap(self, gap: GapNode) -> GapNode:
|
|
249
|
+
self.gaps[gap.gap_id] = gap
|
|
250
|
+
self._touch()
|
|
251
|
+
return gap
|
|
252
|
+
|
|
253
|
+
def set_decision(self, decision: DecisionNode) -> DecisionNode:
|
|
254
|
+
self.decision = decision
|
|
255
|
+
# Connect decision to claims & risks
|
|
256
|
+
for c_id in self.claims:
|
|
257
|
+
self.add_edge(c_id, decision.decision_id, "GROUNDS_DECISION")
|
|
258
|
+
for r_id in self.risks:
|
|
259
|
+
self.add_edge(r_id, decision.decision_id, "GROUNDS_DECISION")
|
|
260
|
+
self._touch()
|
|
261
|
+
return decision
|
|
262
|
+
|
|
263
|
+
def add_edge(self, source_id: str, target_id: str, relation: str, weight: float = 1.0, metadata: Optional[Dict[str, Any]] = None) -> GraphEdge:
|
|
264
|
+
# Check duplicate
|
|
265
|
+
for e in self.edges:
|
|
266
|
+
if e.source_id == source_id and e.target_id == target_id and e.relation == relation:
|
|
267
|
+
e.weight = weight
|
|
268
|
+
if metadata:
|
|
269
|
+
e.metadata.update(metadata)
|
|
270
|
+
return e
|
|
271
|
+
edge = GraphEdge(source_id=source_id, target_id=target_id, relation=relation, weight=weight, metadata=metadata or {})
|
|
272
|
+
self.edges.append(edge)
|
|
273
|
+
return edge
|
|
274
|
+
|
|
275
|
+
def _touch(self):
|
|
276
|
+
self.updated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
277
|
+
|
|
278
|
+
# --- Query & Aggregation Helpers ---
|
|
279
|
+
|
|
280
|
+
def get_evidence_by_dimension(self, dimension: str) -> List[EvidenceNode]:
|
|
281
|
+
return [ev for ev in self.evidence.values() if ev.outcome_dimension == dimension]
|
|
282
|
+
|
|
283
|
+
def get_evidence_for_claim(self, claim_id: str) -> List[EvidenceNode]:
|
|
284
|
+
claim = self.claims.get(claim_id)
|
|
285
|
+
if not claim:
|
|
286
|
+
return []
|
|
287
|
+
return [self.evidence[eid] for eid in claim.evidence_ids if eid in self.evidence]
|
|
288
|
+
|
|
289
|
+
def get_forest_plot_data(self) -> List[Dict[str, Any]]:
|
|
290
|
+
"""Extracts structured forest plot data points across all evidence nodes."""
|
|
291
|
+
points = []
|
|
292
|
+
for ev in self.evidence.values():
|
|
293
|
+
paper = self.papers.get(ev.paper_id)
|
|
294
|
+
study_label = f"{paper.authors[0] if paper and paper.authors else ev.paper_id} ({paper.year if paper else ''})"
|
|
295
|
+
effect_val = ev.effect_size.get("value", 0.0)
|
|
296
|
+
ci_l = ev.effect_size.get("ci_lower")
|
|
297
|
+
ci_u = ev.effect_size.get("ci_upper")
|
|
298
|
+
has_ci = ci_l is not None and ci_u is not None and float(ci_u) >= float(ci_l)
|
|
299
|
+
points.append({
|
|
300
|
+
"evidence_id": ev.evidence_id,
|
|
301
|
+
"study_label": study_label,
|
|
302
|
+
"venue": paper.venue if paper else "Academic",
|
|
303
|
+
"outcome_metric": ev.outcome_metric,
|
|
304
|
+
"outcome_dimension": ev.outcome_dimension,
|
|
305
|
+
"effect_size": effect_val,
|
|
306
|
+
"ci_lower": round(float(ci_l), 3) if has_ci else None,
|
|
307
|
+
"ci_upper": round(float(ci_u), 3) if has_ci else None,
|
|
308
|
+
"precision_provenance": "reported_ci" if has_ci else "not_available",
|
|
309
|
+
"sample_size": ev.sample_size,
|
|
310
|
+
"weight": ev.calibrated_weight,
|
|
311
|
+
"direction": ev.direction,
|
|
312
|
+
"wwc_rating": ev.wwc_rating,
|
|
313
|
+
})
|
|
314
|
+
# Sort by dimension and effect size
|
|
315
|
+
return sorted(points, key=lambda p: (p["outcome_dimension"], p["effect_size"]), reverse=True)
|
|
316
|
+
|
|
317
|
+
def compute_meta_synthesis(self) -> Dict[str, Any]:
|
|
318
|
+
"""Calculates pooled effect sizes across outcome dimensions using Inverse-Variance weighting."""
|
|
319
|
+
by_dim: Dict[str, List[EvidenceNode]] = {}
|
|
320
|
+
for ev in self.evidence.values():
|
|
321
|
+
by_dim.setdefault(ev.outcome_dimension, []).append(ev)
|
|
322
|
+
|
|
323
|
+
dim_results = {}
|
|
324
|
+
for dim, nodes in by_dim.items():
|
|
325
|
+
effects = []
|
|
326
|
+
variances = []
|
|
327
|
+
precision_counts = {"reported_ci": 0, "derived_from_sample_size": 0}
|
|
328
|
+
excluded_no_precision = 0
|
|
329
|
+
for n in nodes:
|
|
330
|
+
eff = n.effect_size.get("value")
|
|
331
|
+
if eff is None or math.isnan(float(eff)) or math.isinf(float(eff)):
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
# Statistical variance derivation (Borenstein et al. 2009)
|
|
335
|
+
ci_l = n.effect_size.get("ci_lower")
|
|
336
|
+
ci_u = n.effect_size.get("ci_upper")
|
|
337
|
+
if ci_l is not None and ci_u is not None and float(ci_u) > float(ci_l):
|
|
338
|
+
se = (float(ci_u) - float(ci_l)) / (2.0 * 1.95996)
|
|
339
|
+
precision_counts["reported_ci"] += 1
|
|
340
|
+
elif n.sample_size and n.sample_size > 0:
|
|
341
|
+
eff_f = float(eff)
|
|
342
|
+
se = math.sqrt(4.0 / n.sample_size + (eff_f ** 2) / (2.0 * n.sample_size))
|
|
343
|
+
precision_counts["derived_from_sample_size"] += 1
|
|
344
|
+
else:
|
|
345
|
+
# No CI and no sample size: precision is genuinely unknown;
|
|
346
|
+
# never substitute a default SE (no false precision).
|
|
347
|
+
excluded_no_precision += 1
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
effects.append(float(eff))
|
|
351
|
+
variances.append(max(1e-4, se ** 2))
|
|
352
|
+
|
|
353
|
+
if not effects:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
inv_w = [1.0 / v for v in variances]
|
|
357
|
+
sum_w = sum(inv_w)
|
|
358
|
+
weighted_g = sum(e * w for e, w in zip(effects, inv_w)) / sum_w
|
|
359
|
+
|
|
360
|
+
k = len(effects)
|
|
361
|
+
if k > 1:
|
|
362
|
+
q_stat = sum(w * ((e - weighted_g) ** 2) for e, w in zip(effects, inv_w))
|
|
363
|
+
df = k - 1
|
|
364
|
+
i_squared = max(0.0, min(100.0, ((q_stat - df) / q_stat) * 100.0)) if q_stat > df else 0.0
|
|
365
|
+
else:
|
|
366
|
+
q_stat = 0.0
|
|
367
|
+
i_squared = 0.0
|
|
368
|
+
|
|
369
|
+
dim_results[dim] = {
|
|
370
|
+
"dimension": dim,
|
|
371
|
+
"studies_count": len(effects),
|
|
372
|
+
"pooled_g": round(weighted_g, 3),
|
|
373
|
+
"q_statistic": round(q_stat, 2),
|
|
374
|
+
"i_squared": round(i_squared, 1),
|
|
375
|
+
"precision_sources": precision_counts,
|
|
376
|
+
"excluded_no_precision": excluded_no_precision,
|
|
377
|
+
"interpretation": (
|
|
378
|
+
"Substantial Positive Effect" if weighted_g > 0.50 else (
|
|
379
|
+
"Moderate Positive Effect" if weighted_g > 0.20 else (
|
|
380
|
+
"Null / Negligible Effect" if weighted_g >= -0.05 else "Negative Deficit"
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
return dim_results
|
|
386
|
+
|
|
387
|
+
def export_echarts_graph(self) -> Dict[str, Any]:
|
|
388
|
+
"""Exports graph topology formatted for ECharts Force graph visualization."""
|
|
389
|
+
category_map = {
|
|
390
|
+
"PaperNode": 0,
|
|
391
|
+
"EvidenceNode": 1,
|
|
392
|
+
"ClaimNode": 2,
|
|
393
|
+
"OutcomeNode": 3,
|
|
394
|
+
"RiskNode": 4,
|
|
395
|
+
"GapNode": 5,
|
|
396
|
+
"DecisionNode": 6,
|
|
397
|
+
}
|
|
398
|
+
categories = [
|
|
399
|
+
{"name": "Literature Paper (文献)"},
|
|
400
|
+
{"name": "Evidence Node (实证发现)"},
|
|
401
|
+
{"name": "Claim (研究主张)"},
|
|
402
|
+
{"name": "Outcome (测量指标)"},
|
|
403
|
+
{"name": "Methodological Risk (学术陷阱)"},
|
|
404
|
+
{"name": "Research Gap (学术缺口)"},
|
|
405
|
+
{"name": "Decision (裁决快照)"},
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
nodes = []
|
|
409
|
+
for p in self.papers.values():
|
|
410
|
+
nodes.append({
|
|
411
|
+
"id": p.paper_id,
|
|
412
|
+
"name": f"{p.authors[0] if p.authors else p.paper_id} ({p.year})",
|
|
413
|
+
"category": category_map["PaperNode"],
|
|
414
|
+
"category_name": "PaperNode (文献)",
|
|
415
|
+
"symbolSize": 28,
|
|
416
|
+
"value": p.title,
|
|
417
|
+
"authors": p.authors,
|
|
418
|
+
"year": p.year,
|
|
419
|
+
"venue": p.venue,
|
|
420
|
+
"doi": p.doi,
|
|
421
|
+
"tier": p.authority_tier,
|
|
422
|
+
"quote": p.summary,
|
|
423
|
+
})
|
|
424
|
+
for ev in self.evidence.values():
|
|
425
|
+
effect_val = ev.effect_size.get("value", 0.0)
|
|
426
|
+
symbol_size = max(18, min(45, int(18 + abs(effect_val) * 20)))
|
|
427
|
+
nodes.append({
|
|
428
|
+
"id": ev.evidence_id,
|
|
429
|
+
"name": f"{ev.evidence_id}: {ev.outcome_metric} (g={effect_val:+.2f})",
|
|
430
|
+
"category": category_map["EvidenceNode"],
|
|
431
|
+
"category_name": "EvidenceNode (实证效应量)",
|
|
432
|
+
"symbolSize": symbol_size,
|
|
433
|
+
"dimension": ev.outcome_dimension,
|
|
434
|
+
"direction": ev.direction,
|
|
435
|
+
"effect_size": effect_val,
|
|
436
|
+
"ci_lower": ev.effect_size.get("ci_lower", "N/A"),
|
|
437
|
+
"ci_upper": ev.effect_size.get("ci_upper", "N/A"),
|
|
438
|
+
"sample_size": ev.sample_size,
|
|
439
|
+
"wwc_rating": ev.wwc_rating,
|
|
440
|
+
"quote": ev.key_quote,
|
|
441
|
+
})
|
|
442
|
+
for o in self.outcomes.values():
|
|
443
|
+
nodes.append({
|
|
444
|
+
"id": o.outcome_id,
|
|
445
|
+
"name": f"📊 {o.name}",
|
|
446
|
+
"category": category_map["OutcomeNode"],
|
|
447
|
+
"category_name": "OutcomeNode (测量指标)",
|
|
448
|
+
"symbolSize": 30,
|
|
449
|
+
"dimension": o.dimension,
|
|
450
|
+
"value": o.description,
|
|
451
|
+
})
|
|
452
|
+
for c in self.claims.values():
|
|
453
|
+
nodes.append({
|
|
454
|
+
"id": c.claim_id,
|
|
455
|
+
"name": f"{c.claim_id}: {c.statement[:24]}...",
|
|
456
|
+
"category": category_map["ClaimNode"],
|
|
457
|
+
"category_name": "ClaimNode (科学主张)",
|
|
458
|
+
"symbolSize": 34,
|
|
459
|
+
"status": c.status,
|
|
460
|
+
"pooled_g": c.pooled_effect_g,
|
|
461
|
+
"value": c.statement,
|
|
462
|
+
"quote": c.bias_warning,
|
|
463
|
+
})
|
|
464
|
+
for r in self.risks.values():
|
|
465
|
+
nodes.append({
|
|
466
|
+
"id": r.risk_id,
|
|
467
|
+
"name": f"⚠️ {r.risk_type}",
|
|
468
|
+
"category": category_map["RiskNode"],
|
|
469
|
+
"category_name": "RiskNode (方法学风险)",
|
|
470
|
+
"symbolSize": 36,
|
|
471
|
+
"severity": r.severity,
|
|
472
|
+
"value": r.description,
|
|
473
|
+
"quote": r.mitigation,
|
|
474
|
+
})
|
|
475
|
+
for g in self.gaps.values():
|
|
476
|
+
nodes.append({
|
|
477
|
+
"id": g.gap_id,
|
|
478
|
+
"name": f"🔬 {g.gap_type}",
|
|
479
|
+
"category": category_map["GapNode"],
|
|
480
|
+
"category_name": "GapNode (研究缺口)",
|
|
481
|
+
"symbolSize": 32,
|
|
482
|
+
"value": g.description,
|
|
483
|
+
"quote": g.recommended_trial_design,
|
|
484
|
+
})
|
|
485
|
+
if self.decision:
|
|
486
|
+
nodes.append({
|
|
487
|
+
"id": self.decision.decision_id,
|
|
488
|
+
"name": f"⚖️ {self.decision.verdict}",
|
|
489
|
+
"category": category_map["DecisionNode"],
|
|
490
|
+
"category_name": "DecisionNode (裁决快照)",
|
|
491
|
+
"symbolSize": 50,
|
|
492
|
+
"verdict": self.decision.verdict,
|
|
493
|
+
"confidence": self.decision.confidence_score,
|
|
494
|
+
"value": self.decision.rationale,
|
|
495
|
+
"quote": self.decision.applicability_boundary,
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
links = []
|
|
499
|
+
for e in self.edges:
|
|
500
|
+
line_color = (
|
|
501
|
+
"#2ea043" if e.relation == "SUPPORTS" else (
|
|
502
|
+
"#f85149" if e.relation in ("CONTRADICTS", "EXPOSES_RISK") else "#58a6ff"
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
links.append({
|
|
506
|
+
"source": e.source_id,
|
|
507
|
+
"target": e.target_id,
|
|
508
|
+
"label": {"show": True, "formatter": e.relation},
|
|
509
|
+
"lineStyle": {"color": line_color, "width": max(1.0, e.weight * 2.0)},
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
"categories": categories,
|
|
514
|
+
"nodes": nodes,
|
|
515
|
+
"links": links,
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# --- Serialization & Persistence ---
|
|
519
|
+
|
|
520
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
521
|
+
return {
|
|
522
|
+
"project_id": self.project_id,
|
|
523
|
+
"revision_id": self.revision_id,
|
|
524
|
+
"intent": self.intent,
|
|
525
|
+
"created_at": self.created_at,
|
|
526
|
+
"updated_at": self.updated_at,
|
|
527
|
+
"audit_warnings": self.audit_warnings,
|
|
528
|
+
"papers": {k: v.to_dict() for k, v in self.papers.items()},
|
|
529
|
+
"evidence": {k: v.to_dict() for k, v in self.evidence.items()},
|
|
530
|
+
"outcomes": {k: v.to_dict() for k, v in self.outcomes.items()},
|
|
531
|
+
"claims": {k: v.to_dict() for k, v in self.claims.items()},
|
|
532
|
+
"risks": {k: v.to_dict() for k, v in self.risks.items()},
|
|
533
|
+
"gaps": {k: v.to_dict() for k, v in self.gaps.items()},
|
|
534
|
+
"decision": self.decision.to_dict() if self.decision else None,
|
|
535
|
+
"edges": [e.to_dict() for e in self.edges],
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
def to_json(self, indent: int = 2) -> str:
|
|
539
|
+
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
|
540
|
+
|
|
541
|
+
@classmethod
|
|
542
|
+
def from_dict(cls, data: Dict[str, Any]) -> EvidenceGraph:
|
|
543
|
+
graph = cls(project_id=data.get("project_id", "default_project"))
|
|
544
|
+
graph.revision_id = data.get("revision_id", 1)
|
|
545
|
+
graph.intent = data.get("intent", {})
|
|
546
|
+
graph.created_at = data.get("created_at", graph.created_at)
|
|
547
|
+
graph.updated_at = data.get("updated_at", graph.updated_at)
|
|
548
|
+
graph.audit_warnings = data.get("audit_warnings", [])
|
|
549
|
+
|
|
550
|
+
for k, v in data.get("papers", {}).items():
|
|
551
|
+
graph.papers[k] = PaperNode(**v)
|
|
552
|
+
for k, v in data.get("evidence", {}).items():
|
|
553
|
+
graph.evidence[k] = EvidenceNode(**v)
|
|
554
|
+
for k, v in data.get("outcomes", {}).items():
|
|
555
|
+
graph.outcomes[k] = OutcomeNode(**v)
|
|
556
|
+
for k, v in data.get("claims", {}).items():
|
|
557
|
+
graph.claims[k] = ClaimNode(**v)
|
|
558
|
+
for k, v in data.get("risks", {}).items():
|
|
559
|
+
graph.risks[k] = RiskNode(**v)
|
|
560
|
+
for k, v in data.get("gaps", {}).items():
|
|
561
|
+
graph.gaps[k] = GapNode(**v)
|
|
562
|
+
if data.get("decision"):
|
|
563
|
+
graph.decision = DecisionNode(**data["decision"])
|
|
564
|
+
for e in data.get("edges", []):
|
|
565
|
+
graph.edges.append(GraphEdge(**e))
|
|
566
|
+
|
|
567
|
+
return graph
|
|
568
|
+
|
|
569
|
+
@classmethod
|
|
570
|
+
def from_json(cls, json_str: str) -> EvidenceGraph:
|
|
571
|
+
return cls.from_dict(json.loads(json_str))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Graph-oriented evidence review mutations.
|
|
2
|
+
|
|
3
|
+
Every scientific change goes through GraphStore.commit() — nothing in this
|
|
4
|
+
module writes JSONL directly. Ingestion gates:
|
|
5
|
+
- a Source whose validation_status is neither valid nor accepted_partial
|
|
6
|
+
cannot be used to build a validated Finding path;
|
|
7
|
+
- Findings/Studies must reference existing Source provenance;
|
|
8
|
+
- Claims may link a negative Finding without changing its direction;
|
|
9
|
+
- counter-evidence Findings are never dropped for unfavorable implications.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from engine.graph_store import GraphStore, GraphMutation, GraphRevision
|
|
15
|
+
|
|
16
|
+
VALID_SOURCE_STATUSES = ("valid", "accepted_partial")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ingest_validated_sources(store: GraphStore, *, run_id: str,
|
|
20
|
+
sources: list[dict]) -> GraphRevision:
|
|
21
|
+
"""Commit validated sources (only valid/accepted_partial statuses)."""
|
|
22
|
+
for src in sources:
|
|
23
|
+
if src.get("validation_status") not in VALID_SOURCE_STATUSES:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"source {src.get('source_id')} has status "
|
|
26
|
+
f"{src.get('validation_status')!r}; only {list(VALID_SOURCE_STATUSES)} "
|
|
27
|
+
f"can enter the graph as validated evidence"
|
|
28
|
+
)
|
|
29
|
+
return store.commit(
|
|
30
|
+
run_id=run_id, reason="ingest validated sources",
|
|
31
|
+
mutation=GraphMutation(upserts={"sources": sources}, retire_ids={}))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ingest_extracted_studies_findings(store: GraphStore, *, run_id: str,
|
|
35
|
+
studies: list[dict],
|
|
36
|
+
findings: list[dict]) -> GraphRevision:
|
|
37
|
+
"""Commit extracted Studies + Findings (must reference existing Sources)."""
|
|
38
|
+
source_ids = {s["source_id"] for s in store.read_table("sources")}
|
|
39
|
+
study_ids = {s["study_id"] for s in store.read_table("studies")}
|
|
40
|
+
for s in studies:
|
|
41
|
+
missing = [sid for sid in s.get("source_ids", []) if sid not in source_ids]
|
|
42
|
+
if missing:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"study {s.get('study_id')} references missing sources: {missing}")
|
|
45
|
+
for f in findings:
|
|
46
|
+
if f.get("study_id") not in study_ids and f.get("study_id") not in {
|
|
47
|
+
s.get("study_id") for s in studies}:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"finding {f.get('finding_id')} references absent study "
|
|
50
|
+
f"{f.get('study_id')}; provenance missing")
|
|
51
|
+
return store.commit(
|
|
52
|
+
run_id=run_id, reason="ingest studies and findings",
|
|
53
|
+
mutation=GraphMutation(upserts={"studies": studies, "findings": findings},
|
|
54
|
+
retire_ids={}))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ingest_methodology_audits(store: GraphStore, *, run_id: str,
|
|
58
|
+
audits: list[dict]) -> GraphRevision:
|
|
59
|
+
"""Commit methodology audits (must reference existing Studies)."""
|
|
60
|
+
study_ids = {s["study_id"] for s in store.read_table("studies")}
|
|
61
|
+
for a in audits:
|
|
62
|
+
if a.get("study_id") not in study_ids:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"audit {a.get('audit_id')} references missing study "
|
|
65
|
+
f"{a.get('study_id')}")
|
|
66
|
+
return store.commit(
|
|
67
|
+
run_id=run_id, reason="ingest methodology audits",
|
|
68
|
+
mutation=GraphMutation(upserts={"audits": audits}, retire_ids={}))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ingest_claims_links(store: GraphStore, *, run_id: str,
|
|
72
|
+
claims: list[dict], links: list[dict]) -> GraphRevision:
|
|
73
|
+
"""Commit Claims + EvidenceLinks (must reference existing Findings).
|
|
74
|
+
|
|
75
|
+
A link may support a negative Finding; the Finding's effect_direction is
|
|
76
|
+
never rewritten by the link. Counter-evidence links with unfavorable
|
|
77
|
+
decision implications are preserved.
|
|
78
|
+
"""
|
|
79
|
+
finding_ids = {f["finding_id"] for f in store.read_table("findings")}
|
|
80
|
+
for link in links:
|
|
81
|
+
if link.get("finding_id") not in finding_ids:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"evidence_link {link.get('evidence_link_id')} references "
|
|
84
|
+
f"missing finding {link.get('finding_id')}")
|
|
85
|
+
return store.commit(
|
|
86
|
+
run_id=run_id, reason="ingest claims and links",
|
|
87
|
+
mutation=GraphMutation(upserts={"claims": claims, "evidence_links": links},
|
|
88
|
+
retire_ids={}))
|