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
package/engine/pilot.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""engine/pilot.py - Decision-to-Outcome Loop (v3).
|
|
2
|
+
|
|
3
|
+
Closes the v2 Full Research Cycle: a PILOT DecisionSnapshot -> PilotRun
|
|
4
|
+
registration -> outcome data import (provenance-safe, PII-blocked) -> analysis
|
|
5
|
+
link -> graph revision with pilot evidence -> tribunal re-adjudication ->
|
|
6
|
+
new DecisionSnapshot + machine-readable diff.
|
|
7
|
+
|
|
8
|
+
Privacy discipline (matches engine/datasets.py): student data stays local,
|
|
9
|
+
PII columns (names / student ids / emails / phones) are refused at import.
|
|
10
|
+
|
|
11
|
+
Contracts: schemas/v3/pilot-outcome.schema.json for the pilot record;
|
|
12
|
+
graph entities follow schemas/v2/*.json (study/finding/evidence-link/source).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from engine.datasets import analysis_blocked_by_privacy, derive_csv_profile, ingest_dataset
|
|
22
|
+
from engine.graph_store import GraphMutation, GraphStore
|
|
23
|
+
from engine.ids import new_local_id, new_run_id
|
|
24
|
+
from engine.project import ProjectWorkspace
|
|
25
|
+
from engine.synthesis import synthesize_project
|
|
26
|
+
from engine.tribunal import adjudicate, decision_diff, save_decision_snapshot
|
|
27
|
+
from engine.versions import (
|
|
28
|
+
CONFIDENCE_POLICY_VERSION,
|
|
29
|
+
METHODOLOGY_POLICY_VERSION,
|
|
30
|
+
SOURCE_VALIDATION_POLICY_VERSION,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
#: Outcome Taxonomy tokens that pilots may measure (mirrors outcome-taxonomy.md).
|
|
34
|
+
OUTCOME_TAXONOMY = {
|
|
35
|
+
"knowledge_gain", "concept_understanding", "retention", "transfer",
|
|
36
|
+
"independent_problem_solving", "completion_time", "accuracy",
|
|
37
|
+
"code_quality", "assignment_score", "engagement", "motivation",
|
|
38
|
+
"cognitive_load", "help_seeking", "metacognition", "ai_dependency",
|
|
39
|
+
"over_reliance", "reduced_effort", "reduced_transfer",
|
|
40
|
+
"academic_integrity_risk", "false_confidence",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
PILOT_STATUSES = ("registered", "data_imported", "analyzed", "adjudicated")
|
|
44
|
+
|
|
45
|
+
#: Column names that reveal individuals; any match blocks pilot data import.
|
|
46
|
+
PII_COLUMN_HINTS = ("name", "student", "学号", "姓名", "email", "mail",
|
|
47
|
+
"phone", "手机", "电话", "id_card", "身份证", "address", "地址")
|
|
48
|
+
|
|
49
|
+
_DECISION_IMPLICATION = {"support": "support_adoption",
|
|
50
|
+
"contradict": "oppose_adoption", "neutral": "neutral"}
|
|
51
|
+
|
|
52
|
+
#: Outcome Taxonomy token -> graph outcome category enum (schemas/v2/outcome).
|
|
53
|
+
_OUTCOME_CATEGORY = {
|
|
54
|
+
"knowledge_gain": "learning", "concept_understanding": "learning",
|
|
55
|
+
"retention": "learning", "transfer": "learning",
|
|
56
|
+
"independent_problem_solving": "learning",
|
|
57
|
+
"completion_time": "task_performance", "accuracy": "task_performance",
|
|
58
|
+
"code_quality": "task_performance", "assignment_score": "task_performance",
|
|
59
|
+
"engagement": "process", "motivation": "process",
|
|
60
|
+
"cognitive_load": "process", "help_seeking": "process",
|
|
61
|
+
"metacognition": "process",
|
|
62
|
+
"ai_dependency": "risk", "over_reliance": "risk",
|
|
63
|
+
"reduced_effort": "risk", "reduced_transfer": "risk",
|
|
64
|
+
"academic_integrity_risk": "risk", "false_confidence": "risk",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _now_iso() -> str:
|
|
69
|
+
return datetime.now(timezone.utc).isoformat()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pilot_path(project: ProjectWorkspace, pilot_id: str) -> Path:
|
|
73
|
+
return project.path / "pilots" / f"{pilot_id}.json"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_pilot(project: ProjectWorkspace, pilot_id: str) -> dict:
|
|
77
|
+
path = _pilot_path(project, pilot_id)
|
|
78
|
+
if not path.is_file():
|
|
79
|
+
raise FileNotFoundError(f"pilot not found: {pilot_id} (missing {path})")
|
|
80
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _save_pilot(project: ProjectWorkspace, pilot: dict) -> Path:
|
|
84
|
+
from scripts.validate_schema import SchemaError, validate # noqa: PLC0415
|
|
85
|
+
|
|
86
|
+
schema_path = (Path(__file__).resolve().parent.parent / "schemas" / "v3"
|
|
87
|
+
/ "pilot-outcome.schema.json")
|
|
88
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
89
|
+
try:
|
|
90
|
+
validate(pilot, schema)
|
|
91
|
+
except SchemaError as exc:
|
|
92
|
+
raise ValueError(f"invalid pilot record: {exc}") from exc
|
|
93
|
+
path = _pilot_path(project, pilot["pilot_id"])
|
|
94
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
tmp = path.with_suffix(".json.tmp")
|
|
96
|
+
tmp.write_text(json.dumps(pilot, ensure_ascii=False, indent=2) + "\n",
|
|
97
|
+
encoding="utf-8")
|
|
98
|
+
tmp.replace(path)
|
|
99
|
+
return path
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def register_pilot(project: ProjectWorkspace, *,
|
|
103
|
+
decision_snapshot_id: str, title: str,
|
|
104
|
+
start_date: str, end_date: str,
|
|
105
|
+
conditions: list[str], sample_size: int, design_id: str,
|
|
106
|
+
anon_policy: dict, outcome_columns: list[str]) -> dict:
|
|
107
|
+
"""Register a PilotRun bound to an existing DecisionSnapshot."""
|
|
108
|
+
decisions_dir = project.path / "decisions"
|
|
109
|
+
if not (decisions_dir / f"{decision_snapshot_id}.json").is_file():
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"decision snapshot {decision_snapshot_id} not found in this project; "
|
|
112
|
+
"a pilot must bind to a real adjudication")
|
|
113
|
+
unknown = [o for o in outcome_columns if o not in OUTCOME_TAXONOMY]
|
|
114
|
+
if unknown:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"outcome_columns outside Outcome Taxonomy: {sorted(unknown)}")
|
|
117
|
+
if not conditions or sample_size < 1:
|
|
118
|
+
raise ValueError("conditions must be non-empty and sample_size >= 1")
|
|
119
|
+
if anon_policy.get("no_pii_columns") is not True:
|
|
120
|
+
raise ValueError("anon_policy.no_pii_columns must be true (student data stays local)")
|
|
121
|
+
|
|
122
|
+
existing = {p.stem for p in (project.path / "pilots").glob("PIL-*.json")} \
|
|
123
|
+
if (project.path / "pilots").is_dir() else set()
|
|
124
|
+
pilot = {
|
|
125
|
+
"pilot_id": new_local_id("PIL", existing),
|
|
126
|
+
"project_id": project.project_id,
|
|
127
|
+
"decision_snapshot_id": decision_snapshot_id,
|
|
128
|
+
"title": title,
|
|
129
|
+
"start_date": start_date,
|
|
130
|
+
"end_date": end_date,
|
|
131
|
+
"conditions": list(conditions),
|
|
132
|
+
"sample_size": int(sample_size),
|
|
133
|
+
"design_id": design_id,
|
|
134
|
+
"anon_policy": dict(anon_policy),
|
|
135
|
+
"outcome_columns": list(outcome_columns),
|
|
136
|
+
"dataset_asset_id": None,
|
|
137
|
+
"analysis_run_id": None,
|
|
138
|
+
"status": "registered",
|
|
139
|
+
"redecide": None,
|
|
140
|
+
"created_at": _now_iso(),
|
|
141
|
+
"extensions": {},
|
|
142
|
+
}
|
|
143
|
+
_save_pilot(project, pilot)
|
|
144
|
+
return pilot
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def import_outcomes(project: ProjectWorkspace, pilot_id: str, *,
|
|
148
|
+
source_path: Path, privacy: dict,
|
|
149
|
+
variable_dictionary: dict[str, str] | None = None) -> dict:
|
|
150
|
+
"""Ingest pilot outcome data (CSV) with a PII column gate.
|
|
151
|
+
|
|
152
|
+
Blocks: PII column names, missing outcome columns, and datasets whose
|
|
153
|
+
deidentification requirements are unmet.
|
|
154
|
+
"""
|
|
155
|
+
pilot = _load_pilot(project, pilot_id)
|
|
156
|
+
if pilot["status"] not in ("registered", "data_imported"):
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"pilot {pilot_id} status {pilot['status']!r} cannot import outcomes")
|
|
159
|
+
|
|
160
|
+
source_path = Path(source_path)
|
|
161
|
+
if not source_path.is_file():
|
|
162
|
+
raise FileNotFoundError(f"outcome data missing: {source_path}")
|
|
163
|
+
|
|
164
|
+
profile = derive_csv_profile(source_path)
|
|
165
|
+
columns = list((profile.get("missingness") or {}).keys())
|
|
166
|
+
lower = {str(c).strip().lower(): str(c).strip() for c in columns}
|
|
167
|
+
|
|
168
|
+
blocked = [c for c in lower if any(hint in c for hint in PII_COLUMN_HINTS)]
|
|
169
|
+
if blocked:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"PII columns detected and refused: " + ", ".join(sorted(blocked)) +
|
|
172
|
+
"; deidentify the file before import")
|
|
173
|
+
|
|
174
|
+
missing = [o for o in pilot["outcome_columns"]
|
|
175
|
+
if o.lower() not in lower and o not in columns]
|
|
176
|
+
if missing:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"outcome column(s) missing from CSV: {missing}; available: {sorted(columns)}")
|
|
179
|
+
|
|
180
|
+
asset = ingest_dataset(project, design_id=pilot["design_id"],
|
|
181
|
+
source_path=source_path, privacy=privacy,
|
|
182
|
+
variable_dictionary=variable_dictionary)
|
|
183
|
+
privacy_reasons = analysis_blocked_by_privacy(asset)
|
|
184
|
+
if privacy_reasons:
|
|
185
|
+
raise ValueError("pilot data blocked from analysis:\n- " +
|
|
186
|
+
"\n- ".join(privacy_reasons))
|
|
187
|
+
|
|
188
|
+
pilot["dataset_asset_id"] = asset["dataset_id"]
|
|
189
|
+
pilot["status"] = "data_imported"
|
|
190
|
+
_save_pilot(project, pilot)
|
|
191
|
+
return asset
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def link_analysis(project: ProjectWorkspace, pilot_id: str, *,
|
|
195
|
+
analysis_run_id: str) -> dict:
|
|
196
|
+
"""Link a completed analysis run to the pilot (status -> analyzed)."""
|
|
197
|
+
pilot = _load_pilot(project, pilot_id)
|
|
198
|
+
if pilot["status"] not in ("data_imported", "analyzed"):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"pilot {pilot_id} must import data before linking an analysis")
|
|
201
|
+
run_path = project.path / "analyses" / f"{analysis_run_id}.json"
|
|
202
|
+
if not run_path.is_file():
|
|
203
|
+
raise ValueError(f"analysis run not found: {analysis_run_id}")
|
|
204
|
+
pilot["analysis_run_id"] = analysis_run_id
|
|
205
|
+
pilot["status"] = "analyzed"
|
|
206
|
+
_save_pilot(project, pilot)
|
|
207
|
+
return pilot
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _ensure_outcome(store: GraphStore, outcome_id: str) -> dict:
|
|
211
|
+
existing = store.get("outcomes", outcome_id)
|
|
212
|
+
if existing:
|
|
213
|
+
return existing
|
|
214
|
+
token = outcome_id[len("OUT-"):]
|
|
215
|
+
return {
|
|
216
|
+
"outcome_id": outcome_id,
|
|
217
|
+
"name": token,
|
|
218
|
+
"outcome_type": _OUTCOME_CATEGORY.get(token, "learning"),
|
|
219
|
+
"extensions": {"pilot_outcome": True},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def redecide(project: ProjectWorkspace, pilot_id: str, *,
|
|
224
|
+
claim_id: str, outcome_token: str, measure: str,
|
|
225
|
+
effect_direction: str, raw_result_text: str,
|
|
226
|
+
relation_to_claim: str, effect_estimate: dict | None = None,
|
|
227
|
+
directness: int = 2, reasoning_note: str = "") -> dict:
|
|
228
|
+
"""Fold pilot outcome evidence into the graph and re-adjudicate.
|
|
229
|
+
|
|
230
|
+
Creates (or reuses) a project-origin source + study for the pilot,
|
|
231
|
+
upserts one finding + evidence link, commits a new graph revision,
|
|
232
|
+
runs the tribunal and writes a new DecisionSnapshot with a diff against
|
|
233
|
+
the decision the pilot was bound to.
|
|
234
|
+
"""
|
|
235
|
+
if effect_direction not in ("positive", "negative", "null"):
|
|
236
|
+
raise ValueError(f"invalid effect_direction {effect_direction!r}")
|
|
237
|
+
if relation_to_claim not in ("support", "contradict", "neutral"):
|
|
238
|
+
raise ValueError(f"invalid relation_to_claim {relation_to_claim!r}")
|
|
239
|
+
if outcome_token not in OUTCOME_TAXONOMY:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"outcome_token {outcome_token!r} outside Outcome Taxonomy")
|
|
242
|
+
outcome_id = f"OUT-{outcome_token}"
|
|
243
|
+
pilot = _load_pilot(project, pilot_id)
|
|
244
|
+
if pilot["status"] not in ("analyzed", "data_imported"):
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"pilot {pilot_id} status {pilot['status']!r}; import data (and ideally "
|
|
247
|
+
"link an analysis) before re-adjudication")
|
|
248
|
+
# Idempotency guard (final review P1-3): a pilot that already produced a
|
|
249
|
+
# new DecisionSnapshot must never be re-adjudicated — a retry after a
|
|
250
|
+
# partial failure would silently duplicate pilot evidence in the graph.
|
|
251
|
+
if pilot.get("redecide") is not None:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"pilot {pilot_id} already adjudicated into "
|
|
254
|
+
f"{pilot['redecide']['new_decision_snapshot_id']}; refusing re-entry "
|
|
255
|
+
"(create a new pilot for a new cycle)")
|
|
256
|
+
|
|
257
|
+
store = GraphStore(project)
|
|
258
|
+
claims = {c["claim_id"]: c for c in store.read_table("claims")}
|
|
259
|
+
if claim_id not in claims:
|
|
260
|
+
raise ValueError(f"claim {claim_id} not found in project graph")
|
|
261
|
+
|
|
262
|
+
source_id = new_local_id("SRC", {s["source_id"] for s in store.read_table("sources")})
|
|
263
|
+
source = {
|
|
264
|
+
"source_id": source_id,
|
|
265
|
+
"origin": "project",
|
|
266
|
+
"source_type": "dataset",
|
|
267
|
+
"canonical_locator": f"project:pilot/{pilot_id}",
|
|
268
|
+
"validation_status": "valid",
|
|
269
|
+
"content_hash": None,
|
|
270
|
+
"extensions": {"pilot_id": pilot_id,
|
|
271
|
+
"dataset_asset_id": pilot.get("dataset_asset_id")},
|
|
272
|
+
}
|
|
273
|
+
study_id = new_local_id("STU", {s["study_id"] for s in store.read_table("studies")})
|
|
274
|
+
study = {
|
|
275
|
+
"study_id": study_id,
|
|
276
|
+
"source_ids": [source_id],
|
|
277
|
+
"study_design": "pilot",
|
|
278
|
+
"population": f"pilot cohort (n={pilot['sample_size']})",
|
|
279
|
+
"sample_ids": [f"PILOT-{pilot['pilot_id']}"],
|
|
280
|
+
"independence_key": f"pilot:{pilot['pilot_id']}",
|
|
281
|
+
"identity_status": "resolved",
|
|
282
|
+
"extensions": {"pilot_id": pilot_id},
|
|
283
|
+
}
|
|
284
|
+
outcome = _ensure_outcome(store, outcome_id)
|
|
285
|
+
estimate = None
|
|
286
|
+
if effect_estimate is not None:
|
|
287
|
+
estimate = {
|
|
288
|
+
"metric": measure,
|
|
289
|
+
"value": effect_estimate.get("value"),
|
|
290
|
+
"raw_text": raw_result_text,
|
|
291
|
+
}
|
|
292
|
+
finding_id = new_local_id("FND", {f["finding_id"] for f in store.read_table("findings")})
|
|
293
|
+
finding = {
|
|
294
|
+
"finding_id": finding_id,
|
|
295
|
+
"study_id": study_id,
|
|
296
|
+
"finding_type": "quantitative_effect" if estimate else "descriptive",
|
|
297
|
+
"outcome_id": outcome_id,
|
|
298
|
+
"measure": measure,
|
|
299
|
+
"timepoint": pilot.get("end_date"),
|
|
300
|
+
"effect_direction": effect_direction,
|
|
301
|
+
"effect_estimate": estimate,
|
|
302
|
+
"raw_result_text": raw_result_text,
|
|
303
|
+
"source_locator": source["canonical_locator"],
|
|
304
|
+
"extensions": {"pilot_id": pilot_id,
|
|
305
|
+
"analysis_run_id": pilot.get("analysis_run_id")},
|
|
306
|
+
}
|
|
307
|
+
link_id = new_local_id("LNK", {l["evidence_link_id"] for l in store.read_table("evidence_links")})
|
|
308
|
+
link = {
|
|
309
|
+
"evidence_link_id": link_id,
|
|
310
|
+
"finding_id": finding_id,
|
|
311
|
+
"claim_id": claim_id,
|
|
312
|
+
"relation_to_claim": relation_to_claim,
|
|
313
|
+
"decision_implication": _DECISION_IMPLICATION[relation_to_claim],
|
|
314
|
+
"directness": directness,
|
|
315
|
+
"applicability": {"scope_match": "direct",
|
|
316
|
+
"target_population": f"pilot cohort (n={pilot['sample_size']})",
|
|
317
|
+
"context_notes": f"pilot {pilot_id}: {pilot['title']}"},
|
|
318
|
+
"reasoning_note": reasoning_note or f"pilot outcomes: {pilot['title']}",
|
|
319
|
+
"created_in_revision": store.active_revision() + 1,
|
|
320
|
+
"extensions": {"pilot_id": pilot_id},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
mutation = GraphMutation(upserts={
|
|
324
|
+
"sources": [source],
|
|
325
|
+
"studies": [study],
|
|
326
|
+
"outcomes": [outcome] if store.get("outcomes", outcome_id) is None else [],
|
|
327
|
+
"findings": [finding],
|
|
328
|
+
"evidence_links": [link],
|
|
329
|
+
})
|
|
330
|
+
revision = store.commit(
|
|
331
|
+
run_id=new_run_id(), reason=f"pilot outcomes re-adjudication: {pilot_id}",
|
|
332
|
+
mutation=mutation)
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
store.repair_head_mirror()
|
|
336
|
+
syntheses = synthesize_project(store)
|
|
337
|
+
snapshot = adjudicate(
|
|
338
|
+
store, project=project, claim_syntheses=syntheses,
|
|
339
|
+
policy_versions={"confidence": CONFIDENCE_POLICY_VERSION,
|
|
340
|
+
"methodology": METHODOLOGY_POLICY_VERSION,
|
|
341
|
+
"source_validation": SOURCE_VALIDATION_POLICY_VERSION})
|
|
342
|
+
path = save_decision_snapshot(project, snapshot)
|
|
343
|
+
|
|
344
|
+
previous = None
|
|
345
|
+
prev_path = project.path / "decisions" / f"{pilot['decision_snapshot_id']}.json"
|
|
346
|
+
if prev_path.is_file():
|
|
347
|
+
previous = json.loads(prev_path.read_text(encoding="utf-8"))
|
|
348
|
+
diff = decision_diff(previous, snapshot)
|
|
349
|
+
except Exception as exc: # noqa: BLE001 - graph committed, mark failure for diagnosis
|
|
350
|
+
# The graph revision is already committed; never let the pilot record
|
|
351
|
+
# claim success. Record the failure so the state machine is
|
|
352
|
+
# diagnosable and a human can recover (P1-3).
|
|
353
|
+
pilot.setdefault("extensions", {})["redecide_failed"] = {
|
|
354
|
+
"graph_revision": revision.revision,
|
|
355
|
+
"error": str(exc),
|
|
356
|
+
}
|
|
357
|
+
_save_pilot(project, pilot)
|
|
358
|
+
raise
|
|
359
|
+
|
|
360
|
+
pilot["redecide"] = {
|
|
361
|
+
"new_decision_snapshot_id": snapshot["decision_snapshot_id"],
|
|
362
|
+
"graph_revision": revision.revision,
|
|
363
|
+
"diff": diff,
|
|
364
|
+
}
|
|
365
|
+
pilot["status"] = "adjudicated"
|
|
366
|
+
_save_pilot(project, pilot)
|
|
367
|
+
return {"snapshot": snapshot, "revision": revision.revision,
|
|
368
|
+
"diff": diff, "snapshot_path": str(path)}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Capability task-DAG planner.
|
|
2
|
+
|
|
3
|
+
Plans research by scientific capability, never by Agent/model names. A
|
|
4
|
+
`PlanStep(kind="capability")` maps 1:1 to a capability; a
|
|
5
|
+
`PlanStep(kind="wait", wait_state="waiting_for_user_data")` is a real state
|
|
6
|
+
when the user's dataset is required but not yet present — never a fake
|
|
7
|
+
capability.
|
|
8
|
+
|
|
9
|
+
`intervention_design` / `evaluation_design` are decision_target-dependent:
|
|
10
|
+
included for `teaching_pilot` / `evaluation_plan` / `research_cycle`, not
|
|
11
|
+
forced for `evidence_review` to satisfy a fixed report template.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from engine.capabilities import capability, capability_registry
|
|
19
|
+
|
|
20
|
+
_FULL_CYCLE_EXTRAS = ("study_design", "measurement_design")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PlanStep:
|
|
25
|
+
step_id: str
|
|
26
|
+
kind: str # capability | wait
|
|
27
|
+
capability_id: str | None
|
|
28
|
+
wait_state: str | None
|
|
29
|
+
depends_on: tuple[str, ...]
|
|
30
|
+
input_refs: tuple[str, ...]
|
|
31
|
+
output_contract: str | None
|
|
32
|
+
gate: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _step(step_id: str, capability_id: str, depends_on: tuple[str, ...]) -> PlanStep:
|
|
36
|
+
spec = capability(capability_id)
|
|
37
|
+
if spec is None:
|
|
38
|
+
raise ValueError(f"unknown capability {capability_id!r}")
|
|
39
|
+
return PlanStep(
|
|
40
|
+
step_id=step_id,
|
|
41
|
+
kind="capability",
|
|
42
|
+
capability_id=capability_id,
|
|
43
|
+
wait_state=None,
|
|
44
|
+
depends_on=depends_on,
|
|
45
|
+
input_refs=spec.input_contracts,
|
|
46
|
+
output_contract=spec.output_contracts[0] if spec.output_contracts else None,
|
|
47
|
+
gate=spec.scientific_gate,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_EVIDENCE_REVIEW_STEPS = (
|
|
52
|
+
("research_framing", ()),
|
|
53
|
+
("literature_search", ("research_framing",)),
|
|
54
|
+
("counter_evidence_search", ("research_framing",)),
|
|
55
|
+
("source_fetch", ("literature_search", "counter_evidence_search")),
|
|
56
|
+
("source_validation", ("source_fetch",)),
|
|
57
|
+
("study_extraction", ("source_validation",)),
|
|
58
|
+
("finding_extraction", ("study_extraction",)),
|
|
59
|
+
("methodology_appraisal", ("study_extraction",)),
|
|
60
|
+
("claim_linking", ("finding_extraction",)),
|
|
61
|
+
("evidence_synthesis", ("claim_linking", "methodology_appraisal")),
|
|
62
|
+
("tribunal", ("evidence_synthesis",)),
|
|
63
|
+
("applicability_analysis", ("claim_linking",)),
|
|
64
|
+
("knowledge_gap_detection", ("research_framing", "claim_linking")),
|
|
65
|
+
("report_projection", ("tribunal", "knowledge_gap_detection")),
|
|
66
|
+
("report_rendering", ("report_projection",)),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_research_plan(*, mode: str, decision_target: str, depth: str,
|
|
71
|
+
has_grounding: bool,
|
|
72
|
+
has_dataset: bool) -> tuple[PlanStep, ...]:
|
|
73
|
+
"""Build the capability task DAG for a research mode.
|
|
74
|
+
|
|
75
|
+
- evidence_review: the fixed 15-step pipeline.
|
|
76
|
+
- full_research_cycle: Evidence Review + study_design/measurement_design
|
|
77
|
+
after grounding/gap tasks; if `has_dataset=False`, emits a wait step
|
|
78
|
+
instead of pretending data exists.
|
|
79
|
+
- intervention/evaluation design depend on decision_target.
|
|
80
|
+
"""
|
|
81
|
+
steps: list[PlanStep] = []
|
|
82
|
+
for capability_id, depends in _EVIDENCE_REVIEW_STEPS:
|
|
83
|
+
steps.append(_step(capability_id, capability_id, depends))
|
|
84
|
+
|
|
85
|
+
if mode == "full_research_cycle":
|
|
86
|
+
if not has_grounding:
|
|
87
|
+
# the plan must not design a study before grounding exists; the
|
|
88
|
+
# router already flagged requires_grounding_review, and the
|
|
89
|
+
# executor runs the minimum Evidence Review first. The planner
|
|
90
|
+
# still emits the design steps but they depend on tribunal/gap.
|
|
91
|
+
pass
|
|
92
|
+
for capability_id in _FULL_CYCLE_EXTRAS:
|
|
93
|
+
dep = ("knowledge_gap_detection",) if capability_id == "study_design" \
|
|
94
|
+
else ("study_design",)
|
|
95
|
+
steps.append(_step(capability_id, capability_id, dep))
|
|
96
|
+
if has_dataset:
|
|
97
|
+
steps.append(_step("data_validation", "data_validation",
|
|
98
|
+
("measurement_design",)))
|
|
99
|
+
steps.append(_step("data_analysis", "data_analysis",
|
|
100
|
+
("data_validation",)))
|
|
101
|
+
else:
|
|
102
|
+
steps.append(PlanStep(
|
|
103
|
+
step_id="wait_for_user_data",
|
|
104
|
+
kind="wait",
|
|
105
|
+
capability_id=None,
|
|
106
|
+
wait_state="waiting_for_user_data",
|
|
107
|
+
depends_on=("measurement_design",),
|
|
108
|
+
input_refs=(),
|
|
109
|
+
output_contract=None,
|
|
110
|
+
gate=None,
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
target_design = decision_target in ("teaching_pilot", "evaluation_plan",
|
|
114
|
+
"research_cycle")
|
|
115
|
+
if target_design:
|
|
116
|
+
steps.append(_step("intervention_design", "intervention_design",
|
|
117
|
+
("tribunal",)))
|
|
118
|
+
steps.append(_step("evaluation_design", "evaluation_design",
|
|
119
|
+
("tribunal",)))
|
|
120
|
+
|
|
121
|
+
return tuple(steps)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def plan_capability_ids(plan: tuple[PlanStep, ...]) -> list[str]:
|
|
125
|
+
"""Capability ids in plan order (excludes wait steps)."""
|
|
126
|
+
return [s.capability_id for s in plan if s.kind == "capability" and s.capability_id]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""ProjectWorkspace lifecycle — the durable unit of V2 research.
|
|
2
|
+
|
|
3
|
+
A Project owns a versioned Evidence Graph plus gap/design/dataset/analysis/
|
|
4
|
+
decision/projection/report/run areas. Run is one execution attempt *inside*
|
|
5
|
+
a Project; Project is long-lived. `project.json` is written atomically and
|
|
6
|
+
always mirrors `graph/HEAD` in `graph_revision`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from engine.contracts import validate_record
|
|
18
|
+
from engine.ids import new_project_id
|
|
19
|
+
from engine.versions import ENGINE_VERSION, GRAPH_SCHEMA_VERSION
|
|
20
|
+
|
|
21
|
+
_SUBDIRS = (
|
|
22
|
+
"graph/revisions",
|
|
23
|
+
"gaps",
|
|
24
|
+
"study-designs",
|
|
25
|
+
"datasets/raw",
|
|
26
|
+
"datasets/processed",
|
|
27
|
+
"datasets/manifests",
|
|
28
|
+
"analyses",
|
|
29
|
+
"decisions",
|
|
30
|
+
"projections",
|
|
31
|
+
"reports",
|
|
32
|
+
"runs",
|
|
33
|
+
"pilots",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_MANIFEST_NAME = "project.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _now_iso() -> str:
|
|
40
|
+
return datetime.now(timezone.utc).isoformat()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _atomic_write_json(path: Path, record: dict) -> None:
|
|
44
|
+
"""Write JSON via tmp file + rename so readers never see partial state."""
|
|
45
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
47
|
+
tmp.write_text(
|
|
48
|
+
json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
49
|
+
)
|
|
50
|
+
os.replace(tmp, path)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _default_decision_target(research_mode: str) -> str:
|
|
54
|
+
return "research_cycle" if research_mode == "full_research_cycle" else "teaching_decision"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ProjectWorkspace:
|
|
59
|
+
home: Path
|
|
60
|
+
project_id: str
|
|
61
|
+
path: Path
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def create(cls, home: Path, *, question: str, title: str,
|
|
65
|
+
research_mode: str) -> "ProjectWorkspace":
|
|
66
|
+
home = Path(home).expanduser().resolve()
|
|
67
|
+
project_id = new_project_id(question)
|
|
68
|
+
path = home / "projects" / project_id
|
|
69
|
+
for sub in _SUBDIRS:
|
|
70
|
+
(path / sub).mkdir(parents=True, exist_ok=True)
|
|
71
|
+
now = _now_iso()
|
|
72
|
+
manifest = {
|
|
73
|
+
"project_id": project_id,
|
|
74
|
+
"title": title,
|
|
75
|
+
"domain": "education",
|
|
76
|
+
"question": question,
|
|
77
|
+
"research_mode": research_mode,
|
|
78
|
+
"decision_target": _default_decision_target(research_mode),
|
|
79
|
+
"created_at": now,
|
|
80
|
+
"updated_at": now,
|
|
81
|
+
"engine_version": ENGINE_VERSION,
|
|
82
|
+
"schema_version": GRAPH_SCHEMA_VERSION,
|
|
83
|
+
"graph_revision": 0,
|
|
84
|
+
"status": "active",
|
|
85
|
+
}
|
|
86
|
+
errors = validate_record("project", manifest)
|
|
87
|
+
if errors:
|
|
88
|
+
raise ValueError(f"invalid project manifest: {errors}")
|
|
89
|
+
_atomic_write_json(path / _MANIFEST_NAME, manifest)
|
|
90
|
+
return cls(home=home, project_id=project_id, path=path)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def open(cls, home: Path, project_id: str) -> "ProjectWorkspace":
|
|
94
|
+
home = Path(home).expanduser().resolve()
|
|
95
|
+
path = home / "projects" / project_id
|
|
96
|
+
if not (path / _MANIFEST_NAME).is_file():
|
|
97
|
+
raise FileNotFoundError(f"project {project_id!r} not found under {home}")
|
|
98
|
+
return cls(home=home, project_id=project_id, path=path)
|
|
99
|
+
|
|
100
|
+
def manifest(self) -> dict:
|
|
101
|
+
return json.loads((self.path / _MANIFEST_NAME).read_text(encoding="utf-8"))
|
|
102
|
+
|
|
103
|
+
def update_manifest(self, **changes) -> dict:
|
|
104
|
+
"""Apply changes, bump updated_at, write atomically; return the new manifest."""
|
|
105
|
+
manifest = self.manifest()
|
|
106
|
+
manifest.update(changes)
|
|
107
|
+
manifest["updated_at"] = _now_iso()
|
|
108
|
+
errors = validate_record("project", manifest)
|
|
109
|
+
if errors:
|
|
110
|
+
raise ValueError(f"invalid project manifest after update: {errors}")
|
|
111
|
+
_atomic_write_json(self.path / _MANIFEST_NAME, manifest)
|
|
112
|
+
return manifest
|
|
113
|
+
|
|
114
|
+
def current_revision(self) -> int:
|
|
115
|
+
return int(self.manifest()["graph_revision"])
|
|
116
|
+
|
|
117
|
+
def runs_dir(self) -> Path:
|
|
118
|
+
return self.path / "runs"
|