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,240 @@
|
|
|
1
|
+
"""V2 graph projections + V1 compatibility result.json.
|
|
2
|
+
|
|
3
|
+
Projections are read-only views of a graph revision: they never mutate graph
|
|
4
|
+
entities. Outcome aggregation uses Finding.effect_direction; claim trace uses
|
|
5
|
+
EvidenceLink.relation_to_claim; study counts use Study IDs. Compatibility
|
|
6
|
+
`evidence` rows are view rows with explicit finding_id/evidence_link_id in
|
|
7
|
+
extensions so no identity is lost.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from engine.graph_store import GraphStore
|
|
16
|
+
from engine.project import ProjectWorkspace
|
|
17
|
+
from engine.semantics import claim_relation, decision_implication, finding_effect
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_latest_snapshot(project: ProjectWorkspace,
|
|
21
|
+
decision_snapshot_id: str | None) -> dict | None:
|
|
22
|
+
decisions = project.path / "decisions"
|
|
23
|
+
if not decisions.is_dir():
|
|
24
|
+
return None
|
|
25
|
+
if decision_snapshot_id:
|
|
26
|
+
p = decisions / f"{decision_snapshot_id}.json"
|
|
27
|
+
if p.is_file():
|
|
28
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
29
|
+
return None
|
|
30
|
+
snaps = []
|
|
31
|
+
for p in decisions.glob("DEC-*.json"):
|
|
32
|
+
try:
|
|
33
|
+
rec = json.loads(p.read_text(encoding="utf-8"))
|
|
34
|
+
except (json.JSONDecodeError, OSError):
|
|
35
|
+
continue
|
|
36
|
+
snaps.append(rec)
|
|
37
|
+
if not snaps:
|
|
38
|
+
return None
|
|
39
|
+
# latest by created_at, never by filename (DEC ids are random hex)
|
|
40
|
+
return max(snaps, key=lambda r: r.get("created_at", ""))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_gaps(project: ProjectWorkspace) -> list[dict]:
|
|
44
|
+
gaps: list[dict] = []
|
|
45
|
+
gaps_dir = project.path / "gaps"
|
|
46
|
+
if gaps_dir.is_dir():
|
|
47
|
+
for f in sorted(gaps_dir.glob("gaps-rev-*.jsonl")):
|
|
48
|
+
for line in f.read_text(encoding="utf-8").splitlines():
|
|
49
|
+
if line.strip():
|
|
50
|
+
gaps.append(json.loads(line))
|
|
51
|
+
return gaps
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_study_designs(project: ProjectWorkspace) -> list[dict]:
|
|
55
|
+
designs: list[dict] = []
|
|
56
|
+
ddir = project.path / "study-designs"
|
|
57
|
+
if ddir.is_dir():
|
|
58
|
+
for f in sorted(ddir.glob("DSN-*.json")):
|
|
59
|
+
designs.append(json.loads(f.read_text(encoding="utf-8")))
|
|
60
|
+
return designs
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_report_projection(project: ProjectWorkspace, *,
|
|
64
|
+
graph_revision: int | None = None,
|
|
65
|
+
decision_snapshot_id: str | None = None) -> dict:
|
|
66
|
+
"""Project the active (or named) graph revision into a report view."""
|
|
67
|
+
store = GraphStore.create(project)
|
|
68
|
+
revision = graph_revision if graph_revision is not None else store.active_revision()
|
|
69
|
+
if revision != store.active_revision():
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"projection of historical revision {revision} not yet supported; "
|
|
72
|
+
f"active revision is {store.active_revision()}")
|
|
73
|
+
|
|
74
|
+
manifest = project.manifest()
|
|
75
|
+
sources = store.read_table("sources")
|
|
76
|
+
studies = store.read_table("studies")
|
|
77
|
+
findings = store.read_table("findings")
|
|
78
|
+
outcomes = store.read_table("outcomes")
|
|
79
|
+
claims = store.read_table("claims")
|
|
80
|
+
links = store.read_table("evidence_links")
|
|
81
|
+
audits = store.read_table("audits")
|
|
82
|
+
|
|
83
|
+
snapshot = _load_latest_snapshot(project, decision_snapshot_id)
|
|
84
|
+
|
|
85
|
+
# outcome aggregation by effect_direction
|
|
86
|
+
outcome_rows: dict[str, dict] = {}
|
|
87
|
+
for o in outcomes:
|
|
88
|
+
outcome_rows[o["outcome_id"]] = {
|
|
89
|
+
"outcome_id": o["outcome_id"],
|
|
90
|
+
"name": o["name"],
|
|
91
|
+
"outcome_type": o["outcome_type"],
|
|
92
|
+
"positive_count": 0, "negative_count": 0, "null_count": 0,
|
|
93
|
+
"evidence_ids": [],
|
|
94
|
+
}
|
|
95
|
+
for f in findings:
|
|
96
|
+
row = outcome_rows.setdefault(f["outcome_id"], {
|
|
97
|
+
"outcome_id": f["outcome_id"], "name": f.get("measure", f["outcome_id"]),
|
|
98
|
+
"outcome_type": "learning", "positive_count": 0,
|
|
99
|
+
"negative_count": 0, "null_count": 0, "evidence_ids": []})
|
|
100
|
+
direction = finding_effect(f)
|
|
101
|
+
if direction == "positive":
|
|
102
|
+
row["positive_count"] += 1
|
|
103
|
+
elif direction == "negative":
|
|
104
|
+
row["negative_count"] += 1
|
|
105
|
+
else:
|
|
106
|
+
row["null_count"] += 1
|
|
107
|
+
row["evidence_ids"].append(f["finding_id"])
|
|
108
|
+
|
|
109
|
+
# claim trace via EvidenceLink semantics
|
|
110
|
+
claim_rows: list[dict] = []
|
|
111
|
+
for c in claims:
|
|
112
|
+
c_links = [l for l in links if l["claim_id"] == c["claim_id"]]
|
|
113
|
+
claim_rows.append({
|
|
114
|
+
"claim_id": c["claim_id"],
|
|
115
|
+
"claim": c["text"],
|
|
116
|
+
"claim_type": c["claim_type"],
|
|
117
|
+
"outcome_type": c["primary_outcome_ids"][0] if c["primary_outcome_ids"] else None,
|
|
118
|
+
"evidence_ids": [l["finding_id"] for l in c_links],
|
|
119
|
+
"status": c["status"],
|
|
120
|
+
"supporting_links": [l["evidence_link_id"] for l in c_links
|
|
121
|
+
if claim_relation(l) == "support"],
|
|
122
|
+
"contradicting_links": [l["evidence_link_id"] for l in c_links
|
|
123
|
+
if claim_relation(l) == "contradict"],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# evidence view rows (compat) with identity preserved
|
|
127
|
+
study_source: dict[str, str | None] = {}
|
|
128
|
+
for s in studies:
|
|
129
|
+
study_source[s["study_id"]] = s["source_ids"][0] if s.get("source_ids") else None
|
|
130
|
+
evidence_rows = []
|
|
131
|
+
for f in findings:
|
|
132
|
+
f_links = [l for l in links if l["finding_id"] == f["finding_id"]]
|
|
133
|
+
evidence_rows.append({
|
|
134
|
+
"evidence_id": f["finding_id"],
|
|
135
|
+
"source_id": study_source.get(f["study_id"]),
|
|
136
|
+
"study_id": f["study_id"],
|
|
137
|
+
"claim": f.get("raw_result_text", ""),
|
|
138
|
+
"outcome_type": f.get("measure", ""),
|
|
139
|
+
"relation_to_claim": claim_relation(f_links[0]) if f_links else "neutral",
|
|
140
|
+
"effect_direction": finding_effect(f),
|
|
141
|
+
"decision_relation": decision_implication(f_links[0]) if f_links else "neutral",
|
|
142
|
+
"source_location": f.get("source_locator", ""),
|
|
143
|
+
"extensions": {
|
|
144
|
+
"finding_id": f["finding_id"],
|
|
145
|
+
"evidence_link_id": f_links[0]["evidence_link_id"] if f_links else None,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"project_id": project.project_id,
|
|
151
|
+
"graph_revision": revision,
|
|
152
|
+
"decision_snapshot_id": snapshot["decision_snapshot_id"] if snapshot else None,
|
|
153
|
+
"decision": snapshot["decision"] if snapshot else "INSUFFICIENT_EVIDENCE",
|
|
154
|
+
"confidence_label": snapshot["confidence_label"] if snapshot else None,
|
|
155
|
+
"research_frame": {
|
|
156
|
+
"education_question": manifest["question"],
|
|
157
|
+
"research_mode": manifest["research_mode"],
|
|
158
|
+
"decision_target": manifest["decision_target"],
|
|
159
|
+
},
|
|
160
|
+
"counts": {
|
|
161
|
+
"source_count": len(sources),
|
|
162
|
+
"study_count": len(studies),
|
|
163
|
+
"finding_count": len(findings),
|
|
164
|
+
"claim_count": len(claims),
|
|
165
|
+
"evidence_link_count": len(links),
|
|
166
|
+
},
|
|
167
|
+
"sources": sources,
|
|
168
|
+
"studies": studies,
|
|
169
|
+
"findings": findings,
|
|
170
|
+
"evidence_links": links,
|
|
171
|
+
"outcomes": list(outcome_rows.values()),
|
|
172
|
+
"claims": claim_rows,
|
|
173
|
+
"evidence": evidence_rows,
|
|
174
|
+
"methodology_reviews": audits,
|
|
175
|
+
"knowledge_gaps": _load_gaps(project),
|
|
176
|
+
"study_designs": _load_study_designs(project),
|
|
177
|
+
"analysis_provenance": [
|
|
178
|
+
{
|
|
179
|
+
"dataset_id": (s.get("extensions") or {}).get("dataset_id"),
|
|
180
|
+
"design_id": (s.get("extensions") or {}).get("design_id"),
|
|
181
|
+
"analysis_run_id": (s.get("extensions") or {}).get("analysis_run_id"),
|
|
182
|
+
}
|
|
183
|
+
for s in sources if s.get("origin") == "project"
|
|
184
|
+
],
|
|
185
|
+
"provenance": {
|
|
186
|
+
"project_id": project.project_id,
|
|
187
|
+
"graph_revision": revision,
|
|
188
|
+
"projected_at": __import__("datetime").datetime.now(
|
|
189
|
+
__import__("datetime").timezone.utc).isoformat(),
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def build_v1_compat_result(project: ProjectWorkspace, *,
|
|
195
|
+
graph_revision: int | None = None,
|
|
196
|
+
decision_snapshot_id: str | None = None) -> dict:
|
|
197
|
+
"""Map the projection into the V1 result.json shape the renderer expects."""
|
|
198
|
+
proj = build_report_projection(project, graph_revision=graph_revision,
|
|
199
|
+
decision_snapshot_id=decision_snapshot_id)
|
|
200
|
+
return {
|
|
201
|
+
"meta": {"engine_version": "2.0.0", "project_id": proj["project_id"]},
|
|
202
|
+
"execution": {"graph_revision": proj["graph_revision"]},
|
|
203
|
+
"research_frame": proj["research_frame"],
|
|
204
|
+
"decision": {
|
|
205
|
+
"verdict": proj["decision"],
|
|
206
|
+
"confidence": proj["confidence_label"],
|
|
207
|
+
"summary": "",
|
|
208
|
+
},
|
|
209
|
+
"outcomes": proj["outcomes"],
|
|
210
|
+
"claims": proj["claims"],
|
|
211
|
+
"sources": proj["sources"],
|
|
212
|
+
"evidence": proj["evidence"],
|
|
213
|
+
"methodology_reviews": proj["methodology_reviews"],
|
|
214
|
+
"conflicts": [],
|
|
215
|
+
"applicability": {},
|
|
216
|
+
"intervention": {},
|
|
217
|
+
"evaluation": {},
|
|
218
|
+
"knowledge_gaps": proj["knowledge_gaps"],
|
|
219
|
+
"study_designs": proj["study_designs"],
|
|
220
|
+
"analysis_provenance": proj["analysis_provenance"],
|
|
221
|
+
"benchmark": {},
|
|
222
|
+
"provenance": proj["provenance"],
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_localization_pack(projection: dict, *, lang: str,
|
|
227
|
+
localized_text: dict[str, str]) -> dict:
|
|
228
|
+
"""Wrap a projection with a localization layer (language strings)."""
|
|
229
|
+
if lang not in ("zh", "en"):
|
|
230
|
+
raise ValueError(f"unsupported language {lang!r}")
|
|
231
|
+
return {
|
|
232
|
+
"lang": lang,
|
|
233
|
+
"project_id": projection.get("project_id"),
|
|
234
|
+
"graph_revision": projection.get("graph_revision"),
|
|
235
|
+
"decision_snapshot_id": projection.get("decision_snapshot_id"),
|
|
236
|
+
"decision": projection.get("decision"),
|
|
237
|
+
"confidence_label": projection.get("confidence_label"),
|
|
238
|
+
"localized_text": dict(localized_text),
|
|
239
|
+
"counts": projection.get("counts", {}),
|
|
240
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""engine/robustness.py - leave-one-out sensitivity analysis for v4 meta-analysis.
|
|
2
|
+
|
|
3
|
+
leave_one_out re-run a pooling function k times, each time dropping
|
|
4
|
+
one study, and compare every result against the
|
|
5
|
+
full-sample pooled effect.
|
|
6
|
+
robustness_label robust vs fragile: any leave-one-out iteration whose
|
|
7
|
+
pooled direction flips, or whose 95% CI crosses zero,
|
|
8
|
+
marks the synthesis fragile.
|
|
9
|
+
|
|
10
|
+
Pure stdlib, no third-party dependencies.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_number(value: Any) -> bool:
|
|
19
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _usable(rows: list[dict]) -> list[dict]:
|
|
23
|
+
import math as _m
|
|
24
|
+
return [r for r in rows or []
|
|
25
|
+
if _is_number(r.get("d")) and _is_number(r.get("se")) and r["se"] > 0
|
|
26
|
+
and _m.isfinite(float(r["d"])) and _m.isfinite(float(r["se"]))]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _direction(d: float) -> int:
|
|
30
|
+
"""+1 positive, −1 negative, 0 exactly zero."""
|
|
31
|
+
if d > 0:
|
|
32
|
+
return 1
|
|
33
|
+
if d < 0:
|
|
34
|
+
return -1
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ci_crosses_zero(pooled: dict) -> bool:
|
|
39
|
+
return pooled["ci_low"] <= 0.0 <= pooled["ci_high"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def leave_one_out(rows: list[dict],
|
|
43
|
+
pooling_fn: Callable[[list[dict]], dict | None]) -> dict | None:
|
|
44
|
+
"""Re-run ``pooling_fn`` on every k−1 subset; compare to the full pooled.
|
|
45
|
+
|
|
46
|
+
``direction_flip`` is True when the dropped study reverses the pooled
|
|
47
|
+
direction (full-sample effect is nonzero and the subset moves to the
|
|
48
|
+
opposite sign); ``ci_crosses_zero`` when the subset CI spans zero;
|
|
49
|
+
``decision_changed`` = either. Returns None when fewer than two usable
|
|
50
|
+
rows exist.
|
|
51
|
+
"""
|
|
52
|
+
usable = _usable(rows)
|
|
53
|
+
k = len(usable)
|
|
54
|
+
if k < 2:
|
|
55
|
+
return None
|
|
56
|
+
full = pooling_fn(usable)
|
|
57
|
+
if full is None:
|
|
58
|
+
return None
|
|
59
|
+
full_dir = _direction(full["d"])
|
|
60
|
+
iterations = []
|
|
61
|
+
for i, removed in enumerate(usable):
|
|
62
|
+
subset = usable[:i] + usable[i + 1:]
|
|
63
|
+
pooled = pooling_fn(subset)
|
|
64
|
+
if pooled is None:
|
|
65
|
+
continue
|
|
66
|
+
direction_flip = (full_dir != 0
|
|
67
|
+
and _direction(pooled["d"]) != 0
|
|
68
|
+
and _direction(pooled["d"]) != full_dir)
|
|
69
|
+
ci_crosses_zero = _ci_crosses_zero(pooled)
|
|
70
|
+
iterations.append({
|
|
71
|
+
"removed_study_id": removed.get("study_id"),
|
|
72
|
+
"removed_index": i,
|
|
73
|
+
"pooled": pooled,
|
|
74
|
+
"direction_flip": direction_flip,
|
|
75
|
+
"ci_crosses_zero": ci_crosses_zero,
|
|
76
|
+
"decision_changed": direction_flip or ci_crosses_zero,
|
|
77
|
+
})
|
|
78
|
+
return {
|
|
79
|
+
"method": "leave_one_out",
|
|
80
|
+
"k": k,
|
|
81
|
+
"full_pooled": full,
|
|
82
|
+
"iterations": iterations,
|
|
83
|
+
"n_direction_flips": sum(1 for it in iterations if it["direction_flip"]),
|
|
84
|
+
"n_ci_crossings": sum(1 for it in iterations if it["ci_crosses_zero"]),
|
|
85
|
+
"n_decision_changes": sum(1 for it in iterations if it["decision_changed"]),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def robustness_label(results: dict | None) -> str:
|
|
90
|
+
"""robust / fragile classification of a leave-one-out result.
|
|
91
|
+
|
|
92
|
+
Fragile when any dropped study flips the pooled direction or makes the
|
|
93
|
+
CI cross zero; otherwise robust. None (no analysis possible) is fragile.
|
|
94
|
+
"""
|
|
95
|
+
if results is None:
|
|
96
|
+
return "fragile"
|
|
97
|
+
for it in results.get("iterations", []):
|
|
98
|
+
if it.get("direction_flip") or it.get("ci_crosses_zero"):
|
|
99
|
+
return "fragile"
|
|
100
|
+
return "robust"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _mean_std(values: list[float]) -> tuple[float, float]:
|
|
104
|
+
"""Unused helper kept for parity with the textbook SD definition."""
|
|
105
|
+
if not values:
|
|
106
|
+
return 0.0, 0.0
|
|
107
|
+
m = sum(values) / len(values)
|
|
108
|
+
v = sum((x - m) ** 2 for x in values) / (len(values) - 1) if len(values) > 1 else 0.0
|
|
109
|
+
return m, math.sqrt(v)
|
package/engine/run.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""RunRecord lifecycle — one execution attempt or mutation inside a Project.
|
|
2
|
+
|
|
3
|
+
A Run starts at the Project's current graph revision (`graph_revision_before`)
|
|
4
|
+
and, on completion, records `graph_revision_after`. Runs live at
|
|
5
|
+
`runs/<run_id>/run.json`; a second Run never overwrites the first.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from engine.contracts import validate_record
|
|
16
|
+
from engine.ids import new_run_id
|
|
17
|
+
from engine.project import ProjectWorkspace
|
|
18
|
+
from engine.versions import (
|
|
19
|
+
CONFIDENCE_POLICY_VERSION,
|
|
20
|
+
METHODOLOGY_POLICY_VERSION,
|
|
21
|
+
SOURCE_VALIDATION_POLICY_VERSION,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _now_iso() -> str:
|
|
26
|
+
return datetime.now(timezone.utc).isoformat()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _run_path(project: ProjectWorkspace, run_id: str) -> Path:
|
|
30
|
+
return project.runs_dir() / run_id / "run.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _atomic_write_json(path: Path, record: dict) -> None:
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
36
|
+
tmp.write_text(
|
|
37
|
+
json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
38
|
+
)
|
|
39
|
+
os.replace(tmp, path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _check(record: dict) -> None:
|
|
43
|
+
errors = validate_record("run", record)
|
|
44
|
+
if errors:
|
|
45
|
+
raise ValueError(f"invalid run record: {errors}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def start_run(project: ProjectWorkspace, *, purpose: str,
|
|
49
|
+
capabilities: list[str], execution_backend: str) -> dict:
|
|
50
|
+
"""Open a new Run at the Project's current revision."""
|
|
51
|
+
run = {
|
|
52
|
+
"run_id": new_run_id(),
|
|
53
|
+
"project_id": project.project_id,
|
|
54
|
+
"purpose": purpose,
|
|
55
|
+
"started_at": _now_iso(),
|
|
56
|
+
"status": "running",
|
|
57
|
+
"graph_revision_before": project.current_revision(),
|
|
58
|
+
"graph_revision_after": None,
|
|
59
|
+
"capabilities": sorted(set(capabilities)),
|
|
60
|
+
"execution_backend": execution_backend,
|
|
61
|
+
"policy_versions": {
|
|
62
|
+
"source_validation": SOURCE_VALIDATION_POLICY_VERSION,
|
|
63
|
+
"methodology": METHODOLOGY_POLICY_VERSION,
|
|
64
|
+
"confidence": CONFIDENCE_POLICY_VERSION,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
_check(run)
|
|
68
|
+
_atomic_write_json(_run_path(project, run["run_id"]), run)
|
|
69
|
+
return run
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def finish_run(project: ProjectWorkspace, run_id: str, *,
|
|
73
|
+
status: str, graph_revision_after: int) -> dict:
|
|
74
|
+
"""Close a Run with its final status and end revision."""
|
|
75
|
+
path = _run_path(project, run_id)
|
|
76
|
+
if not path.is_file():
|
|
77
|
+
raise FileNotFoundError(f"run {run_id!r} not found in project {project.project_id}")
|
|
78
|
+
run = json.loads(path.read_text(encoding="utf-8"))
|
|
79
|
+
if run["status"] != "running":
|
|
80
|
+
raise ValueError(f"run {run_id} is already {run['status']!r}; cannot finish twice")
|
|
81
|
+
run["status"] = status
|
|
82
|
+
run["graph_revision_after"] = graph_revision_after
|
|
83
|
+
_check(run)
|
|
84
|
+
_atomic_write_json(path, run)
|
|
85
|
+
return run
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""engine/semantics.py — Centralized V2 & V4/V5 semantics and Outcome Ontology.
|
|
2
|
+
|
|
3
|
+
Combines:
|
|
4
|
+
1. V2 Direction & Implication Semantics (finding_effect, claim_relation, decision_implication)
|
|
5
|
+
2. V5 Social Science Outcome Ontology & OutcomeClassifier (OutcomeDimension, OutcomeClassifier)
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
# --- V2 Direction & Relation Constants ---
|
|
12
|
+
FINDING_EFFECTS = ("positive", "negative", "null", "mixed", "not_applicable")
|
|
13
|
+
CLAIM_RELATIONS = ("support", "contradict", "neutral")
|
|
14
|
+
DECISION_IMPLICATIONS = ("support_adoption", "oppose_adoption", "conditional", "neutral")
|
|
15
|
+
|
|
16
|
+
_RELATION_TO_IMPLICATION = {
|
|
17
|
+
"support": "support_adoption",
|
|
18
|
+
"contradict": "oppose_adoption",
|
|
19
|
+
"neutral": "neutral",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def finding_effect(finding: dict) -> str:
|
|
24
|
+
"""Observed effect direction of a V2 Finding."""
|
|
25
|
+
effect = finding.get("effect_direction")
|
|
26
|
+
if effect in FINDING_EFFECTS:
|
|
27
|
+
return effect
|
|
28
|
+
return "null"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def claim_relation(link: dict) -> str:
|
|
32
|
+
"""Relation of an EvidenceLink to the Claim it binds."""
|
|
33
|
+
relation = link.get("relation_to_claim")
|
|
34
|
+
if relation in CLAIM_RELATIONS:
|
|
35
|
+
return relation
|
|
36
|
+
return "neutral"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decision_implication(link: dict) -> str:
|
|
40
|
+
"""Implication of an EvidenceLink for the current teaching decision."""
|
|
41
|
+
implication = link.get("decision_implication")
|
|
42
|
+
if implication in DECISION_IMPLICATIONS:
|
|
43
|
+
return implication
|
|
44
|
+
return _RELATION_TO_IMPLICATION[claim_relation(link)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def independent_study_ids(findings: list[dict]) -> set[str]:
|
|
48
|
+
"""Unique Study IDs behind a set of Findings (independent-study counting)."""
|
|
49
|
+
return {f["study_id"] for f in findings if f.get("study_id")}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def independent_sample_keys(studies: list[dict]) -> set[str]:
|
|
53
|
+
"""Unique independence keys across Studies (independent sample counting)."""
|
|
54
|
+
keys = {s["independence_key"] for s in studies if s.get("independence_key")}
|
|
55
|
+
return keys
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def graph_counts(store) -> dict[str, int]:
|
|
59
|
+
"""Entity counts of the active graph revision."""
|
|
60
|
+
return {
|
|
61
|
+
"source_count": len(store.read_table("sources")),
|
|
62
|
+
"study_count": len(store.read_table("studies")),
|
|
63
|
+
"finding_count": len(store.read_table("findings")),
|
|
64
|
+
"outcome_count": len(store.read_table("outcomes")),
|
|
65
|
+
"claim_count": len(store.read_table("claims")),
|
|
66
|
+
"evidence_link_count": len(store.read_table("evidence_links")),
|
|
67
|
+
"audit_count": len(store.read_table("audits")),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --- V5 Outcome Ontology & Dimension Classifier ---
|
|
72
|
+
|
|
73
|
+
class OutcomeDimension:
|
|
74
|
+
PROCEDURAL_EFFICIENCY = "PROCEDURAL_EFFICIENCY" # Speed, task velocity, immediate completion during intervention
|
|
75
|
+
CONCEPTUAL_MASTERY = "CONCEPTUAL_MASTERY" # Deep understanding, mental model construction, reasoning
|
|
76
|
+
INDEPENDENT_TRANSFER = "INDEPENDENT_TRANSFER" # Delayed retention, unassisted solo closed-book transfer
|
|
77
|
+
AFFECTIVE_PSYCHOSOCIAL = "AFFECTIVE_PSYCHOSOCIAL" # Anxiety, engagement, self-efficacy, motivation
|
|
78
|
+
SOCIOECONOMIC_POLICY = "SOCIOECONOMIC_POLICY" # Family expenditure, equity gap, resource allocation
|
|
79
|
+
GENERAL_MEASURE = "GENERAL_MEASURE"
|
|
80
|
+
|
|
81
|
+
ALL = [
|
|
82
|
+
PROCEDURAL_EFFICIENCY,
|
|
83
|
+
CONCEPTUAL_MASTERY,
|
|
84
|
+
INDEPENDENT_TRANSFER,
|
|
85
|
+
AFFECTIVE_PSYCHOSOCIAL,
|
|
86
|
+
SOCIOECONOMIC_POLICY,
|
|
87
|
+
GENERAL_MEASURE,
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class OutcomeClassifier:
|
|
92
|
+
"""Classifies outcome metric descriptions into canonical social science dimensions."""
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def classify(metric_text: str) -> str:
|
|
96
|
+
if not metric_text:
|
|
97
|
+
return OutcomeDimension.GENERAL_MEASURE
|
|
98
|
+
|
|
99
|
+
t = metric_text.lower()
|
|
100
|
+
# 1. Independent Transfer & Delayed Retention (Highest Priority to detect Scaffolding Traps)
|
|
101
|
+
if any(k in t for k in [
|
|
102
|
+
"transfer", "retention", "delayed", "exam", "solo", "unassisted", "no-ai",
|
|
103
|
+
"post-test", "follow-up", "long-term", "persistence",
|
|
104
|
+
"迁移", "闭卷", "期末", "长期", "留存", "保持", "无ai测试", "独立解题", "手写代码"
|
|
105
|
+
]):
|
|
106
|
+
return OutcomeDimension.INDEPENDENT_TRANSFER
|
|
107
|
+
# 2. Conceptual Mastery & Deep Reasoning
|
|
108
|
+
elif any(k in t for k in [
|
|
109
|
+
"thinking", "conceptual", "problem solving", "reasoning", "comprehension",
|
|
110
|
+
"mental model", "algorithmic", "abstraction", "debugging strategy",
|
|
111
|
+
"思维", "概念", "架构", "认知", "问题解决", "算法理解", "心智模型", "调试策略"
|
|
112
|
+
]):
|
|
113
|
+
return OutcomeDimension.CONCEPTUAL_MASTERY
|
|
114
|
+
# 3. Procedural Efficiency & In-task velocity
|
|
115
|
+
elif any(k in t for k in [
|
|
116
|
+
"speed", "velocity", "completion", "time", "procedural", "task performance",
|
|
117
|
+
"accuracy", "efficiency", "correctness", "syntax", "lines of code", "loc",
|
|
118
|
+
"速度", "耗时", "效率", "作业完成", "完成时间", "语法正确", "即时准确率", "代码量"
|
|
119
|
+
]):
|
|
120
|
+
return OutcomeDimension.PROCEDURAL_EFFICIENCY
|
|
121
|
+
# 4. Affective & Psychosocial
|
|
122
|
+
elif any(k in t for k in [
|
|
123
|
+
"anxiety", "engagement", "interest", "efficacy", "collaboration", "motivation",
|
|
124
|
+
"confidence", "self-regulation", "frustration",
|
|
125
|
+
"焦虑", "投入", "效能", "协作", "动机", "自信", "自我调节", "挫败感", "学习兴趣"
|
|
126
|
+
]):
|
|
127
|
+
return OutcomeDimension.AFFECTIVE_PSYCHOSOCIAL
|
|
128
|
+
# 5. Socioeconomic & Policy
|
|
129
|
+
elif any(k in t for k in [
|
|
130
|
+
"expenditure", "cost", "equity", "burden", "socioeconomic", "disparity",
|
|
131
|
+
"支出", "负担", "公平", "成本", "数字鸿沟", "社会经济"
|
|
132
|
+
]):
|
|
133
|
+
return OutcomeDimension.SOCIOECONOMIC_POLICY
|
|
134
|
+
|
|
135
|
+
return OutcomeDimension.GENERAL_MEASURE
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Evidence-grounded StudyDesign gate + persistence.
|
|
2
|
+
|
|
3
|
+
A StudyDesign may only be created from explicit KnowledgeGap entities that
|
|
4
|
+
exist in THIS project (same project_id) and were derived from the current
|
|
5
|
+
graph revision. The engine validates grounding and stores designs; it does
|
|
6
|
+
not itself claim ethics approval — human-subject flags are surfaced, and
|
|
7
|
+
institutional review remains the institution's decision.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from engine.contracts import validate_record
|
|
17
|
+
from engine.project import ProjectWorkspace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _now_iso() -> str:
|
|
21
|
+
return datetime.now(timezone.utc).isoformat()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_design_grounding(project: ProjectWorkspace, design: dict) -> list[str]:
|
|
25
|
+
"""Validate that the design references real, project-local GAP ids.
|
|
26
|
+
|
|
27
|
+
Returns a list of error strings (empty == valid).
|
|
28
|
+
"""
|
|
29
|
+
errors: list[str] = []
|
|
30
|
+
# grounding gate first: an empty gap list is a grounding failure with a
|
|
31
|
+
# clear message, not just a schema minItems violation
|
|
32
|
+
if not design.get("gap_ids"):
|
|
33
|
+
return ["gap_ids must be non-empty: no new study design without "
|
|
34
|
+
"evidence grounding"]
|
|
35
|
+
schema_errors = validate_record("study-design", design)
|
|
36
|
+
if schema_errors:
|
|
37
|
+
errors.extend(schema_errors)
|
|
38
|
+
return errors
|
|
39
|
+
|
|
40
|
+
gap_ids = design["gap_ids"]
|
|
41
|
+
|
|
42
|
+
# load this project's gaps (all revisions; the gap file records revision)
|
|
43
|
+
gaps_dir = project.path / "gaps"
|
|
44
|
+
known_gap_ids: set[str] = set()
|
|
45
|
+
if gaps_dir.is_dir():
|
|
46
|
+
for f in sorted(gaps_dir.glob("gaps-rev-*.jsonl")):
|
|
47
|
+
for line in f.read_text(encoding="utf-8").splitlines():
|
|
48
|
+
if line.strip():
|
|
49
|
+
rec = json.loads(line)
|
|
50
|
+
known_gap_ids.add(rec["gap_id"])
|
|
51
|
+
# gap must be derived from the current graph revision
|
|
52
|
+
if rec["gap_id"] in gap_ids and rec.get(
|
|
53
|
+
"derived_from_graph_revision") != project.current_revision():
|
|
54
|
+
errors.append(
|
|
55
|
+
f"gap {rec['gap_id']} derives from revision "
|
|
56
|
+
f"{rec.get('derived_from_graph_revision')} but the "
|
|
57
|
+
f"project is at revision {project.current_revision()}")
|
|
58
|
+
missing = [g for g in gap_ids if g not in known_gap_ids]
|
|
59
|
+
if missing:
|
|
60
|
+
errors.append(f"design references unknown gaps: {missing}")
|
|
61
|
+
return errors
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_study_design(project: ProjectWorkspace, design: dict) -> Path:
|
|
65
|
+
"""Persist a StudyDesign under study-designs/ (never rewritten)."""
|
|
66
|
+
errors = validate_design_grounding(project, design)
|
|
67
|
+
if errors:
|
|
68
|
+
raise ValueError("design fails grounding gate:\n- " + "\n- ".join(errors))
|
|
69
|
+
path = project.path / "study-designs" / f"{design['design_id']}.json"
|
|
70
|
+
if path.exists():
|
|
71
|
+
raise FileExistsError(f"study design already exists: {path}")
|
|
72
|
+
path.write_text(json.dumps(design, ensure_ascii=False, indent=2) + "\n",
|
|
73
|
+
encoding="utf-8")
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def save_analysis_plan(project: ProjectWorkspace, plan: dict) -> Path:
|
|
78
|
+
"""Persist an AnalysisPlan under study-designs/ (or analyses/)."""
|
|
79
|
+
errors = validate_record("analysis-plan", plan)
|
|
80
|
+
if errors:
|
|
81
|
+
raise ValueError(f"invalid analysis plan: {errors}")
|
|
82
|
+
path = project.path / "analyses" / f"{plan['analysis_plan_id']}.json"
|
|
83
|
+
if path.exists():
|
|
84
|
+
raise FileExistsError(f"analysis plan already exists: {path}")
|
|
85
|
+
path.write_text(json.dumps(plan, ensure_ascii=False, indent=2) + "\n",
|
|
86
|
+
encoding="utf-8")
|
|
87
|
+
return path
|