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,187 @@
|
|
|
1
|
+
"""Claim synthesis with independent-study semantics.
|
|
2
|
+
|
|
3
|
+
Status rule (frozen baseline in code + tests): a usable Study requires
|
|
4
|
+
valid/accepted_partial Source provenance, resolvable Study identity, latest
|
|
5
|
+
MethodologyAudit != `fail`, and valid active Finding/Link. Multiple Findings
|
|
6
|
+
from one Study never create multiple votes.
|
|
7
|
+
|
|
8
|
+
support-only usable Studies → supported
|
|
9
|
+
contradiction-only → refuted
|
|
10
|
+
both independent directions → contested
|
|
11
|
+
no decisive usable Study / neutral-only → insufficient
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from engine.contracts import validate_record
|
|
19
|
+
from engine.graph_store import GraphStore
|
|
20
|
+
from engine.semantics import claim_relation, decision_implication
|
|
21
|
+
|
|
22
|
+
VALID_SOURCE_STATUSES = ("valid", "accepted_partial")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ClaimSynthesis:
|
|
27
|
+
claim_id: str
|
|
28
|
+
status: str
|
|
29
|
+
study_ids: tuple[str, ...]
|
|
30
|
+
independent_sample_keys: tuple[str, ...]
|
|
31
|
+
supporting_link_ids: tuple[str, ...]
|
|
32
|
+
contradicting_link_ids: tuple[str, ...]
|
|
33
|
+
neutral_link_ids: tuple[str, ...]
|
|
34
|
+
methodology_summary: dict
|
|
35
|
+
directness_summary: dict
|
|
36
|
+
unresolved_conflicts: tuple[str, ...]
|
|
37
|
+
missing_evidence: tuple[str, ...]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _usable_studies(store: GraphStore) -> dict[str, dict]:
|
|
41
|
+
"""Study id → Study record for usable studies.
|
|
42
|
+
|
|
43
|
+
Usable = resolvable identity + at least one valid/accepted_partial source.
|
|
44
|
+
"""
|
|
45
|
+
sources = {s["source_id"]: s for s in store.read_table("sources")}
|
|
46
|
+
usable: dict[str, dict] = {}
|
|
47
|
+
for s in store.read_table("studies"):
|
|
48
|
+
if s.get("identity_status") == "unresolved":
|
|
49
|
+
continue
|
|
50
|
+
if not any(sid in sources and sources[sid]["validation_status"] in VALID_SOURCE_STATUSES
|
|
51
|
+
for sid in s.get("source_ids", [])):
|
|
52
|
+
continue
|
|
53
|
+
usable[s["study_id"]] = s
|
|
54
|
+
return usable
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _latest_audits(store: GraphStore) -> dict[str, dict]:
|
|
58
|
+
"""Study id → latest MethodologyAudit (by audited_at)."""
|
|
59
|
+
latest: dict[str, dict] = {}
|
|
60
|
+
for a in store.read_table("audits"):
|
|
61
|
+
cur = latest.get(a["study_id"])
|
|
62
|
+
if cur is None or a["audited_at"] >= cur["audited_at"]:
|
|
63
|
+
latest[a["study_id"]] = a
|
|
64
|
+
return latest
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def synthesize_claim(store: GraphStore, claim_id: str) -> ClaimSynthesis:
|
|
68
|
+
usable_studies = _usable_studies(store)
|
|
69
|
+
audits = _latest_audits(store)
|
|
70
|
+
findings = {f["finding_id"]: f for f in store.read_table("findings")}
|
|
71
|
+
links = store.read_table("evidence_links")
|
|
72
|
+
claims = {c["claim_id"]: c for c in store.read_table("claims")}
|
|
73
|
+
if claim_id not in claims:
|
|
74
|
+
raise KeyError(f"claim {claim_id} not in graph")
|
|
75
|
+
|
|
76
|
+
claim = claims[claim_id]
|
|
77
|
+
claim_links = [l for l in links if l["claim_id"] == claim_id]
|
|
78
|
+
|
|
79
|
+
# group links per usable study
|
|
80
|
+
study_links: dict[str, list[dict]] = {}
|
|
81
|
+
for link in claim_links:
|
|
82
|
+
fnd = findings.get(link["finding_id"])
|
|
83
|
+
if fnd is None:
|
|
84
|
+
continue
|
|
85
|
+
sid = fnd["study_id"]
|
|
86
|
+
study = usable_studies.get(sid)
|
|
87
|
+
if study is None:
|
|
88
|
+
continue
|
|
89
|
+
audit = audits.get(sid)
|
|
90
|
+
if audit is not None and audit.get("overall_status") == "fail":
|
|
91
|
+
continue
|
|
92
|
+
study_links.setdefault(sid, []).append(link)
|
|
93
|
+
|
|
94
|
+
# per-study decision relation (independent votes)
|
|
95
|
+
from collections import Counter
|
|
96
|
+
support_votes: list[str] = []
|
|
97
|
+
oppose_votes: list[str] = []
|
|
98
|
+
neutral_only: list[str] = []
|
|
99
|
+
independent_samples: set[str] = set()
|
|
100
|
+
|
|
101
|
+
study_ids: list[str] = []
|
|
102
|
+
supporting_link_ids: list[str] = []
|
|
103
|
+
contradicting_link_ids: list[str] = []
|
|
104
|
+
neutral_link_ids: list[str] = []
|
|
105
|
+
unresolved: list[str] = []
|
|
106
|
+
|
|
107
|
+
seen_keys: set[str] = set()
|
|
108
|
+
for sid, links_for_study in sorted(study_links.items()):
|
|
109
|
+
study = usable_studies[sid]
|
|
110
|
+
independent_samples.add(study["independence_key"])
|
|
111
|
+
relations = [claim_relation(l) for l in links_for_study]
|
|
112
|
+
implications = [decision_implication(l) for l in links_for_study]
|
|
113
|
+
has_support = any(r == "support" for r in relations)
|
|
114
|
+
has_contradict = any(r == "contradict" for r in relations)
|
|
115
|
+
has_conditional = any(i == "conditional" for i in implications)
|
|
116
|
+
if has_conditional:
|
|
117
|
+
unresolved.append(f"{sid}: conditional implication")
|
|
118
|
+
# independent-study votes: two Studies sharing an independence_key
|
|
119
|
+
# are one independent unit — only the first votes
|
|
120
|
+
key = study["independence_key"]
|
|
121
|
+
is_first_with_key = key not in seen_keys
|
|
122
|
+
seen_keys.add(key)
|
|
123
|
+
if has_support and not has_contradict and is_first_with_key:
|
|
124
|
+
support_votes.append(sid)
|
|
125
|
+
elif has_contradict and not has_support and is_first_with_key:
|
|
126
|
+
oppose_votes.append(sid)
|
|
127
|
+
elif has_support and has_contradict:
|
|
128
|
+
# within-study conflict is a real unresolved conflict, not a vote
|
|
129
|
+
unresolved.append(f"{sid}: within-study support+contradict links")
|
|
130
|
+
elif not is_first_with_key:
|
|
131
|
+
unresolved.append(
|
|
132
|
+
f"{sid}: shares independence_key {key!r} with another study; "
|
|
133
|
+
f"not counted as an independent vote")
|
|
134
|
+
else:
|
|
135
|
+
neutral_only.append(sid)
|
|
136
|
+
study_ids.append(sid)
|
|
137
|
+
for l in links_for_study:
|
|
138
|
+
rel = claim_relation(l)
|
|
139
|
+
if rel == "support":
|
|
140
|
+
supporting_link_ids.append(l["evidence_link_id"])
|
|
141
|
+
elif rel == "contradict":
|
|
142
|
+
contradicting_link_ids.append(l["evidence_link_id"])
|
|
143
|
+
else:
|
|
144
|
+
neutral_link_ids.append(l["evidence_link_id"])
|
|
145
|
+
|
|
146
|
+
if support_votes and not oppose_votes:
|
|
147
|
+
status = "supported"
|
|
148
|
+
elif oppose_votes and not support_votes:
|
|
149
|
+
status = "refuted"
|
|
150
|
+
elif support_votes and oppose_votes:
|
|
151
|
+
status = "contested"
|
|
152
|
+
else:
|
|
153
|
+
status = "insufficient"
|
|
154
|
+
|
|
155
|
+
# methodology + directness summaries
|
|
156
|
+
meth_summary: dict = {"studies": len(study_ids), "audit_fail": 0, "concern": 0, "pass": 0}
|
|
157
|
+
directness_values: list[int] = []
|
|
158
|
+
for sid in study_ids:
|
|
159
|
+
a = audits.get(sid)
|
|
160
|
+
if a is None:
|
|
161
|
+
continue
|
|
162
|
+
status_ = a.get("overall_status")
|
|
163
|
+
meth_summary[status_ if status_ in meth_summary else "concern"] += 1
|
|
164
|
+
for l in study_links.get(sid, []):
|
|
165
|
+
directness_values.append(int(l.get("directness", 0)))
|
|
166
|
+
|
|
167
|
+
return ClaimSynthesis(
|
|
168
|
+
claim_id=claim_id,
|
|
169
|
+
status=status,
|
|
170
|
+
study_ids=tuple(study_ids),
|
|
171
|
+
independent_sample_keys=tuple(sorted(independent_samples)),
|
|
172
|
+
supporting_link_ids=tuple(supporting_link_ids),
|
|
173
|
+
contradicting_link_ids=tuple(contradicting_link_ids),
|
|
174
|
+
neutral_link_ids=tuple(neutral_link_ids),
|
|
175
|
+
methodology_summary=meth_summary,
|
|
176
|
+
directness_summary={"mean": (sum(directness_values) / len(directness_values)
|
|
177
|
+
if directness_values else 0.0),
|
|
178
|
+
"count": len(directness_values)},
|
|
179
|
+
unresolved_conflicts=tuple(unresolved),
|
|
180
|
+
missing_evidence=(),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def synthesize_project(store: GraphStore) -> tuple[ClaimSynthesis, ...]:
|
|
185
|
+
claims = store.read_table("claims")
|
|
186
|
+
return tuple(synthesize_claim(store, c["claim_id"]) for c in sorted(
|
|
187
|
+
claims, key=lambda c: c["claim_id"]))
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""V2 Evidence Tribunal — revision-bound DecisionSnapshot generation.
|
|
2
|
+
|
|
3
|
+
Confidence policy (migration baseline, CONFIDENCE_POLICY_VERSION
|
|
4
|
+
2026-08-12.v3):
|
|
5
|
+
|
|
6
|
+
quality_term = mean per usable independent Study of
|
|
7
|
+
(design+sample+measurement+temporal) / 8
|
|
8
|
+
study relation = collapse active relevant links in one Study:
|
|
9
|
+
support-only → support_adoption; oppose-only →
|
|
10
|
+
oppose_adoption; both/conditional → conditional;
|
|
11
|
+
neutral-only → neutral
|
|
12
|
+
consistency_term = majority proportion over decisive Study relations
|
|
13
|
+
directness_term = mean across Studies of (mean link directness / 2)
|
|
14
|
+
count_term = min(1.0, usable independent studies / 4)
|
|
15
|
+
conflict_penalty = 0.15 only when independent support_adoption AND
|
|
16
|
+
oppose_adoption Studies both exist
|
|
17
|
+
uncertainty_penalty = min(0.20, 0.05 * critical_uncertainty_units)
|
|
18
|
+
|
|
19
|
+
score = 0.30*q + 0.25*c + 0.20*d + 0.25*n - conflict - uncertainty
|
|
20
|
+
|
|
21
|
+
High >= .72 | Moderate >= .45 | Low >= .20 | else Insufficient
|
|
22
|
+
|
|
23
|
+
Directness is NOT double-counted (it is excluded from the quality term's
|
|
24
|
+
methodology scope by construction here: quality reads only the four audit
|
|
25
|
+
dimensions). The score is an auditable internal index, never a probability.
|
|
26
|
+
|
|
27
|
+
Decision action is gate-enforced:
|
|
28
|
+
Low/Insufficient cannot yield ADOPT
|
|
29
|
+
REJECT requires usable direct negative/opposition evidence
|
|
30
|
+
unresolved but promising evidence may yield PILOT
|
|
31
|
+
otherwise INSUFFICIENT_EVIDENCE
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
from datetime import datetime, timezone
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from engine.contracts import validate_record
|
|
41
|
+
from engine.graph_store import GraphStore
|
|
42
|
+
from engine.ids import new_local_id
|
|
43
|
+
from engine.semantics import claim_relation, decision_implication
|
|
44
|
+
from engine.synthesis import ClaimSynthesis, synthesize_project
|
|
45
|
+
from engine.versions import (
|
|
46
|
+
CONFIDENCE_POLICY_VERSION,
|
|
47
|
+
METHODOLOGY_POLICY_VERSION,
|
|
48
|
+
SOURCE_VALIDATION_POLICY_VERSION,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
THRESHOLD_HIGH = 0.72
|
|
52
|
+
THRESHOLD_MODERATE = 0.45
|
|
53
|
+
THRESHOLD_LOW = 0.20
|
|
54
|
+
CONFLICT_PENALTY = 0.15
|
|
55
|
+
UNCERTAINTY_PER_UNIT = 0.05
|
|
56
|
+
UNCERTAINTY_CAP = 0.20
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _now_iso() -> str:
|
|
60
|
+
return datetime.now(timezone.utc).isoformat()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _label(score: float) -> str:
|
|
64
|
+
if score >= THRESHOLD_HIGH:
|
|
65
|
+
return "High"
|
|
66
|
+
if score >= THRESHOLD_MODERATE:
|
|
67
|
+
return "Moderate"
|
|
68
|
+
if score >= THRESHOLD_LOW:
|
|
69
|
+
return "Low"
|
|
70
|
+
return "Insufficient"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _study_relation(store: GraphStore, claim_syn: ClaimSynthesis,
|
|
74
|
+
study_id: str, findings: dict, links: list[dict]) -> str:
|
|
75
|
+
"""Collapse all active relevant links of one Study into one relation."""
|
|
76
|
+
implications = []
|
|
77
|
+
for link in links:
|
|
78
|
+
if link["claim_id"] != claim_syn.claim_id:
|
|
79
|
+
continue
|
|
80
|
+
fnd = findings.get(link["finding_id"])
|
|
81
|
+
if fnd is None or fnd["study_id"] != study_id:
|
|
82
|
+
continue
|
|
83
|
+
implications.append(decision_implication(link))
|
|
84
|
+
if not implications:
|
|
85
|
+
return "neutral"
|
|
86
|
+
support = any(i == "support_adoption" for i in implications)
|
|
87
|
+
oppose = any(i == "oppose_adoption" for i in implications)
|
|
88
|
+
conditional = any(i == "conditional" for i in implications)
|
|
89
|
+
if support and oppose:
|
|
90
|
+
return "conditional"
|
|
91
|
+
if conditional:
|
|
92
|
+
return "conditional"
|
|
93
|
+
if support:
|
|
94
|
+
return "support_adoption"
|
|
95
|
+
if oppose:
|
|
96
|
+
return "oppose_adoption"
|
|
97
|
+
return "neutral"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _usable_studies(store: GraphStore) -> dict[str, dict]:
|
|
101
|
+
sources = {s["source_id"]: s for s in store.read_table("sources")}
|
|
102
|
+
usable: dict[str, dict] = {}
|
|
103
|
+
for s in store.read_table("studies"):
|
|
104
|
+
if s.get("identity_status") == "unresolved":
|
|
105
|
+
continue
|
|
106
|
+
if not any(sid in sources and sources[sid]["validation_status"] in
|
|
107
|
+
("valid", "accepted_partial") for sid in s.get("source_ids", [])):
|
|
108
|
+
continue
|
|
109
|
+
usable[s["study_id"]] = s
|
|
110
|
+
return usable
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _latest_audits(store: GraphStore) -> dict[str, dict]:
|
|
114
|
+
latest: dict[str, dict] = {}
|
|
115
|
+
for a in store.read_table("audits"):
|
|
116
|
+
cur = latest.get(a["study_id"])
|
|
117
|
+
if cur is None or a["audited_at"] >= cur["audited_at"]:
|
|
118
|
+
latest[a["study_id"]] = a
|
|
119
|
+
return latest
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _study_implication(implications: list[str]) -> str:
|
|
123
|
+
"""Collapse all active links of one Study into one decision relation.
|
|
124
|
+
|
|
125
|
+
support-only → support_adoption; oppose-only → oppose_adoption;
|
|
126
|
+
any conditional or both directions → conditional; neutral-only → neutral.
|
|
127
|
+
This is Study-level folding: every Study is folded exactly once across
|
|
128
|
+
ALL its links, so a multi-claim Study can never vote twice or be
|
|
129
|
+
overwritten by a later Claim.
|
|
130
|
+
"""
|
|
131
|
+
if not implications:
|
|
132
|
+
return "neutral"
|
|
133
|
+
support = any(i == "support_adoption" for i in implications)
|
|
134
|
+
oppose = any(i == "oppose_adoption" for i in implications)
|
|
135
|
+
conditional = any(i == "conditional" for i in implications)
|
|
136
|
+
if conditional or (support and oppose):
|
|
137
|
+
return "conditional"
|
|
138
|
+
if support:
|
|
139
|
+
return "support_adoption"
|
|
140
|
+
if oppose:
|
|
141
|
+
return "oppose_adoption"
|
|
142
|
+
return "neutral"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _confidence(store: GraphStore, syntheses: tuple[ClaimSynthesis, ...]) -> dict:
|
|
146
|
+
"""Deterministic confidence over usable independent studies.
|
|
147
|
+
|
|
148
|
+
Each usable Study (valid source provenance, resolved identity, latest
|
|
149
|
+
audit != fail) is folded exactly once across ALL its links, independent
|
|
150
|
+
of Claim: support_adoption / oppose_adoption are decisive votes,
|
|
151
|
+
conditional counts as a critical-uncertainty unit, neutral is ignored.
|
|
152
|
+
|
|
153
|
+
quality_term = mean over usable studies of
|
|
154
|
+
(design+sample+measurement+temporal) / 8
|
|
155
|
+
consistency_term = majority proportion over decisive relations
|
|
156
|
+
directness_term = mean across studies of (mean link directness / 2)
|
|
157
|
+
count_term = min(1.0, usable_independent_studies / 4)
|
|
158
|
+
conflict_penalty = 0.15 only when independent support_adoption AND
|
|
159
|
+
oppose_adoption studies both exist
|
|
160
|
+
uncertainty = min(0.20, 0.05 * critical_uncertainty_units)
|
|
161
|
+
|
|
162
|
+
Directness is not double-counted (quality reads only the four audit
|
|
163
|
+
dimensions). The score is an auditable internal index, never a
|
|
164
|
+
probability.
|
|
165
|
+
"""
|
|
166
|
+
usable = _usable_studies(store)
|
|
167
|
+
audits = _latest_audits(store)
|
|
168
|
+
findings = {f["finding_id"]: f for f in store.read_table("findings")}
|
|
169
|
+
links = store.read_table("evidence_links")
|
|
170
|
+
|
|
171
|
+
# group links per study (one fold per study across all claims)
|
|
172
|
+
study_link_map: dict[str, list[dict]] = {}
|
|
173
|
+
for link in links:
|
|
174
|
+
fnd = findings.get(link["finding_id"])
|
|
175
|
+
if fnd is not None:
|
|
176
|
+
study_link_map.setdefault(fnd["study_id"], []).append(link)
|
|
177
|
+
|
|
178
|
+
decisive: dict[str, str] = {} # study_id -> support_adoption|oppose_adoption
|
|
179
|
+
usable_studies: list[str] = []
|
|
180
|
+
quality_sum = 0.0
|
|
181
|
+
directness_sum = 0.0
|
|
182
|
+
directness_count = 0
|
|
183
|
+
critical_uncertainty_units = 0
|
|
184
|
+
|
|
185
|
+
for sid, study in sorted(usable.items()):
|
|
186
|
+
audit = audits.get(sid)
|
|
187
|
+
if audit is None or audit.get("overall_status") == "fail":
|
|
188
|
+
continue
|
|
189
|
+
usable_studies.append(sid)
|
|
190
|
+
impls = [decision_implication(l) for l in study_link_map.get(sid, [])]
|
|
191
|
+
relation = _study_implication(impls)
|
|
192
|
+
if relation in ("support_adoption", "oppose_adoption"):
|
|
193
|
+
decisive[sid] = relation
|
|
194
|
+
elif relation == "conditional":
|
|
195
|
+
critical_uncertainty_units += 1
|
|
196
|
+
quality_sum += (
|
|
197
|
+
audit.get("design_quality", 0) + audit.get("sample_quality", 0)
|
|
198
|
+
+ audit.get("measurement_validity", 0)
|
|
199
|
+
+ audit.get("temporal_strength", 0)) / 8.0
|
|
200
|
+
dirs = [int(l.get("directness", 0)) for l in study_link_map.get(sid, [])]
|
|
201
|
+
if dirs:
|
|
202
|
+
directness_sum += sum(dirs) / len(dirs) / 2.0
|
|
203
|
+
directness_count += 1
|
|
204
|
+
|
|
205
|
+
n_decisive = len(decisive)
|
|
206
|
+
n_usable = len(usable_studies)
|
|
207
|
+
if n_decisive == 0:
|
|
208
|
+
return {"score": 0.0, "label": "Insufficient",
|
|
209
|
+
"decisive_studies": 0, "usable_studies": n_usable,
|
|
210
|
+
"decisive_relations": {}}
|
|
211
|
+
|
|
212
|
+
quality_term = quality_sum / n_usable if n_usable else 0.0
|
|
213
|
+
counts: dict[str, int] = {}
|
|
214
|
+
for r in decisive.values():
|
|
215
|
+
counts[r] = counts.get(r, 0) + 1
|
|
216
|
+
majority = max(counts.values())
|
|
217
|
+
consistency_term = majority / n_decisive
|
|
218
|
+
directness_term = (directness_sum / directness_count) if directness_count else 0.0
|
|
219
|
+
count_term = min(1.0, n_usable / 4.0)
|
|
220
|
+
conflict = CONFLICT_PENALTY if (counts.get("support_adoption", 0) > 0
|
|
221
|
+
and counts.get("oppose_adoption", 0) > 0) else 0.0
|
|
222
|
+
uncertainty = min(UNCERTAINTY_CAP, UNCERTAINTY_PER_UNIT * critical_uncertainty_units)
|
|
223
|
+
score = (0.30 * quality_term + 0.25 * consistency_term
|
|
224
|
+
+ 0.20 * directness_term + 0.25 * count_term
|
|
225
|
+
- conflict - uncertainty)
|
|
226
|
+
score = max(0.0, min(1.0, score))
|
|
227
|
+
return {"score": round(score, 4), "label": _label(score),
|
|
228
|
+
"decisive_studies": n_decisive, "usable_studies": n_usable,
|
|
229
|
+
"decisive_relations": dict(decisive)}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _has_direct_learning_evidence(store: GraphStore,
|
|
233
|
+
decisive_relations: dict[str, str]) -> bool:
|
|
234
|
+
"""True when at least one decisive support_adoption Study measures a
|
|
235
|
+
learning outcome directly.
|
|
236
|
+
|
|
237
|
+
Learning evidence means the finding's outcome is declared
|
|
238
|
+
outcome_type == "learning" AND its evidence link carries directness == 2.
|
|
239
|
+
Task performance / process / risk outcomes never qualify, and a missing
|
|
240
|
+
outcome record or missing directness fails closed (False).
|
|
241
|
+
"""
|
|
242
|
+
findings = {f["finding_id"]: f for f in store.read_table("findings")}
|
|
243
|
+
outcomes = {o["outcome_id"]: o for o in store.read_table("outcomes")}
|
|
244
|
+
links_by_finding: dict[str, list[dict]] = {}
|
|
245
|
+
for link in store.read_table("evidence_links"):
|
|
246
|
+
links_by_finding.setdefault(link["finding_id"], []).append(link)
|
|
247
|
+
|
|
248
|
+
support_studies = {sid for sid, rel in decisive_relations.items()
|
|
249
|
+
if rel == "support_adoption"}
|
|
250
|
+
if not support_studies:
|
|
251
|
+
return False
|
|
252
|
+
for fid, fnd in findings.items():
|
|
253
|
+
if fnd.get("study_id") not in support_studies:
|
|
254
|
+
continue
|
|
255
|
+
outcome = outcomes.get(fnd.get("outcome_id"))
|
|
256
|
+
if outcome is None or outcome.get("outcome_type") != "learning":
|
|
257
|
+
continue
|
|
258
|
+
for link in links_by_finding.get(fid, []):
|
|
259
|
+
directness = link.get("directness")
|
|
260
|
+
if isinstance(directness, (int, float)) and int(directness) == 2:
|
|
261
|
+
return True
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _decision_action(syn_statuses: dict[str, str], confidence: dict,
|
|
266
|
+
decisive_relations: dict[str, str],
|
|
267
|
+
has_direct_learning_evidence: bool = False) -> str:
|
|
268
|
+
"""Gate-enforced decision action.
|
|
269
|
+
|
|
270
|
+
REJECT requires usable direct opposition evidence (an independent Study
|
|
271
|
+
folded to oppose_adoption). Low/Insufficient can never yield ADOPT.
|
|
272
|
+
ADOPT additionally requires direct learning/transfer evidence: High +
|
|
273
|
+
decisive support WITHOUT a direct learning outcome downgrades to PILOT
|
|
274
|
+
(task performance / procedural efficiency is not learning). Moderate +
|
|
275
|
+
decisive support → PILOT; otherwise INSUFFICIENT_EVIDENCE.
|
|
276
|
+
"""
|
|
277
|
+
label = confidence["label"]
|
|
278
|
+
has_oppose = any(r == "oppose_adoption" for r in decisive_relations.values())
|
|
279
|
+
has_support = any(r == "support_adoption" for r in decisive_relations.values())
|
|
280
|
+
if has_oppose:
|
|
281
|
+
return "REJECT"
|
|
282
|
+
if label == "High" and has_support and has_direct_learning_evidence:
|
|
283
|
+
return "ADOPT"
|
|
284
|
+
if label in ("High", "Moderate") and has_support:
|
|
285
|
+
return "PILOT"
|
|
286
|
+
return "INSUFFICIENT_EVIDENCE"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def adjudicate(store: GraphStore, *, project: ProjectWorkspace,
|
|
293
|
+
claim_syntheses: tuple[ClaimSynthesis, ...] | None = None,
|
|
294
|
+
applicability: dict | None = None,
|
|
295
|
+
policy_versions: dict[str, str] | None = None) -> dict:
|
|
296
|
+
"""Generate a DecisionSnapshot dict (not yet persisted)."""
|
|
297
|
+
syntheses = claim_syntheses if claim_syntheses is not None else synthesize_project(store)
|
|
298
|
+
confidence = _confidence(store, syntheses)
|
|
299
|
+
applicability = applicability or {"boundary": "evidence scope", "notes": ""}
|
|
300
|
+
|
|
301
|
+
syn_statuses = {s.claim_id: s.status for s in syntheses}
|
|
302
|
+
decisive_relations = confidence.get("decisive_relations", {})
|
|
303
|
+
direct_learning = _has_direct_learning_evidence(store, decisive_relations)
|
|
304
|
+
|
|
305
|
+
decision = _decision_action(syn_statuses, confidence, decisive_relations,
|
|
306
|
+
has_direct_learning_evidence=direct_learning)
|
|
307
|
+
|
|
308
|
+
key_links: list[str] = []
|
|
309
|
+
for syn in syntheses:
|
|
310
|
+
key_links.extend(syn.supporting_link_ids)
|
|
311
|
+
key_links.extend(syn.contradicting_link_ids)
|
|
312
|
+
|
|
313
|
+
risks: list[str] = []
|
|
314
|
+
for syn in syntheses:
|
|
315
|
+
risks.extend(syn.unresolved_conflicts)
|
|
316
|
+
if not risks and decision == "ADOPT":
|
|
317
|
+
risks.append("long-term retention/transfer may still be untested")
|
|
318
|
+
|
|
319
|
+
missing: list[str] = []
|
|
320
|
+
for syn in syntheses:
|
|
321
|
+
missing.extend(syn.missing_evidence)
|
|
322
|
+
if not missing:
|
|
323
|
+
missing.append("no explicit knowledge-gap analysis in this snapshot")
|
|
324
|
+
|
|
325
|
+
claim_assessments = {}
|
|
326
|
+
for syn in syntheses:
|
|
327
|
+
claim_assessments[syn.claim_id] = {
|
|
328
|
+
"status": syn.status,
|
|
329
|
+
"independent_studies": len(syn.study_ids),
|
|
330
|
+
"independent_samples": len(syn.independent_sample_keys),
|
|
331
|
+
"supporting_links": list(syn.supporting_link_ids),
|
|
332
|
+
"contradicting_links": list(syn.contradicting_link_ids),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
snapshot = {
|
|
336
|
+
"decision_snapshot_id": new_local_id("DEC", set()),
|
|
337
|
+
"decision": decision,
|
|
338
|
+
"confidence_label": confidence["label"],
|
|
339
|
+
"confidence_score_internal": confidence["score"],
|
|
340
|
+
"claim_assessments": claim_assessments,
|
|
341
|
+
"key_evidence_links": sorted(set(key_links)),
|
|
342
|
+
"key_risks": sorted(set(risks)),
|
|
343
|
+
"applicability_boundary": applicability.get("boundary", ""),
|
|
344
|
+
"missing_evidence": sorted(set(missing)),
|
|
345
|
+
"graph_revision": store.active_revision(),
|
|
346
|
+
"policy_versions": policy_versions or {
|
|
347
|
+
"confidence": CONFIDENCE_POLICY_VERSION,
|
|
348
|
+
"methodology": METHODOLOGY_POLICY_VERSION,
|
|
349
|
+
"source_validation": SOURCE_VALIDATION_POLICY_VERSION,
|
|
350
|
+
},
|
|
351
|
+
"created_at": _now_iso(),
|
|
352
|
+
"extensions": {"confidence_components": {
|
|
353
|
+
"decisive_studies": confidence.get("decisive_studies", 0),
|
|
354
|
+
"usable_studies": confidence.get("usable_studies", 0),
|
|
355
|
+
"has_direct_learning_evidence": direct_learning,
|
|
356
|
+
}},
|
|
357
|
+
}
|
|
358
|
+
errors = validate_record("decision-snapshot", snapshot)
|
|
359
|
+
if errors:
|
|
360
|
+
raise ValueError(f"invalid decision snapshot: {errors}")
|
|
361
|
+
return snapshot
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def save_decision_snapshot(project: ProjectWorkspace, snapshot: dict) -> Path:
|
|
365
|
+
"""Persist a snapshot under decisions/ (immutable; never rewritten)."""
|
|
366
|
+
path = project.path / "decisions" / f"{snapshot['decision_snapshot_id']}.json"
|
|
367
|
+
if path.exists():
|
|
368
|
+
raise FileExistsError(f"decision snapshot already exists: {path}")
|
|
369
|
+
path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n",
|
|
370
|
+
encoding="utf-8")
|
|
371
|
+
return path
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def decision_diff(previous: dict | None, current: dict, *,
|
|
375
|
+
previous_gaps: tuple[dict, ...] = (),
|
|
376
|
+
current_gaps: tuple[dict, ...] = ()) -> dict:
|
|
377
|
+
"""Machine-readable diff between two revision-bound DecisionSnapshots.
|
|
378
|
+
|
|
379
|
+
`resolved_gaps` / `new_gaps` are computed from the GAP inputs, never
|
|
380
|
+
claimed by the report layer. The diff never attributes graph change to
|
|
381
|
+
the report.
|
|
382
|
+
"""
|
|
383
|
+
prev_links = set(previous.get("key_evidence_links", [])) if previous else set()
|
|
384
|
+
cur_links = set(current.get("key_evidence_links", []))
|
|
385
|
+
prev_claims = dict(previous.get("claim_assessments", {})) if previous else {}
|
|
386
|
+
cur_claims = dict(current.get("claim_assessments", {}))
|
|
387
|
+
|
|
388
|
+
prev_gap_ids = {g["gap_id"] for g in previous_gaps}
|
|
389
|
+
cur_gap_ids = {g["gap_id"] for g in current_gaps}
|
|
390
|
+
|
|
391
|
+
changed_claims = []
|
|
392
|
+
if previous:
|
|
393
|
+
for cid in sorted(prev_claims | cur_claims):
|
|
394
|
+
if prev_claims.get(cid) != cur_claims.get(cid):
|
|
395
|
+
changed_claims.append(cid)
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
"from_decision_snapshot_id": (previous or {}).get("decision_snapshot_id"),
|
|
399
|
+
"to_decision_snapshot_id": current.get("decision_snapshot_id"),
|
|
400
|
+
"from_graph_revision": (previous or {}).get("graph_revision"),
|
|
401
|
+
"to_graph_revision": current.get("graph_revision"),
|
|
402
|
+
"action_changed": (previous or {}).get("decision") != current.get("decision"),
|
|
403
|
+
"confidence_changed": (previous or {}).get("confidence_label") != current.get("confidence_label"),
|
|
404
|
+
"changed_claims": changed_claims,
|
|
405
|
+
"new_key_evidence_links": sorted(cur_links - prev_links),
|
|
406
|
+
"resolved_gaps": sorted(prev_gap_ids - cur_gap_ids),
|
|
407
|
+
"new_gaps": sorted(cur_gap_ids - prev_gap_ids),
|
|
408
|
+
}
|