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,1456 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""orchestrator.py — Run Orchestrator (Phase 11) + Resume (Phase 32) + Failure Matrix (Phase 33).
|
|
3
|
+
|
|
4
|
+
The orchestrator owns the RUN mechanics only — stage routing, workspace state,
|
|
5
|
+
schema gates, resource routing, execution backend selection, artifacts and
|
|
6
|
+
failure handling. It never performs domain reasoning (framing, retrieval,
|
|
7
|
+
extraction, skepticism, audit or intervention design are external-agent
|
|
8
|
+
stages); the two deterministic stages it executes locally are:
|
|
9
|
+
|
|
10
|
+
adjudicate — Pre-Verdict Gate (scripts/pre_verdict_gate.py) + deterministic
|
|
11
|
+
confidence (scripts/compute_confidence.py) producing
|
|
12
|
+
final_verdict.json from raw_verdict.json + evidence.jsonl
|
|
13
|
+
present — assemble result.json from the workspace artifacts
|
|
14
|
+
(decision = final_verdict.json; claims carry claim_id per
|
|
15
|
+
report-result.schema.json)
|
|
16
|
+
|
|
17
|
+
Stage machine (execution_plan.json / state.json):
|
|
18
|
+
|
|
19
|
+
frame -> retrieve -> extract -> challenge -> audit -> adjudicate
|
|
20
|
+
-> intervene -> evaluate -> present
|
|
21
|
+
|
|
22
|
+
Each stage writes exactly one primary artifact and is schema-gated against
|
|
23
|
+
schemas/*. When the artifact is missing the orchestrator either seeds it from
|
|
24
|
+
a demo pack (--demo-pack, tests/demo mode) or leaves a task brief for an
|
|
25
|
+
external agent and marks the stage pending. Resume (Phase 32) continues from
|
|
26
|
+
the first non-completed stage using state.json; failures are mapped through
|
|
27
|
+
the FAILURE_MATRIX (Phase 33).
|
|
28
|
+
|
|
29
|
+
CLI (entry points `eduevidence` via eduevidence_cli.py, or directly):
|
|
30
|
+
|
|
31
|
+
python scripts/orchestrator.py run --question "..." --depth deep
|
|
32
|
+
python scripts/orchestrator.py run --question "..." --demo-pack examples/ai-coding-assistant
|
|
33
|
+
python scripts/orchestrator.py resume --run-id 20260812-103000
|
|
34
|
+
python scripts/orchestrator.py status --run-id 20260812-103000
|
|
35
|
+
python scripts/orchestrator.py list
|
|
36
|
+
python scripts/orchestrator.py gate --run-id 20260812-103000
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import copy
|
|
42
|
+
import json
|
|
43
|
+
import logging
|
|
44
|
+
import os
|
|
45
|
+
import sys
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Any
|
|
48
|
+
|
|
49
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
50
|
+
for _p in (str(ROOT), str(ROOT / "scripts")):
|
|
51
|
+
if _p not in sys.path:
|
|
52
|
+
sys.path.insert(0, _p)
|
|
53
|
+
|
|
54
|
+
from run_workspace import (RESOURCE_POLICY_VERSION, STAGES, RunWorkspace, # noqa: E402
|
|
55
|
+
load_json, load_jsonl, next_run_id, save_jsonl)
|
|
56
|
+
from pre_verdict_gate import apply_enforcement, evaluate_workspace # noqa: E402
|
|
57
|
+
from engine.versions import ENGINE_VERSION # noqa: E402
|
|
58
|
+
from engine.log import enable_console_logging, get_log # noqa: E402
|
|
59
|
+
|
|
60
|
+
log = get_log("orchestrator")
|
|
61
|
+
|
|
62
|
+
DEPTH_ALIASES = {"quick": "S", "standard": "M", "deep": "L"}
|
|
63
|
+
DEPTHS = ("S", "M", "L")
|
|
64
|
+
|
|
65
|
+
#: Stage -> primary artifact + schema gate + whether it is locally executable.
|
|
66
|
+
STAGE_SPEC: dict[str, dict[str, Any]] = {
|
|
67
|
+
"frame": {"artifact": "frame.json", "schema": "education-frame.schema.json", "jsonl": False, "local": False},
|
|
68
|
+
"retrieve": {"artifact": "sources.jsonl", "schema": "source.schema.json", "jsonl": True, "local": False},
|
|
69
|
+
"extract": {"artifact": "evidence.jsonl", "schema": "evidence.schema.json", "jsonl": True, "local": False},
|
|
70
|
+
"challenge": {"artifact": "skeptic.json", "schema": None, "jsonl": False, "local": False},
|
|
71
|
+
"audit": {"artifact": "methodology.json", "schema": "methodology.schema.json", "jsonl": False, "local": False},
|
|
72
|
+
"adjudicate": {"artifact": "final_verdict.json", "schema": "verdict.schema.json", "jsonl": False, "local": True},
|
|
73
|
+
"intervene": {"artifact": "intervention.json", "schema": "intervention.schema.json", "jsonl": False, "local": False},
|
|
74
|
+
"evaluate": {"artifact": "evaluation.json", "schema": "evaluation.schema.json", "jsonl": False, "local": False},
|
|
75
|
+
"present": {"artifact": "result.json", "schema": "report-result.schema.json", "jsonl": False, "local": True},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#: Phase 33 — canonical failure -> handling-action mapping. Extends the
|
|
79
|
+
#: retrieval-layer taxonomy (retrieval/failures.py) with orchestration states.
|
|
80
|
+
FAILURE_MATRIX: dict[str, dict[str, Any]] = {
|
|
81
|
+
"TOOL_FAILURE": {"action": "retry_with_fallback_tool", "retry": True,
|
|
82
|
+
"note": "a tool invocation failed; retry with an alternate tool before aborting"},
|
|
83
|
+
"SCHEMA_INVALID": {"action": "block_stage_advance_and_fix_artifact", "retry": False,
|
|
84
|
+
"note": "stage artifact violates its schema gate; regenerate or repair the artifact"},
|
|
85
|
+
"STAGE_ARTIFACT_MISSING": {"action": "write_brief_and_mark_pending", "retry": False,
|
|
86
|
+
"note": "stage waits for an external agent; resume continues when the artifact appears"},
|
|
87
|
+
"GATE_CRITICAL_FAILURE": {"action": "cap_confidence_and_force_pilot_or_insufficient", "retry": False,
|
|
88
|
+
"note": "Pre-Verdict Gate critical failures forbid high confidence; verdict is capped"},
|
|
89
|
+
"SEARCH_NO_RESULT": {"action": "rerun_search_with_broader_terms", "retry": True,
|
|
90
|
+
"note": "no results for the query; broaden terms or switch discovery provider"},
|
|
91
|
+
"SEARCH_LOW_QUALITY": {"action": "widen_query_or_accept_lower_authority_tier", "retry": True,
|
|
92
|
+
"note": "results are low quality; widen the query or accept a lower authority tier"},
|
|
93
|
+
"FETCH_FAILED": {"action": "alternate_fetch_provider_then_alternate_source", "retry": True,
|
|
94
|
+
"note": "degradation chain exhausted; do not retry the same URL, return to Discovery"},
|
|
95
|
+
"FETCH_PARTIAL": {"action": "rule_confirm_or_human_confirm_before_extraction", "retry": False,
|
|
96
|
+
"note": "content partially readable; require rule/human confirmation before extraction"},
|
|
97
|
+
"SOURCE_INVALID": {"action": "discard_and_find_alternate_source", "retry": False,
|
|
98
|
+
"note": "source does not validate; discard and find an alternate source"},
|
|
99
|
+
"SOURCE_DUPLICATE": {"action": "merge_keep_highest_authority", "retry": False,
|
|
100
|
+
"note": "same paper behind mirror URLs; merge and keep the highest-authority entry"},
|
|
101
|
+
"UNSUPPORTED_CLAIM": {"action": "downgrade_claim_or_drop", "retry": False,
|
|
102
|
+
"note": "claim cannot be bound to a verifiable source; downgrade or drop"},
|
|
103
|
+
"CONFLICT_UNRESOLVED": {"action": "stay_uncertain_do_not_force_adjudication", "retry": False,
|
|
104
|
+
"note": "conflicting evidence without resolution; remain uncertain"},
|
|
105
|
+
"SCOPE_MISMATCH": {"action": "shrink_conclusion_scope", "retry": False,
|
|
106
|
+
"note": "conclusion scope exceeds evidence scope; shrink the conclusion"},
|
|
107
|
+
"METHODOLOGY_TOO_WEAK": {"action": "do_not_use_as_support", "retry": False,
|
|
108
|
+
"note": "methodology audit fails; the study cannot support claims"},
|
|
109
|
+
"INSUFFICIENT_EVIDENCE": {"action": "mark_insufficient_evidence", "retry": False,
|
|
110
|
+
"note": "evidence base is too thin; output INSUFFICIENT EVIDENCE"},
|
|
111
|
+
"AGENT_MCP_UNAVAILABLE": {"action": "degrade_to_platform_native_mode", "retry": False,
|
|
112
|
+
"note": "agent-mcp not reachable; fall back to Mode A semantics"},
|
|
113
|
+
"REPORT_INVALID": {"action": "block_publish_rerun_render", "retry": True,
|
|
114
|
+
"note": "rendered report fails validation; block publishing and rerun the render"},
|
|
115
|
+
"DEMO_PACK_MISSING": {"action": "write_brief_and_mark_pending", "retry": False,
|
|
116
|
+
"note": "requested demo seed not in the demo pack; treat as a normal pending stage"},
|
|
117
|
+
# -- states documented in docs/failure-matrix.md (Phase 33), kept as aliases
|
|
118
|
+
# so the code matrix is a superset of the documented failure taxonomy.
|
|
119
|
+
"INSUFFICIENT_SOURCES": {"action": "supplement_search_or_mark_insufficient", "retry": True,
|
|
120
|
+
"note": "too few/direct/strong sources to support a conclusion; supplement search or output INSUFFICIENT"},
|
|
121
|
+
"NEEDS_USER_CONTEXT": {"action": "request_user_context_before_continuing", "retry": False,
|
|
122
|
+
"note": "minimal learner/course/intervention/outcome inputs missing; ask the user, never guess defaults"},
|
|
123
|
+
"AGENT_MCP_APPROVAL_REQUIRED": {"action": "do_not_spawn_confirm_model_table_first", "retry": False,
|
|
124
|
+
"note": "agent-mcp installed but model table not user-confirmed; no spawn until approved"},
|
|
125
|
+
"PRE_VERDICT_FAILED": {"action": "fix_pre_verdict_artifacts_rerun_gate", "retry": True,
|
|
126
|
+
"note": "pre-verdict prerequisites failed (verdict schema / cross-model review / methodology); fix and re-run the gate"},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_STAGE_BRIEFS: dict[str, str] = {
|
|
130
|
+
"frame": ("Structure the raw education question into an Education Research Frame "
|
|
131
|
+
"(learner/course/intervention/comparison/outcomes/context/scope). "
|
|
132
|
+
"Write frame.json."),
|
|
133
|
+
"retrieve": ("Search for candidate sources within the frame scope; record Source Objects "
|
|
134
|
+
"(source_id, title, canonical_url, authority_level) in sources.jsonl "
|
|
135
|
+
"(one JSON object per line) and store fetched content under fetch/."),
|
|
136
|
+
"extract": ("Extract claim-level Evidence Objects from the retrieved sources "
|
|
137
|
+
"(evidence_id, source_id, claim, outcome_type, direction, source_location) "
|
|
138
|
+
"into evidence.jsonl."),
|
|
139
|
+
"challenge": ("Act as the Skeptic: actively search for counter-evidence, null results and "
|
|
140
|
+
"confounders; write skeptic.json with search_performed=true and the findings."),
|
|
141
|
+
"audit": ("Method-review every study: methodology.json with audit_items, "
|
|
142
|
+
"task_vs_learning_guard and a PASS/CONCERN/FAIL verdict."),
|
|
143
|
+
"adjudicate": ("Judge the evidence: write raw_verdict.json (model verdict). The orchestrator "
|
|
144
|
+
"then runs the Pre-Verdict Gate and deterministic confidence to produce "
|
|
145
|
+
"final_verdict.json."),
|
|
146
|
+
"intervene": ("Design the minimal verifiable teaching intervention (phased pilot, "
|
|
147
|
+
"stop conditions, evidence alignment); write intervention.json."),
|
|
148
|
+
"evaluate": ("Design the evaluation plan (baseline/post/retention/transfer, task vs learning "
|
|
149
|
+
"separation); write evaluation.json."),
|
|
150
|
+
"present": ("Translate result.json into result.zh.json and render report_spec.json / "
|
|
151
|
+
"report.html via the visualization layer."),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _utc_now() -> str:
|
|
156
|
+
from run_workspace import utc_now
|
|
157
|
+
return utc_now()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def handle_failure(token: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
161
|
+
"""Phase 33 failure routing: canonical token -> handling action."""
|
|
162
|
+
entry = FAILURE_MATRIX.get(token)
|
|
163
|
+
if entry is None:
|
|
164
|
+
raise ValueError(f"unknown failure state {token!r}; known: {sorted(FAILURE_MATRIX)}")
|
|
165
|
+
plan = {"state": token, **entry}
|
|
166
|
+
if context:
|
|
167
|
+
plan["context"] = context
|
|
168
|
+
return plan
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------- workspace plans
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def init_run(
|
|
175
|
+
runs_dir: Path,
|
|
176
|
+
question: str,
|
|
177
|
+
*,
|
|
178
|
+
depth: str = "M",
|
|
179
|
+
run_id: str | None = None,
|
|
180
|
+
approve_agent_mcp: bool = False,
|
|
181
|
+
scp_available: bool | None = None,
|
|
182
|
+
) -> RunWorkspace:
|
|
183
|
+
"""Create the run workspace + manifest + planning artifacts (Phase 11-13)."""
|
|
184
|
+
depth = DEPTH_ALIASES.get(depth, depth)
|
|
185
|
+
if depth not in DEPTHS:
|
|
186
|
+
raise ValueError(f"unknown depth {depth!r}; use quick/standard/deep or S/M/L")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
from integrations.agent_mcp import detect_agent_mcp
|
|
190
|
+
detection = detect_agent_mcp()
|
|
191
|
+
agent_mode = detection["mode"]
|
|
192
|
+
agent_available = detection["available"]
|
|
193
|
+
except Exception:
|
|
194
|
+
agent_mode, agent_available = "platform_native", False
|
|
195
|
+
|
|
196
|
+
from run_workspace import build_manifest
|
|
197
|
+
|
|
198
|
+
if run_id is None:
|
|
199
|
+
run_id = next_run_id(runs_dir)
|
|
200
|
+
ws = RunWorkspace(runs_dir, run_id)
|
|
201
|
+
ws.create()
|
|
202
|
+
|
|
203
|
+
manifest = build_manifest(
|
|
204
|
+
run_id, question,
|
|
205
|
+
execution_mode=agent_mode,
|
|
206
|
+
agent_mcp_available=agent_available,
|
|
207
|
+
agent_mcp_approved=approve_agent_mcp,
|
|
208
|
+
root=ROOT,
|
|
209
|
+
)
|
|
210
|
+
ws.save_manifest(manifest)
|
|
211
|
+
|
|
212
|
+
state = ws.load_state()
|
|
213
|
+
state.update({"run_id": run_id, "question": question, "depth": depth,
|
|
214
|
+
"status": "running", "current_stage": STAGES[0]})
|
|
215
|
+
ws.save_state(state)
|
|
216
|
+
|
|
217
|
+
# -- planning artifacts ------------------------------------------------
|
|
218
|
+
capability_plan = {
|
|
219
|
+
"run_id": run_id,
|
|
220
|
+
"depth": depth,
|
|
221
|
+
"required_capabilities": [
|
|
222
|
+
"education_framing", "evidence_retrieval", "evidence_extraction",
|
|
223
|
+
"skeptic_review", "methodology_audit", "adjudication",
|
|
224
|
+
"intervention_design", "evaluation_design", "bilingual_reporting"],
|
|
225
|
+
"local_capabilities": {
|
|
226
|
+
"schema_validation": True, "deterministic_confidence": True,
|
|
227
|
+
"claim_audit": True, "evidence_matrix": True,
|
|
228
|
+
"pre_verdict_gate": True, "result_assembly": True,
|
|
229
|
+
"complexity_gate": True},
|
|
230
|
+
"external_capabilities": {
|
|
231
|
+
"web_fetch": True, "smart_web_fetch": True, "search": True,
|
|
232
|
+
"agent_mcp_dispatch": agent_available},
|
|
233
|
+
}
|
|
234
|
+
resource_plan = {
|
|
235
|
+
"run_id": run_id,
|
|
236
|
+
"execution_mode": agent_mode,
|
|
237
|
+
"resource_policy_version": RESOURCE_POLICY_VERSION,
|
|
238
|
+
"token_budget_per_stage": {
|
|
239
|
+
"frame": 4000, "retrieve": 8000, "extract": 12000, "challenge": 8000,
|
|
240
|
+
"audit": 8000, "adjudicate": 8000, "intervene": 6000,
|
|
241
|
+
"evaluate": 6000, "present": 6000},
|
|
242
|
+
"max_concurrent_agents": {"S": 0, "M": 2, "L": 4}[depth],
|
|
243
|
+
"timeouts_s": {"fetch": 20, "agent": 1800},
|
|
244
|
+
}
|
|
245
|
+
execution_plan = {
|
|
246
|
+
"run_id": run_id,
|
|
247
|
+
"depth": depth,
|
|
248
|
+
"stages": [
|
|
249
|
+
{"name": s, "status": "pending",
|
|
250
|
+
"artifact": STAGE_SPEC[s]["artifact"],
|
|
251
|
+
"schema": STAGE_SPEC[s]["schema"],
|
|
252
|
+
"mode": "local" if STAGE_SPEC[s]["local"] else "external"}
|
|
253
|
+
for s in STAGES],
|
|
254
|
+
}
|
|
255
|
+
model_inventory = {
|
|
256
|
+
"run_id": run_id,
|
|
257
|
+
"execution_mode": agent_mode,
|
|
258
|
+
"routing": {
|
|
259
|
+
"education-planner": "strong/reasoning",
|
|
260
|
+
"evidence-retriever": "fast/low-cost",
|
|
261
|
+
"evidence-analyst": "strong/structured",
|
|
262
|
+
"skeptic": "independent/reasoning",
|
|
263
|
+
"method-reviewer": "strong/reasoning",
|
|
264
|
+
"evidence-judge": "strong/reasoning",
|
|
265
|
+
"intervention-designer": "strong/reasoning",
|
|
266
|
+
"evaluation-designer": "strong/reasoning"} if agent_available else {},
|
|
267
|
+
"agents": {},
|
|
268
|
+
}
|
|
269
|
+
agent_mcp_approval = {
|
|
270
|
+
"run_id": run_id,
|
|
271
|
+
"agent_mcp_available": agent_available,
|
|
272
|
+
"approved": approve_agent_mcp,
|
|
273
|
+
"approved_at": _utc_now() if approve_agent_mcp else None,
|
|
274
|
+
"mode": agent_mode,
|
|
275
|
+
"reason": ("user-approved via --approve-agent-mcp"
|
|
276
|
+
if approve_agent_mcp else "not yet approved; runs in platform-native mode"),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for name, data in (("capability_plan", capability_plan),
|
|
280
|
+
("resource_plan", resource_plan),
|
|
281
|
+
("execution_plan", execution_plan),
|
|
282
|
+
("model_inventory", model_inventory),
|
|
283
|
+
("agent_mcp_approval", agent_mcp_approval)):
|
|
284
|
+
(ws.path / f"{name}.json").write_text(
|
|
285
|
+
json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
286
|
+
|
|
287
|
+
ws.trace("run_initialized", detail=f"depth={depth} mode={agent_mode} question={question[:120]}")
|
|
288
|
+
return ws
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ------------------------------------------------------------- schema gates
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _load_artifact(ws: RunWorkspace, artifact: str) -> list[dict[str, Any]]:
|
|
295
|
+
path = ws.path / artifact
|
|
296
|
+
if not path.is_file():
|
|
297
|
+
return []
|
|
298
|
+
if artifact.endswith(".jsonl"):
|
|
299
|
+
return load_jsonl(path)
|
|
300
|
+
data = load_json(path)
|
|
301
|
+
return [data] if data else []
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def schema_gate(ws: RunWorkspace, stage: str) -> dict[str, Any]:
|
|
305
|
+
"""Validate a stage's primary artifact against its schema. Never raises."""
|
|
306
|
+
spec = STAGE_SPEC[stage]
|
|
307
|
+
artifact = spec["artifact"]
|
|
308
|
+
schema_name = spec["schema"]
|
|
309
|
+
if schema_name is None: # challenge: light parseability contract
|
|
310
|
+
data = load_json(ws.path / artifact)
|
|
311
|
+
ok = bool(data) and isinstance(data, dict)
|
|
312
|
+
return {"passed": ok, "stage": stage, "artifact": artifact,
|
|
313
|
+
"schema": None, "issues": [] if ok else ["skeptic.json missing or unparseable"]}
|
|
314
|
+
|
|
315
|
+
from validate_schema import SchemaError, Validator
|
|
316
|
+
|
|
317
|
+
schemas_dir = ROOT / "schemas"
|
|
318
|
+
if not (schemas_dir / schema_name).is_file():
|
|
319
|
+
share_dir = Path(sys.prefix) / "share" / "eduevidence" / "schemas"
|
|
320
|
+
if (share_dir / schema_name).is_file():
|
|
321
|
+
schemas_dir = share_dir
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
schema = json.loads((schemas_dir / schema_name).read_text(encoding="utf-8"))
|
|
325
|
+
except OSError:
|
|
326
|
+
return {"passed": False, "stage": stage, "artifact": artifact,
|
|
327
|
+
"schema": schema_name, "issues": [f"schema file {schema_name} not found"]}
|
|
328
|
+
|
|
329
|
+
records = _load_artifact(ws, artifact)
|
|
330
|
+
if not records:
|
|
331
|
+
return {"passed": False, "stage": stage, "artifact": artifact,
|
|
332
|
+
"schema": schema_name, "issues": [f"{artifact} missing or empty"]}
|
|
333
|
+
|
|
334
|
+
issues = []
|
|
335
|
+
validator = Validator(schema, base_dir=schemas_dir)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
for idx, record in enumerate(records):
|
|
339
|
+
try:
|
|
340
|
+
validator.validate(record, schema, f"{artifact}[{idx}]")
|
|
341
|
+
except SchemaError as exc:
|
|
342
|
+
issues.append(str(exc))
|
|
343
|
+
return {"passed": not issues, "stage": stage, "artifact": artifact,
|
|
344
|
+
"schema": schema_name, "issues": issues[:5]}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------- deterministic stages
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def derive_sources_from_evidence(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
351
|
+
"""Deterministic source registry derived from evidence records (demo/test mode).
|
|
352
|
+
|
|
353
|
+
Honest fallback (review P1-2): authority comes from a real DOI parsed out
|
|
354
|
+
of the location, otherwise tier5 + source_metadata_incomplete. Never
|
|
355
|
+
fabricates a canonical URL from an id.
|
|
356
|
+
"""
|
|
357
|
+
from build_result import _derive_source_from_evidence
|
|
358
|
+
from retrieval.source import make_source
|
|
359
|
+
|
|
360
|
+
seen: dict[str, dict[str, Any]] = {}
|
|
361
|
+
for ev in evidence:
|
|
362
|
+
sid = ev.get("source_id", "")
|
|
363
|
+
if not sid or sid in seen:
|
|
364
|
+
continue
|
|
365
|
+
loc = ev.get("source_location", "") or ""
|
|
366
|
+
if loc:
|
|
367
|
+
derived = _derive_source_from_evidence(ev)
|
|
368
|
+
if derived is not None:
|
|
369
|
+
seen[sid] = make_source(
|
|
370
|
+
source_id=sid,
|
|
371
|
+
title=ev.get("title", sid),
|
|
372
|
+
canonical_url=loc,
|
|
373
|
+
authority_level=derived["authority_level"],
|
|
374
|
+
year=ev.get("year"),
|
|
375
|
+
)
|
|
376
|
+
seen[sid].setdefault("extensions", {})["source_metadata_incomplete"] = derived["extensions"]["source_metadata_incomplete"]
|
|
377
|
+
if not seen.get(sid, {}).get("fetch"):
|
|
378
|
+
# fetchProvenance requires fetch_status; an empty dict is schema-invalid
|
|
379
|
+
seen.get(sid, {}).pop("fetch", None)
|
|
380
|
+
return list(seen.values())
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def derive_skeptic_from_evidence(evidence: list[dict[str, Any]]) -> dict[str, Any]:
|
|
384
|
+
"""Deterministic skeptic summary derived from evidence directions (demo/test mode).
|
|
385
|
+
|
|
386
|
+
Records what the corpus itself contains (contradictions / null results /
|
|
387
|
+
confounders); it never invents counter-evidence.
|
|
388
|
+
"""
|
|
389
|
+
contradictions = [e.get("evidence_id") for e in evidence if e.get("direction") == "contradict"]
|
|
390
|
+
null_results = [e.get("evidence_id") for e in evidence if e.get("direction") == "neutral"]
|
|
391
|
+
confounders = sorted({c for e in evidence for c in (e.get("confounders", []) or [])})
|
|
392
|
+
return {
|
|
393
|
+
"search_performed": True,
|
|
394
|
+
"method": "derived from evidence corpus directions (demo/test mode)",
|
|
395
|
+
"contradictions": contradictions,
|
|
396
|
+
"null_results": null_results,
|
|
397
|
+
"confounders": confounders,
|
|
398
|
+
"no_contradictory_evidence_found": not contradictions,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _cap_verdict(gate: dict[str, Any], raw_verdict: dict[str, Any],
|
|
403
|
+
computed: dict[str, Any]) -> dict[str, Any]:
|
|
404
|
+
"""Build final_verdict.json: deterministic confidence + gate enforcement."""
|
|
405
|
+
final = copy.deepcopy(raw_verdict)
|
|
406
|
+
final["raw_model_confidence"] = raw_verdict.get("confidence")
|
|
407
|
+
final["raw_model_confidence_breakdown"] = raw_verdict.get("confidence_breakdown")
|
|
408
|
+
final["confidence"] = computed["confidence"]
|
|
409
|
+
final["confidence_score"] = computed["confidence_breakdown"].get("score")
|
|
410
|
+
final["confidence_policy_version"] = computed["confidence_policy_version"]
|
|
411
|
+
final["independent_studies"] = computed["independent_studies"]
|
|
412
|
+
final["independent_samples"] = computed["independent_samples"]
|
|
413
|
+
final["confidence_breakdown"] = computed["confidence_breakdown"]
|
|
414
|
+
return apply_enforcement(final, gate)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _run_adjudicate(ws: RunWorkspace, question: str,
|
|
418
|
+
demo_pack: Path | None = None) -> dict[str, Any]:
|
|
419
|
+
"""Local adjudicate: Pre-Verdict Gate + deterministic confidence."""
|
|
420
|
+
from compute_confidence import compute_confidence
|
|
421
|
+
|
|
422
|
+
raw_path = ws.path / "raw_verdict.json"
|
|
423
|
+
raw = load_json(raw_path)
|
|
424
|
+
if not raw and demo_pack is not None and Path(demo_pack).is_dir():
|
|
425
|
+
pack_verdict = Path(demo_pack) / "verdict.json"
|
|
426
|
+
if pack_verdict.is_file():
|
|
427
|
+
raw_path.write_bytes(pack_verdict.read_bytes())
|
|
428
|
+
raw = load_json(raw_path)
|
|
429
|
+
ws.trace("demo_seeded", stage="adjudicate",
|
|
430
|
+
detail="raw_verdict.json seeded from demo pack verdict.json")
|
|
431
|
+
evidence = load_jsonl(ws.path / "evidence.jsonl")
|
|
432
|
+
if not raw:
|
|
433
|
+
ws.write_brief("adjudicate", question, _STAGE_BRIEFS["adjudicate"])
|
|
434
|
+
return {"status": "pending", "detail": "raw_verdict.json missing; brief written for evidence judge"}
|
|
435
|
+
if not evidence:
|
|
436
|
+
ws.write_brief("adjudicate", question, _STAGE_BRIEFS["adjudicate"])
|
|
437
|
+
return {"status": "pending", "detail": "evidence.jsonl empty; extraction must complete first"}
|
|
438
|
+
|
|
439
|
+
pre = evaluate_workspace(ws.path, require_final=False)
|
|
440
|
+
computed = compute_confidence(evidence)
|
|
441
|
+
final = _cap_verdict(pre, raw, computed)
|
|
442
|
+
(ws.path / "final_verdict.json").write_text(
|
|
443
|
+
json.dumps(final, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
444
|
+
|
|
445
|
+
post = evaluate_workspace(ws.path, require_final=True)
|
|
446
|
+
gate_report = {"run_id": ws.run_id, "stage": "adjudicate",
|
|
447
|
+
"pre": {"passed": pre["passed"], "max_confidence": pre["max_confidence"],
|
|
448
|
+
"critical_failures": pre["critical_failures"]},
|
|
449
|
+
"post": post,
|
|
450
|
+
"final_confidence": final.get("confidence"),
|
|
451
|
+
"final_action": final.get("recommended_action"),
|
|
452
|
+
"checked_at": _utc_now()}
|
|
453
|
+
(ws.path / "gate_report.json").write_text(
|
|
454
|
+
json.dumps(gate_report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
455
|
+
|
|
456
|
+
if post["passed"]:
|
|
457
|
+
detail = (f"gate passed; confidence={final.get('confidence')} "
|
|
458
|
+
f"(policy={final.get('confidence_policy_version')})")
|
|
459
|
+
ws.trace("adjudicate_completed", stage="adjudicate", detail=detail)
|
|
460
|
+
return {"status": "completed", "detail": detail}
|
|
461
|
+
ws.trace("gate_failure_capped", stage="adjudicate",
|
|
462
|
+
detail=f"critical failures: {post['critical_failures']}; confidence capped at "
|
|
463
|
+
f"{post['max_confidence']}")
|
|
464
|
+
return {"status": "completed",
|
|
465
|
+
"detail": f"gate critical failures {post['critical_failures']}; verdict capped at "
|
|
466
|
+
f"{post['max_confidence']}"}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _assemble_result(ws: RunWorkspace, manifest: dict[str, Any]) -> dict[str, Any]:
|
|
470
|
+
"""Assemble result.json from workspace artifacts (decision=final_verdict.json)."""
|
|
471
|
+
from build_result import (NOT_CAPTURED_USAGE, OUTCOME_ORDER,
|
|
472
|
+
aggregate_outcomes, build_claims,
|
|
473
|
+
build_outcome_mapping, derive_provenance)
|
|
474
|
+
|
|
475
|
+
frame = load_json(ws.path / "frame.json")
|
|
476
|
+
evidence = load_jsonl(ws.path / "evidence.jsonl")
|
|
477
|
+
verdict = load_json(ws.path / "final_verdict.json") or load_json(ws.path / "raw_verdict.json")
|
|
478
|
+
methodology = load_json(ws.path / "methodology.json")
|
|
479
|
+
methodology_list = [methodology] if methodology else []
|
|
480
|
+
intervention = load_json(ws.path / "intervention.json")
|
|
481
|
+
evaluation = load_json(ws.path / "evaluation.json")
|
|
482
|
+
sources = load_jsonl(ws.path / "sources.jsonl")
|
|
483
|
+
if not sources:
|
|
484
|
+
sources = derive_sources_from_evidence(evidence)
|
|
485
|
+
|
|
486
|
+
claims = build_claims(evidence)
|
|
487
|
+
for idx, claim in enumerate(claims, 1):
|
|
488
|
+
claim["claim_id"] = f"C-{idx:03d}"
|
|
489
|
+
|
|
490
|
+
mode = manifest.get("execution_mode", "platform_native")
|
|
491
|
+
return {
|
|
492
|
+
"meta": {
|
|
493
|
+
"skill": "eduevidence",
|
|
494
|
+
"version": manifest.get("skill_version") or ENGINE_VERSION,
|
|
495
|
+
"generated_at": _utc_now(),
|
|
496
|
+
"mode": mode,
|
|
497
|
+
"question": frame.get("question", manifest.get("question", "")),
|
|
498
|
+
},
|
|
499
|
+
"execution": {
|
|
500
|
+
"complexity": frame.get("complexity") or manifest.get("depth", "M"),
|
|
501
|
+
"mode": mode,
|
|
502
|
+
"agents": [],
|
|
503
|
+
"usage": dict(NOT_CAPTURED_USAGE),
|
|
504
|
+
},
|
|
505
|
+
"research_frame": frame,
|
|
506
|
+
"decision": verdict,
|
|
507
|
+
"outcomes": aggregate_outcomes(evidence),
|
|
508
|
+
"outcome_mapping": build_outcome_mapping(evidence, frame),
|
|
509
|
+
"claims": claims,
|
|
510
|
+
"sources": sources,
|
|
511
|
+
"evidence": evidence,
|
|
512
|
+
"methodology_reviews": methodology_list,
|
|
513
|
+
"conflicts": [{"reason_for_disagreement": verdict.get("reason_for_disagreement", "")}]
|
|
514
|
+
if verdict.get("reason_for_disagreement") else [],
|
|
515
|
+
"applicability": verdict.get("applicability", {}),
|
|
516
|
+
"intervention": intervention,
|
|
517
|
+
"evaluation": evaluation,
|
|
518
|
+
"benchmark": {},
|
|
519
|
+
"provenance": derive_provenance(sources),
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _run_present(ws: RunWorkspace, manifest: dict[str, Any], question: str,
|
|
524
|
+
demo_pack: Path | None = None) -> dict[str, Any]:
|
|
525
|
+
"""Local present: assemble + validate result.json, seed render artifacts."""
|
|
526
|
+
required = ("final_verdict.json", "intervention.json", "evaluation.json")
|
|
527
|
+
missing = [name for name in required if not (ws.path / name).is_file()
|
|
528
|
+
or not load_json(ws.path / name)]
|
|
529
|
+
if missing:
|
|
530
|
+
ws.write_brief("present", question, _STAGE_BRIEFS["present"])
|
|
531
|
+
return {"status": "pending",
|
|
532
|
+
"detail": f"missing prerequisite artifacts: {', '.join(missing)}"}
|
|
533
|
+
|
|
534
|
+
# demo/test mode: seed the bilingual + render artifacts from the example pack
|
|
535
|
+
seeded = []
|
|
536
|
+
if demo_pack is not None and Path(demo_pack).is_dir():
|
|
537
|
+
pack = Path(demo_pack)
|
|
538
|
+
for name in ("result.zh.json", "report_spec.json"):
|
|
539
|
+
target = ws.path / name
|
|
540
|
+
if (pack / name).is_file() and (not target.is_file() or target.stat().st_size <= 2):
|
|
541
|
+
target.write_bytes((pack / name).read_bytes())
|
|
542
|
+
seeded.append(name)
|
|
543
|
+
if not (ws.path / "report.html").is_file() or (ws.path / "report.html").stat().st_size <= 2:
|
|
544
|
+
for name in ("EduEvidence_Report.html", "report.html"):
|
|
545
|
+
if (pack / name).is_file():
|
|
546
|
+
(ws.path / "report.html").write_bytes((pack / name).read_bytes())
|
|
547
|
+
seeded.append(name)
|
|
548
|
+
break
|
|
549
|
+
|
|
550
|
+
result = _assemble_result(ws, manifest)
|
|
551
|
+
(ws.path / "result.json").write_text(
|
|
552
|
+
json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
553
|
+
gate = schema_gate(ws, "present")
|
|
554
|
+
if not gate["passed"]:
|
|
555
|
+
return {"status": "failed", "detail": f"result.json schema gate: {gate['issues']}"}
|
|
556
|
+
missing_render = [n for n in ("result.zh.json", "report_spec.json", "report.html")
|
|
557
|
+
if not (ws.path / n).is_file() or (ws.path / n).stat().st_size <= 2]
|
|
558
|
+
if seeded:
|
|
559
|
+
extras = f"seeded render artifacts: {', '.join(seeded)}"
|
|
560
|
+
elif missing_render:
|
|
561
|
+
extras = f"render artifacts missing: {', '.join(missing_render)}"
|
|
562
|
+
else:
|
|
563
|
+
extras = "render artifacts present"
|
|
564
|
+
return {"status": "completed",
|
|
565
|
+
"detail": (f"result.json assembled (sources={len(result['sources'])}, "
|
|
566
|
+
f"evidence={len(result['evidence'])}, claims={len(result['claims'])}); "
|
|
567
|
+
f"{extras}")}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ------------------------------------------------------------- stage runner
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def run_stage(ws: RunWorkspace, stage: str, *, demo_pack: Path | None = None) -> dict[str, Any]:
|
|
574
|
+
"""Advance one stage. Returns {status: completed|pending|failed, detail}.
|
|
575
|
+
|
|
576
|
+
Deterministic stages execute locally; external stages are either seeded
|
|
577
|
+
from ``demo_pack`` (demo/test mode) or handed off via a task brief.
|
|
578
|
+
"""
|
|
579
|
+
ws.trace("stage_started", stage=stage)
|
|
580
|
+
log.info("stage=%s run=%s start", stage, ws.run_id)
|
|
581
|
+
spec = STAGE_SPEC[stage]
|
|
582
|
+
question = ws.load_manifest().get("question", "")
|
|
583
|
+
artifact_path = ws.path / spec["artifact"]
|
|
584
|
+
|
|
585
|
+
# already-present artifact -> schema gate only (empty seeds count as missing)
|
|
586
|
+
if artifact_path.is_file() and artifact_path.stat().st_size > 2:
|
|
587
|
+
gate = schema_gate(ws, stage)
|
|
588
|
+
if gate["passed"]:
|
|
589
|
+
ws.mark_stage(stage, "completed",
|
|
590
|
+
detail=f"artifact {spec['artifact']} schema-valid",
|
|
591
|
+
artifacts=[spec["artifact"]])
|
|
592
|
+
ws.trace("stage_completed", stage=stage, detail="schema gate passed")
|
|
593
|
+
return {"status": "completed", "detail": "artifact present, schema-valid"}
|
|
594
|
+
ws.trace("schema_invalid", stage=stage, detail=gate["issues"][0])
|
|
595
|
+
ws.mark_stage(stage, "failed", detail=f"schema gate failed: {gate['issues'][0]}")
|
|
596
|
+
return {"status": "failed", "detail": f"schema gate failed: {gate['issues']}"}
|
|
597
|
+
|
|
598
|
+
# local deterministic stages
|
|
599
|
+
if stage == "adjudicate":
|
|
600
|
+
result = _run_adjudicate(ws, question, demo_pack=demo_pack)
|
|
601
|
+
elif stage == "present":
|
|
602
|
+
result = _run_present(ws, ws.load_manifest(), question, demo_pack=demo_pack)
|
|
603
|
+
else:
|
|
604
|
+
# demo/test seeding
|
|
605
|
+
if demo_pack is not None:
|
|
606
|
+
seeded = _seed_from_demo(ws, stage, demo_pack)
|
|
607
|
+
if seeded["seeded"]:
|
|
608
|
+
gate = schema_gate(ws, stage)
|
|
609
|
+
if gate["passed"]:
|
|
610
|
+
ws.mark_stage(stage, "completed",
|
|
611
|
+
detail=f"demo-seeded from {demo_pack.name}; schema-valid",
|
|
612
|
+
artifacts=[spec["artifact"]])
|
|
613
|
+
ws.trace("stage_completed", stage=stage, detail="demo seed, schema gate passed")
|
|
614
|
+
return {"status": "completed", "detail": seeded["detail"]}
|
|
615
|
+
ws.mark_stage(stage, "failed", detail=f"demo seed failed schema gate: {gate['issues']}")
|
|
616
|
+
return {"status": "failed", "detail": f"demo seed failed schema gate: {gate['issues']}"}
|
|
617
|
+
if seeded["reason"]:
|
|
618
|
+
return {"status": "pending", "detail": seeded["reason"]}
|
|
619
|
+
|
|
620
|
+
ws.write_brief(stage, question, _STAGE_BRIEFS[stage])
|
|
621
|
+
ws.mark_stage(stage, "pending",
|
|
622
|
+
detail=f"awaiting external agent; brief at task-briefs/{stage}.md")
|
|
623
|
+
return {"status": "pending",
|
|
624
|
+
"detail": f"external stage; brief written to task-briefs/{stage}.md"}
|
|
625
|
+
|
|
626
|
+
if result["status"] == "completed":
|
|
627
|
+
ws.mark_stage(stage, "completed", detail=result["detail"],
|
|
628
|
+
artifacts=[spec["artifact"]])
|
|
629
|
+
elif result["status"] == "failed":
|
|
630
|
+
ws.mark_stage(stage, "failed", detail=result["detail"])
|
|
631
|
+
else:
|
|
632
|
+
ws.mark_stage(stage, "pending", detail=result["detail"])
|
|
633
|
+
return result
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _seed_from_demo(ws: RunWorkspace, stage: str, demo_pack: Path) -> dict[str, Any]:
|
|
637
|
+
"""Copy/derive the stage artifact from a demo pack (tests + --demo-pack)."""
|
|
638
|
+
pack = Path(demo_pack)
|
|
639
|
+
if not pack.is_dir():
|
|
640
|
+
return {"seeded": False, "reason": f"demo pack {demo_pack} not found"}
|
|
641
|
+
evidence = load_jsonl(pack / "evidence.jsonl")
|
|
642
|
+
|
|
643
|
+
if stage == "frame":
|
|
644
|
+
if (pack / "frame.json").is_file():
|
|
645
|
+
(ws.path / "frame.json").write_bytes((pack / "frame.json").read_bytes())
|
|
646
|
+
return {"seeded": True, "detail": "frame.json seeded from demo pack"}
|
|
647
|
+
elif stage == "retrieve":
|
|
648
|
+
if (pack / "sources.jsonl").is_file():
|
|
649
|
+
(ws.path / "sources.jsonl").write_bytes((pack / "sources.jsonl").read_bytes())
|
|
650
|
+
elif evidence:
|
|
651
|
+
save_jsonl(ws.path / "sources.jsonl", derive_sources_from_evidence(evidence))
|
|
652
|
+
else:
|
|
653
|
+
return {"seeded": False, "reason": "demo pack has no sources/evidence to derive from"}
|
|
654
|
+
return {"seeded": True, "detail": "sources.jsonl seeded from demo pack"}
|
|
655
|
+
elif stage == "extract":
|
|
656
|
+
if not evidence:
|
|
657
|
+
return {"seeded": False, "reason": "demo pack has no evidence.jsonl"}
|
|
658
|
+
(ws.path / "evidence.jsonl").write_bytes((pack / "evidence.jsonl").read_bytes())
|
|
659
|
+
return {"seeded": True, "detail": "evidence.jsonl seeded from demo pack"}
|
|
660
|
+
elif stage == "challenge":
|
|
661
|
+
(ws.path / "skeptic.json").write_text(
|
|
662
|
+
json.dumps(derive_skeptic_from_evidence(evidence), ensure_ascii=False, indent=2) + "\n",
|
|
663
|
+
encoding="utf-8")
|
|
664
|
+
return {"seeded": True, "detail": "skeptic.json derived from evidence (demo)"}
|
|
665
|
+
elif stage == "audit":
|
|
666
|
+
if (pack / "methodology.json").is_file():
|
|
667
|
+
(ws.path / "methodology.json").write_bytes((pack / "methodology.json").read_bytes())
|
|
668
|
+
return {"seeded": True, "detail": "methodology.json seeded from demo pack"}
|
|
669
|
+
elif stage == "intervene":
|
|
670
|
+
if (pack / "intervention.json").is_file():
|
|
671
|
+
(ws.path / "intervention.json").write_bytes((pack / "intervention.json").read_bytes())
|
|
672
|
+
return {"seeded": True, "detail": "intervention.json seeded from demo pack"}
|
|
673
|
+
elif stage == "evaluate":
|
|
674
|
+
if (pack / "evaluation.json").is_file():
|
|
675
|
+
(ws.path / "evaluation.json").write_bytes((pack / "evaluation.json").read_bytes())
|
|
676
|
+
return {"seeded": True, "detail": "evaluation.json seeded from demo pack"}
|
|
677
|
+
return {"seeded": False, "reason": f"demo pack has no artifact for stage {stage}"}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def advance(ws: RunWorkspace, *, demo_pack: Path | None = None) -> dict[str, Any]:
|
|
681
|
+
"""Resume loop: advance stages in order until blocked (Phase 32).
|
|
682
|
+
|
|
683
|
+
Completed stages are skipped; pending/failed stages stop the pass so an
|
|
684
|
+
external agent can act, then ``resume`` continues.
|
|
685
|
+
"""
|
|
686
|
+
summary = {"run_id": ws.run_id, "advanced": [], "blocked_on": None,
|
|
687
|
+
"failures": [], "completed_all": False}
|
|
688
|
+
state = ws.load_state()
|
|
689
|
+
for stage in STAGES:
|
|
690
|
+
status = state["stages"].get(stage, {}).get("status", "pending")
|
|
691
|
+
if status == "completed":
|
|
692
|
+
# crash/interruption safety: a completed stage whose primary artifact
|
|
693
|
+
# vanished (or was truncated to an empty seed) must re-run.
|
|
694
|
+
artifact = STAGE_SPEC[stage]["artifact"]
|
|
695
|
+
path = ws.path / artifact
|
|
696
|
+
if path.is_file() and path.stat().st_size > 2:
|
|
697
|
+
continue
|
|
698
|
+
ws.mark_stage(stage, "pending",
|
|
699
|
+
detail="artifact missing on resume; stage will re-run")
|
|
700
|
+
status = "pending"
|
|
701
|
+
result = run_stage(ws, stage, demo_pack=demo_pack)
|
|
702
|
+
summary["advanced"].append({"stage": stage, **result})
|
|
703
|
+
if result["status"] == "pending":
|
|
704
|
+
summary["blocked_on"] = stage
|
|
705
|
+
break
|
|
706
|
+
if result["status"] == "failed":
|
|
707
|
+
plan = handle_failure("SCHEMA_INVALID" if "schema" in result["detail"] else "TOOL_FAILURE")
|
|
708
|
+
summary["failures"].append({"stage": stage, "detail": result["detail"],
|
|
709
|
+
"handling": plan["action"]})
|
|
710
|
+
ws.trace("stage_failed", stage=stage, detail=result["detail"])
|
|
711
|
+
summary["blocked_on"] = stage
|
|
712
|
+
break
|
|
713
|
+
state = ws.load_state() # refresh after stage writes
|
|
714
|
+
|
|
715
|
+
state = ws.load_state()
|
|
716
|
+
remaining = [s for s in STAGES if state["stages"].get(s, {}).get("status") != "completed"]
|
|
717
|
+
if not remaining:
|
|
718
|
+
state = ws.save_state({"status": "completed", "current_stage": STAGES[-1]})
|
|
719
|
+
summary["completed_all"] = True
|
|
720
|
+
ws.trace("run_completed", detail="all stages completed")
|
|
721
|
+
else:
|
|
722
|
+
run_status = "failed" if summary["failures"] else "running"
|
|
723
|
+
state = ws.save_state({"status": run_status, "current_stage": remaining[0]})
|
|
724
|
+
summary["blocked_on"] = summary.get("blocked_on") or remaining[0]
|
|
725
|
+
summary["current_stage"] = state["current_stage"]
|
|
726
|
+
summary["status"] = state["status"]
|
|
727
|
+
return summary
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def interactive_agent_mcp_setup(approved: bool) -> bool:
|
|
731
|
+
"""W3.1 启动授权流程:检测 → 提示推荐启用 Agent MCP → 用户授权。
|
|
732
|
+
|
|
733
|
+
保留 GitHub 版全部行为(--approve-agent-mcp 旗标、agent_mcp_approval.json、
|
|
734
|
+
safe_spawn 门);此处只补 run 启动时的交互提示层。非交互终端直接返回原值。
|
|
735
|
+
"""
|
|
736
|
+
if approved:
|
|
737
|
+
return True
|
|
738
|
+
try:
|
|
739
|
+
from integrations.agent_mcp import detect_agent_mcp
|
|
740
|
+
detection = detect_agent_mcp()
|
|
741
|
+
except Exception:
|
|
742
|
+
detection = {"state": "unavailable", "mode": "platform_native",
|
|
743
|
+
"hint": "", "reasons": ["detect failed"]}
|
|
744
|
+
|
|
745
|
+
state_ = detection.get("state", "unavailable")
|
|
746
|
+
print("------------------------------------------------------------")
|
|
747
|
+
print(f"[startup] 执行模式: {detection.get('mode', 'platform_native')}"
|
|
748
|
+
f" | Agent MCP 状态: {state_}")
|
|
749
|
+
if detection.get("hint"):
|
|
750
|
+
print(f"[startup] 提示: {detection['hint']}")
|
|
751
|
+
print(" 启用 Agent MCP 增强 = 多 CLI/多模型分工、独立子上下文、"
|
|
752
|
+
"超时恢复与成本优化、Memory Bank;未启用自动降级 platform-native。")
|
|
753
|
+
print("------------------------------------------------------------")
|
|
754
|
+
|
|
755
|
+
if state_ != "available" and state_ != "daemon_reachable_undeclared":
|
|
756
|
+
print("[startup] Agent MCP 不可用(未安装或 daemon 未启动),本次以 platform-native 运行。")
|
|
757
|
+
return False
|
|
758
|
+
if not sys.stdin.isatty():
|
|
759
|
+
print("[startup] 非交互终端:需授权请使用 --approve-agent-mcp。")
|
|
760
|
+
return False
|
|
761
|
+
|
|
762
|
+
if state_ == "daemon_reachable_undeclared":
|
|
763
|
+
answer = input("检测到 daemon 在运行但未声明安装。是否写入 AGENT_MCP_INSTALLED=1 "
|
|
764
|
+
"到 ~/.eduevidence/env?[Y/n] ").strip().lower()
|
|
765
|
+
if answer in ("n", "no"):
|
|
766
|
+
return False
|
|
767
|
+
try:
|
|
768
|
+
env_file = os.path.expanduser("~/.eduevidence/env")
|
|
769
|
+
os.makedirs(os.path.dirname(env_file), exist_ok=True)
|
|
770
|
+
lines = []
|
|
771
|
+
if os.path.exists(env_file):
|
|
772
|
+
lines = Path(env_file).read_text(encoding="utf-8").splitlines()
|
|
773
|
+
if not any(l.strip().startswith("AGENT_MCP_INSTALLED=") for l in lines):
|
|
774
|
+
lines.append("AGENT_MCP_INSTALLED=1")
|
|
775
|
+
Path(env_file).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
776
|
+
print(f"[startup] 已写入 {env_file}")
|
|
777
|
+
except OSError as exc:
|
|
778
|
+
print(f"[startup] 写入失败: {exc};继续 platform-native。")
|
|
779
|
+
return False
|
|
780
|
+
|
|
781
|
+
answer = input("是否启用 Agent MCP 增强模式(推荐)?[Y/n] ").strip().lower()
|
|
782
|
+
return answer not in ("n", "no")
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
786
|
+
approve = args.approve_agent_mcp or interactive_agent_mcp_setup(args.approve_agent_mcp)
|
|
787
|
+
ws = init_run(Path(args.runs_dir), args.question, depth=args.depth, run_id=args.run_id,
|
|
788
|
+
approve_agent_mcp=approve)
|
|
789
|
+
print(f"workspace created: {ws.path}")
|
|
790
|
+
print(f"manifest: {json.dumps(ws.load_manifest(), ensure_ascii=False, indent=2)}")
|
|
791
|
+
if args.dry_run:
|
|
792
|
+
print("[dry-run] workspace initialized; stage execution skipped")
|
|
793
|
+
return 0
|
|
794
|
+
summary = advance(ws, demo_pack=args.demo_pack)
|
|
795
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
796
|
+
if summary["failures"]:
|
|
797
|
+
return 1
|
|
798
|
+
return 0
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _print_status(ws) -> None:
|
|
802
|
+
state = ws.load_state()
|
|
803
|
+
print(f"run_id: {ws.run_id}")
|
|
804
|
+
print(f"question: {state.get('question', '')[:120]}")
|
|
805
|
+
print(f"status: {state.get('status')} current_stage: {state.get('current_stage')}")
|
|
806
|
+
print("stages:")
|
|
807
|
+
for stage in STAGES:
|
|
808
|
+
row = state["stages"].get(stage, {})
|
|
809
|
+
detail = row.get("detail", "")
|
|
810
|
+
print(f" {stage:12} {row.get('status', 'pending'):10} {detail}")
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _cmd_resume(args: argparse.Namespace) -> int:
|
|
814
|
+
ws = RunWorkspace(Path(args.runs_dir), args.run_id)
|
|
815
|
+
if not ws.exists():
|
|
816
|
+
print(f"ERROR: no run {args.run_id} under {args.runs_dir}", file=sys.stderr)
|
|
817
|
+
return 2
|
|
818
|
+
summary = advance(ws, demo_pack=args.demo_pack)
|
|
819
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
820
|
+
return 1 if summary["failures"] else 0
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _cmd_status(args: argparse.Namespace) -> int:
|
|
824
|
+
ws = RunWorkspace(Path(args.runs_dir), args.run_id)
|
|
825
|
+
if not ws.exists():
|
|
826
|
+
print(f"ERROR: no run {args.run_id} under {args.runs_dir}", file=sys.stderr)
|
|
827
|
+
return 2
|
|
828
|
+
_print_status(ws)
|
|
829
|
+
return 0
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
833
|
+
runs_dir = Path(args.runs_dir)
|
|
834
|
+
if not runs_dir.is_dir():
|
|
835
|
+
print("no runs yet")
|
|
836
|
+
return 0
|
|
837
|
+
for entry in sorted(runs_dir.iterdir()):
|
|
838
|
+
if not entry.is_dir():
|
|
839
|
+
continue
|
|
840
|
+
ws = RunWorkspace(runs_dir, entry.name)
|
|
841
|
+
if not ws.exists():
|
|
842
|
+
continue
|
|
843
|
+
state = ws.load_state()
|
|
844
|
+
print(f"{entry.name:24} {state.get('status', '?'):10} {state.get('question', '')[:80]}")
|
|
845
|
+
return 0
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _cmd_gate(args: argparse.Namespace) -> int:
|
|
849
|
+
ws = RunWorkspace(Path(args.runs_dir), args.run_id)
|
|
850
|
+
if not ws.exists():
|
|
851
|
+
print(f"ERROR: no run {args.run_id} under {args.runs_dir}", file=sys.stderr)
|
|
852
|
+
return 2
|
|
853
|
+
return main_gate(argv=["--workspace", str(ws.path),
|
|
854
|
+
*(["--require-final"] if args.require_final else []),
|
|
855
|
+
"--json"])
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def main_gate(argv: list[str] | None = None) -> int:
|
|
859
|
+
"""Re-export of the Pre-Verdict Gate CLI (used by `eduevidence gate`)."""
|
|
860
|
+
from pre_verdict_gate import main as gate_main
|
|
861
|
+
return gate_main(argv)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# ---- V4 command handlers -----------------------------------------------
|
|
867
|
+
|
|
868
|
+
def _cmd_domain(args) -> int:
|
|
869
|
+
from engine.evidencecore import list_domains, load_domain
|
|
870
|
+
if args.action == "list":
|
|
871
|
+
for d in list_domains():
|
|
872
|
+
print(d["id"], "|", d["description"][:80])
|
|
873
|
+
return 0
|
|
874
|
+
if args.action == "select":
|
|
875
|
+
domain = load_domain(args.domain)
|
|
876
|
+
print(f"selected domain: {domain['id']} (frame_schema={domain['frame_schema']})")
|
|
877
|
+
return 0
|
|
878
|
+
if args.action == "check":
|
|
879
|
+
domain = load_domain(args.domain)
|
|
880
|
+
try:
|
|
881
|
+
import json as _json
|
|
882
|
+
from pathlib import Path as _P
|
|
883
|
+
_json.load(open(_P(__file__).resolve().parent.parent / domain["frame_schema"]))
|
|
884
|
+
print(f"domain {domain['id']}: contracts OK (frame schema loads)")
|
|
885
|
+
return 0
|
|
886
|
+
except Exception as exc:
|
|
887
|
+
print(f"ERROR: domain {domain['id']} contract broken: {exc}", file=sys.stderr)
|
|
888
|
+
return 1
|
|
889
|
+
return 2
|
|
890
|
+
|
|
891
|
+
def _cmd_living(args) -> int:
|
|
892
|
+
from engine.project import ProjectWorkspace
|
|
893
|
+
from engine.living import create_subscription, refresh, set_subscription_status
|
|
894
|
+
home = _home(args)
|
|
895
|
+
ws = ProjectWorkspace.open(home, args.project)
|
|
896
|
+
if args.action == "subscribe":
|
|
897
|
+
sub = create_subscription(
|
|
898
|
+
ws, decision_snapshot_id=args.decision,
|
|
899
|
+
query_terms=args.term or [])
|
|
900
|
+
print(sub["subscription_id"])
|
|
901
|
+
return 0
|
|
902
|
+
if args.action == "refresh":
|
|
903
|
+
result = refresh(
|
|
904
|
+
ws, args.subscription,
|
|
905
|
+
new_evidence=args.evidence_file and [dict(json.loads(l)) for l in open(args.evidence_file) if l.strip()] or None,
|
|
906
|
+
retriever=None)
|
|
907
|
+
drift = result["drift"]
|
|
908
|
+
print(f"drift {drift['drift_id']}: rev {drift['from_revision']}->{drift['to_revision']} | "
|
|
909
|
+
f"suggested_action={drift['suggested_action']}")
|
|
910
|
+
print(drift.get("summary", ""))
|
|
911
|
+
return 0
|
|
912
|
+
if args.action == "status":
|
|
913
|
+
from pathlib import Path as _P
|
|
914
|
+
import json as _json
|
|
915
|
+
p = ws.path / "living" / "subscriptions" / f"{args.subscription}.json"
|
|
916
|
+
if not p.is_file():
|
|
917
|
+
print(f"ERROR: subscription not found: {args.subscription}", file=sys.stderr)
|
|
918
|
+
return 2
|
|
919
|
+
sub = _json.loads(p.read_text(encoding="utf-8"))
|
|
920
|
+
print(f"{sub['subscription_id']} | status={sub['status']} | "
|
|
921
|
+
f"decision={sub['decision_snapshot_id']} | terms={sub['query_terms']}")
|
|
922
|
+
return 0
|
|
923
|
+
return 2
|
|
924
|
+
|
|
925
|
+
def _cmd_benchmark_judge(args) -> int:
|
|
926
|
+
import benchmark_judge as bj
|
|
927
|
+
if args.action == "run":
|
|
928
|
+
return bj.main(["run", "--run", args.run, "--out", args.out, "--limit", str(args.limit)])
|
|
929
|
+
if args.action == "report":
|
|
930
|
+
return bj.main(["report", "--out", args.report])
|
|
931
|
+
return 2
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
# ---- V3 command handlers -----------------------------------------------
|
|
935
|
+
|
|
936
|
+
def _cmd_pilot(args) -> int:
|
|
937
|
+
from engine.project import ProjectWorkspace
|
|
938
|
+
from engine.pilot import import_outcomes, link_analysis, redecide, register_pilot
|
|
939
|
+
home = _home(args)
|
|
940
|
+
ws = ProjectWorkspace.open(home, args.project)
|
|
941
|
+
if args.action == "register":
|
|
942
|
+
pilot = register_pilot(
|
|
943
|
+
ws, decision_snapshot_id=args.decision, title=args.title,
|
|
944
|
+
start_date=args.start, end_date=args.end,
|
|
945
|
+
conditions=args.condition, sample_size=args.sample,
|
|
946
|
+
design_id=args.design,
|
|
947
|
+
anon_policy={"no_pii_columns": True, "note": "CLI default: PII columns refused"},
|
|
948
|
+
outcome_columns=args.outcome)
|
|
949
|
+
print(pilot["pilot_id"])
|
|
950
|
+
return 0
|
|
951
|
+
if args.action == "import":
|
|
952
|
+
import_outcomes(
|
|
953
|
+
ws, args.pilot, source_path=args.file,
|
|
954
|
+
privacy={"classification": args.privacy, "deidentification_status": "done"})
|
|
955
|
+
print("imported", args.file, "->", args.pilot)
|
|
956
|
+
return 0
|
|
957
|
+
if args.action == "analyze-link":
|
|
958
|
+
link_analysis(ws, args.pilot, analysis_run_id=args.analysis)
|
|
959
|
+
print("linked analysis", args.analysis)
|
|
960
|
+
return 0
|
|
961
|
+
if args.action == "redecide":
|
|
962
|
+
result = redecide(
|
|
963
|
+
ws, args.pilot, claim_id=args.claim, outcome_token=args.outcome,
|
|
964
|
+
measure=args.measure, effect_direction=args.effect,
|
|
965
|
+
raw_result_text=args.result_text, relation_to_claim=args.relation,
|
|
966
|
+
effect_estimate=({"value": args.effect_value} if args.effect_value is not None else None))
|
|
967
|
+
print("new decision:", result["snapshot"]["decision_snapshot_id"],
|
|
968
|
+
result["snapshot"]["decision"], result["snapshot"]["confidence_label"])
|
|
969
|
+
print("diff:", json.dumps(result["diff"], ensure_ascii=False)[:300])
|
|
970
|
+
return 0
|
|
971
|
+
return 2
|
|
972
|
+
|
|
973
|
+
def _cmd_synthesize(args) -> int:
|
|
974
|
+
from engine.library import ResearchLibrary
|
|
975
|
+
from engine.meta_synthesis import save_synthesis, synthesize_library
|
|
976
|
+
home = _home(args)
|
|
977
|
+
lib = ResearchLibrary.open(home)
|
|
978
|
+
syn = synthesize_library(lib)
|
|
979
|
+
out_dir = home / "syntheses" if args.out is None else args.out
|
|
980
|
+
path = save_synthesis(syn, out_dir)
|
|
981
|
+
print(f"wrote {path} (revision {syn['library_revision']}, studies {syn['independent_studies']})")
|
|
982
|
+
return 0
|
|
983
|
+
|
|
984
|
+
def _cmd_benchmark(args) -> int:
|
|
985
|
+
import benchmark_v3 as bv3
|
|
986
|
+
if args.action == "run":
|
|
987
|
+
return bv3.main(["run", "--baselines", args.baselines, "--questions", args.questions,
|
|
988
|
+
"--repeats", str(args.repeats), "--driver", args.driver,
|
|
989
|
+
"--out", args.out, "--budget-tokens", str(args.budget)])
|
|
990
|
+
if args.action == "eval":
|
|
991
|
+
return bv3.main(["eval", "--run", args.run, "--annotations", args.annotations])
|
|
992
|
+
if args.action == "report":
|
|
993
|
+
return bv3.main(["report", "--run", args.run, "--out", args.report])
|
|
994
|
+
return 2
|
|
995
|
+
|
|
996
|
+
# ---- V2 command handlers --------------------------------------------------
|
|
997
|
+
|
|
998
|
+
def _home(args) -> "object":
|
|
999
|
+
from engine.paths import resolve_home
|
|
1000
|
+
return resolve_home(getattr(args, "home", None))
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _cmd_project(args) -> int:
|
|
1004
|
+
from engine.project import ProjectWorkspace
|
|
1005
|
+
home = _home(args)
|
|
1006
|
+
if args.action == "create":
|
|
1007
|
+
if not args.question:
|
|
1008
|
+
print("project create requires --question", file=sys.stderr)
|
|
1009
|
+
return 2
|
|
1010
|
+
ws = ProjectWorkspace.create(
|
|
1011
|
+
home, question=args.question, title=args.title or args.question,
|
|
1012
|
+
research_mode=args.mode)
|
|
1013
|
+
print(ws.project_id)
|
|
1014
|
+
return 0
|
|
1015
|
+
if args.action == "list":
|
|
1016
|
+
projects_dir = home / "projects"
|
|
1017
|
+
if not projects_dir.is_dir():
|
|
1018
|
+
return 0
|
|
1019
|
+
for p in sorted(projects_dir.iterdir()):
|
|
1020
|
+
manifest = p / "project.json"
|
|
1021
|
+
if manifest.is_file():
|
|
1022
|
+
import json
|
|
1023
|
+
m = json.loads(manifest.read_text(encoding="utf-8"))
|
|
1024
|
+
print(f"{m['project_id']}\t{m['status']}\trev {m['graph_revision']}\t{m['question'][:60]}")
|
|
1025
|
+
return 0
|
|
1026
|
+
# status
|
|
1027
|
+
ws = ProjectWorkspace.open(home, args.project)
|
|
1028
|
+
m = ws.manifest()
|
|
1029
|
+
print(f"project: {m['project_id']}")
|
|
1030
|
+
print(f"mode: {m['research_mode']} target: {m['decision_target']}")
|
|
1031
|
+
print(f"status: {m['status']} graph_revision: {m['graph_revision']}")
|
|
1032
|
+
return 0
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _cmd_research(args) -> int:
|
|
1036
|
+
from engine.mode_router import recommend_mode
|
|
1037
|
+
from engine.planner import build_research_plan
|
|
1038
|
+
from engine.project import ProjectWorkspace
|
|
1039
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1040
|
+
if args.action == "plan":
|
|
1041
|
+
intent = {
|
|
1042
|
+
"decision_target": ws.manifest()["decision_target"],
|
|
1043
|
+
"wants_existing_evidence": True,
|
|
1044
|
+
"wants_study_design": ws.manifest()["research_mode"] == "full_research_cycle",
|
|
1045
|
+
"has_user_data": False,
|
|
1046
|
+
"wants_data_analysis": False,
|
|
1047
|
+
"wants_decision_update": False,
|
|
1048
|
+
}
|
|
1049
|
+
rec = recommend_mode(intent, project_has_grounding=ws.current_revision() > 0)
|
|
1050
|
+
plan = build_research_plan(
|
|
1051
|
+
mode=rec.mode, decision_target=ws.manifest()["decision_target"],
|
|
1052
|
+
depth="standard", has_grounding=ws.current_revision() > 0,
|
|
1053
|
+
has_dataset=False)
|
|
1054
|
+
print(f"mode: {rec.mode}")
|
|
1055
|
+
for step in plan:
|
|
1056
|
+
print(f" {step.kind:10s} {step.capability_id or step.wait_state}")
|
|
1057
|
+
return 0
|
|
1058
|
+
# run/resume: create a Run record
|
|
1059
|
+
from engine.run import start_run
|
|
1060
|
+
run = start_run(ws, purpose="research run", capabilities=[],
|
|
1061
|
+
execution_backend="sequential_main_agent")
|
|
1062
|
+
print(run["run_id"])
|
|
1063
|
+
return 0
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _cmd_graph(args) -> int:
|
|
1067
|
+
from engine.graph_store import GraphStore
|
|
1068
|
+
from engine.project import ProjectWorkspace
|
|
1069
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1070
|
+
store = GraphStore.create(ws)
|
|
1071
|
+
problems = store.validate()
|
|
1072
|
+
if problems:
|
|
1073
|
+
for p in problems:
|
|
1074
|
+
print(p, file=sys.stderr)
|
|
1075
|
+
return 1
|
|
1076
|
+
print(f"graph valid at revision {store.active_revision()}")
|
|
1077
|
+
return 0
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _cmd_study(args) -> int:
|
|
1081
|
+
from engine.study_design import validate_design_grounding
|
|
1082
|
+
from engine.project import ProjectWorkspace
|
|
1083
|
+
from engine.ids import new_local_id
|
|
1084
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1085
|
+
question = getattr(args, "question", None) or "grounded study"
|
|
1086
|
+
design = {
|
|
1087
|
+
"design_id": new_local_id("DSN", set()),
|
|
1088
|
+
"gap_ids": args.gap,
|
|
1089
|
+
"research_question": question,
|
|
1090
|
+
"design_type": getattr(args, "design_type", "rct") or "rct",
|
|
1091
|
+
"population": getattr(args, "population", "") or "unspecified",
|
|
1092
|
+
"sampling_plan": getattr(args, "sampling", "") or "unspecified",
|
|
1093
|
+
"intervention": getattr(args, "intervention", None),
|
|
1094
|
+
"comparison": getattr(args, "comparison", None),
|
|
1095
|
+
"outcomes": [getattr(args, "outcome", "outcome")] if getattr(args, "outcome", None) else ["outcome"],
|
|
1096
|
+
"measures": [getattr(args, "measure", "measure")] if getattr(args, "measure", None) else ["measure"],
|
|
1097
|
+
"timepoints": ["post"],
|
|
1098
|
+
"assignment_strategy": getattr(args, "assignment", "") or "unspecified",
|
|
1099
|
+
"confounder_plan": "",
|
|
1100
|
+
"analysis_requirements": ["descriptive_statistics"],
|
|
1101
|
+
"success_criteria": [],
|
|
1102
|
+
"stop_conditions": [],
|
|
1103
|
+
"ethics_flags": {"human_subjects": True, "sensitive_data": False,
|
|
1104
|
+
"minors_involved": False, "consent_status": "unknown",
|
|
1105
|
+
"ethics_review_required": True,
|
|
1106
|
+
"deidentification_required": False},
|
|
1107
|
+
"preregistration_fields": {},
|
|
1108
|
+
"derived_from_graph_revision": ws.current_revision(),
|
|
1109
|
+
"created_at": __import__("datetime").datetime.now(
|
|
1110
|
+
__import__("datetime").timezone.utc).isoformat(),
|
|
1111
|
+
"extensions": {},
|
|
1112
|
+
}
|
|
1113
|
+
errors = validate_design_grounding(ws, design)
|
|
1114
|
+
if errors:
|
|
1115
|
+
for e in errors:
|
|
1116
|
+
print(e, file=sys.stderr)
|
|
1117
|
+
return 1
|
|
1118
|
+
from engine.study_design import save_study_design
|
|
1119
|
+
path = save_study_design(ws, design)
|
|
1120
|
+
print(design["design_id"])
|
|
1121
|
+
return 0
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _cmd_data(args) -> int:
|
|
1125
|
+
from engine.datasets import ingest_dataset
|
|
1126
|
+
from engine.project import ProjectWorkspace
|
|
1127
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1128
|
+
asset = ingest_dataset(
|
|
1129
|
+
ws, design_id=args.design, source_path=args.file,
|
|
1130
|
+
privacy={"classification": args.privacy, "deidentification_status": "not_done",
|
|
1131
|
+
"consent_metadata": None})
|
|
1132
|
+
print(asset["dataset_id"])
|
|
1133
|
+
return 0
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _cmd_analyze(args) -> int:
|
|
1137
|
+
from engine.analysis import run_native_descriptive
|
|
1138
|
+
from engine.project import ProjectWorkspace
|
|
1139
|
+
import json
|
|
1140
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1141
|
+
plan_path = ws.path / "analyses" / f"{args.plan}.json"
|
|
1142
|
+
if not plan_path.is_file():
|
|
1143
|
+
print(f"plan not found: {plan_path}", file=sys.stderr)
|
|
1144
|
+
return 1
|
|
1145
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
1146
|
+
run = run_native_descriptive(ws, plan)
|
|
1147
|
+
print(json.dumps(run, ensure_ascii=False, indent=2))
|
|
1148
|
+
return 0
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _cmd_adjudicate(args) -> int:
|
|
1152
|
+
from engine.graph_store import GraphStore
|
|
1153
|
+
from engine.project import ProjectWorkspace
|
|
1154
|
+
from engine.tribunal import adjudicate, save_decision_snapshot
|
|
1155
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1156
|
+
store = GraphStore.create(ws)
|
|
1157
|
+
snap = adjudicate(store, project=ws)
|
|
1158
|
+
path = save_decision_snapshot(ws, snap)
|
|
1159
|
+
print(f"{snap['decision']} / {snap['confidence_label']} ({path})")
|
|
1160
|
+
return 0
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _cmd_report(args) -> int:
|
|
1164
|
+
from engine.project import ProjectWorkspace
|
|
1165
|
+
from engine.projections import build_v1_compat_result
|
|
1166
|
+
ws = ProjectWorkspace.open(_home(args), args.project)
|
|
1167
|
+
compat = build_v1_compat_result(ws)
|
|
1168
|
+
out = ws.path / "projections" / f"report-rev-{ws.current_revision():06d}.json"
|
|
1169
|
+
out.write_text(__import__("json").dumps(compat, ensure_ascii=False, indent=2) + "\n",
|
|
1170
|
+
encoding="utf-8")
|
|
1171
|
+
print(out)
|
|
1172
|
+
return 0
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def _cmd_migrate(args) -> int:
|
|
1176
|
+
from engine.migration import migrate_v1_pack
|
|
1177
|
+
result = migrate_v1_pack(args.pack, home=_home(args), title=args.title)
|
|
1178
|
+
print(f"{result.project_id} rev {result.graph_revision}")
|
|
1179
|
+
for w in result.warnings:
|
|
1180
|
+
print(f"warning: {w}", file=sys.stderr)
|
|
1181
|
+
return 0
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def _cmd_dashboard(args) -> int:
|
|
1185
|
+
from dashboard_server import run_dashboard_server
|
|
1186
|
+
run_dashboard_server(host=args.host, port=args.port)
|
|
1187
|
+
return 0
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _cmd_search(args) -> int:
|
|
1191
|
+
from retrieval.search import search_evidence
|
|
1192
|
+
hits = search_evidence(args.query, limit=args.limit, academic_only=args.academic)
|
|
1193
|
+
print(json.dumps(hits, indent=2, ensure_ascii=False))
|
|
1194
|
+
return 0
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _cmd_did(args) -> int:
|
|
1198
|
+
from did_regression import run_did_analysis
|
|
1199
|
+
res = run_did_analysis(str(args.csv))
|
|
1200
|
+
print(json.dumps(res, indent=2, ensure_ascii=False))
|
|
1201
|
+
return 0 if res.get("status") == "success" else 1
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _cmd_effect(args) -> int:
|
|
1205
|
+
from effect_calculator import compute_hedges_g
|
|
1206
|
+
res = compute_hedges_g(args.mean1, args.sd1, args.n1, args.mean2, args.sd2, args.n2)
|
|
1207
|
+
print(json.dumps(res, indent=2, ensure_ascii=False))
|
|
1208
|
+
return 0
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def _cmd_lint(args) -> int:
|
|
1212
|
+
from skill_lint import lint_skill
|
|
1213
|
+
errs = lint_skill()
|
|
1214
|
+
if errs:
|
|
1215
|
+
for e in errs:
|
|
1216
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1217
|
+
return 1
|
|
1218
|
+
print("Skill lint passed.")
|
|
1219
|
+
return 0
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1223
|
+
parser = argparse.ArgumentParser(
|
|
1224
|
+
prog="eduevidence",
|
|
1225
|
+
description="EduEvidence run orchestrator — stage routing, schema gates, resume, failures")
|
|
1226
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
1227
|
+
|
|
1228
|
+
p_run = sub.add_parser("run", help="create a run workspace and advance stages")
|
|
1229
|
+
p_run.add_argument("--question", required=True, help="education question to research")
|
|
1230
|
+
p_run.add_argument("--depth", default="M", choices=["quick", "standard", "deep", "S", "M", "L"],
|
|
1231
|
+
help="complexity depth (default: standard/M)")
|
|
1232
|
+
p_run.add_argument("--run-id", default=None, help="explicit run id (default: timestamp)")
|
|
1233
|
+
p_run.add_argument("--demo-pack", default=None, type=Path,
|
|
1234
|
+
help="seed external stages from an example pack (demo/test mode)")
|
|
1235
|
+
p_run.add_argument("--approve-agent-mcp", action="store_true",
|
|
1236
|
+
help="record agent-mcp approval in the manifest")
|
|
1237
|
+
p_run.add_argument("--dry-run", action="store_true", help="initialize only, do not advance")
|
|
1238
|
+
p_run.add_argument("--runs-dir", default=os.environ.get("EDUEVIDENCE_RUNS_DIR", str(ROOT / "runs")),
|
|
1239
|
+
help="directory holding run workspaces (default: <repo>/runs)")
|
|
1240
|
+
p_run.set_defaults(func=_cmd_run)
|
|
1241
|
+
|
|
1242
|
+
p_resume = sub.add_parser("resume", help="continue a run from its state.json")
|
|
1243
|
+
p_resume.add_argument("--run-id", required=True)
|
|
1244
|
+
p_resume.add_argument("--demo-pack", default=None, type=Path)
|
|
1245
|
+
p_resume.add_argument("--runs-dir", default=os.environ.get("EDUEVIDENCE_RUNS_DIR", str(ROOT / "runs")))
|
|
1246
|
+
p_resume.set_defaults(func=_cmd_resume)
|
|
1247
|
+
|
|
1248
|
+
p_status = sub.add_parser("status", help="show run state")
|
|
1249
|
+
p_status.add_argument("--run-id", required=True)
|
|
1250
|
+
p_status.add_argument("--runs-dir", default=os.environ.get("EDUEVIDENCE_RUNS_DIR", str(ROOT / "runs")))
|
|
1251
|
+
p_status.set_defaults(func=_cmd_status)
|
|
1252
|
+
|
|
1253
|
+
p_list = sub.add_parser("list", help="list runs")
|
|
1254
|
+
p_list.add_argument("--runs-dir", default=os.environ.get("EDUEVIDENCE_RUNS_DIR", str(ROOT / "runs")))
|
|
1255
|
+
p_list.set_defaults(func=_cmd_list)
|
|
1256
|
+
|
|
1257
|
+
p_gate = sub.add_parser("gate", help="run the Pre-Verdict Gate over a run")
|
|
1258
|
+
p_gate.add_argument("--run-id", required=True)
|
|
1259
|
+
p_gate.add_argument("--require-final", action="store_true")
|
|
1260
|
+
p_gate.add_argument("--runs-dir", default=os.environ.get("EDUEVIDENCE_RUNS_DIR", str(ROOT / "runs")))
|
|
1261
|
+
p_gate.set_defaults(func=_cmd_gate)
|
|
1262
|
+
|
|
1263
|
+
# ---- V2 project-scoped commands ------------------------------------
|
|
1264
|
+
p_proj_create = sub.add_parser("project", help="V2 project lifecycle")
|
|
1265
|
+
p_proj_create.add_argument("action", choices=["create", "list", "status"])
|
|
1266
|
+
p_proj_create.add_argument("--question", default=None, help="research question (create)")
|
|
1267
|
+
p_proj_create.add_argument("--title", default=None, help="project title (create)")
|
|
1268
|
+
p_proj_create.add_argument("--mode", default="evidence_review",
|
|
1269
|
+
choices=["evidence_review", "full_research_cycle"])
|
|
1270
|
+
p_proj_create.add_argument("--project", default=None, help="project id (status)")
|
|
1271
|
+
p_proj_create.add_argument("--home", default=None, help="EDUEVIDENCE_HOME override")
|
|
1272
|
+
p_proj_create.set_defaults(func=_cmd_project)
|
|
1273
|
+
|
|
1274
|
+
p_research = sub.add_parser("research", help="V2 research planning/run")
|
|
1275
|
+
p_research.add_argument("action", choices=["plan", "run", "resume"])
|
|
1276
|
+
p_research.add_argument("--project", required=True)
|
|
1277
|
+
p_research.add_argument("--home", default=None)
|
|
1278
|
+
p_research.set_defaults(func=_cmd_research)
|
|
1279
|
+
|
|
1280
|
+
p_graph = sub.add_parser("graph", help="V2 graph validation")
|
|
1281
|
+
p_graph.add_argument("action", choices=["validate"])
|
|
1282
|
+
p_graph.add_argument("--project", required=True)
|
|
1283
|
+
p_graph.add_argument("--home", default=None)
|
|
1284
|
+
p_graph.set_defaults(func=_cmd_graph)
|
|
1285
|
+
|
|
1286
|
+
p_study = sub.add_parser("study", help="V2 study design (grounded)")
|
|
1287
|
+
p_study.add_argument("action", choices=["design"])
|
|
1288
|
+
p_study.add_argument("--project", required=True)
|
|
1289
|
+
p_study.add_argument("--gap", action="append", default=[], help="GAP-xxx ids")
|
|
1290
|
+
p_study.add_argument("--question", default=None, help="research question")
|
|
1291
|
+
p_study.add_argument("--design-type", default="rct",
|
|
1292
|
+
choices=["rct", "cluster_rct", "quasi_experimental",
|
|
1293
|
+
"pre_post", "observational", "survey",
|
|
1294
|
+
"qualitative", "mixed_methods"])
|
|
1295
|
+
p_study.add_argument("--population", default=None)
|
|
1296
|
+
p_study.add_argument("--sampling", default=None)
|
|
1297
|
+
p_study.add_argument("--intervention", default=None)
|
|
1298
|
+
p_study.add_argument("--comparison", default=None)
|
|
1299
|
+
p_study.add_argument("--outcome", default=None)
|
|
1300
|
+
p_study.add_argument("--measure", default=None)
|
|
1301
|
+
p_study.add_argument("--assignment", default=None)
|
|
1302
|
+
p_study.add_argument("--home", default=None)
|
|
1303
|
+
|
|
1304
|
+
p_data = sub.add_parser("data", help="V2 dataset ingest")
|
|
1305
|
+
p_data.add_argument("action", choices=["ingest"])
|
|
1306
|
+
p_data.add_argument("--project", required=True)
|
|
1307
|
+
p_data.add_argument("--design", required=True)
|
|
1308
|
+
p_data.add_argument("--file", required=True, type=Path)
|
|
1309
|
+
p_data.add_argument("--privacy", default="internal",
|
|
1310
|
+
choices=["public", "internal", "confidential", "restricted"])
|
|
1311
|
+
p_data.add_argument("--home", default=None)
|
|
1312
|
+
p_data.set_defaults(func=_cmd_data)
|
|
1313
|
+
|
|
1314
|
+
p_analyze = sub.add_parser("analyze", help="V2 analysis (native descriptive)")
|
|
1315
|
+
p_analyze.add_argument("--project", required=True)
|
|
1316
|
+
p_analyze.add_argument("--plan", required=True)
|
|
1317
|
+
p_analyze.add_argument("--home", default=None)
|
|
1318
|
+
p_analyze.set_defaults(func=_cmd_analyze)
|
|
1319
|
+
|
|
1320
|
+
p_adjudicate = sub.add_parser("adjudicate", help="V2 tribunal over current graph")
|
|
1321
|
+
p_adjudicate.add_argument("--project", required=True)
|
|
1322
|
+
p_adjudicate.add_argument("--home", default=None)
|
|
1323
|
+
p_adjudicate.set_defaults(func=_cmd_adjudicate)
|
|
1324
|
+
|
|
1325
|
+
p_report = sub.add_parser("report", help="V2 projection/report")
|
|
1326
|
+
p_report.add_argument("--project", required=True)
|
|
1327
|
+
p_report.add_argument("--theme", default="claude",
|
|
1328
|
+
choices=["claude", "academic", "datalab", "datalab-dark", "presentation"])
|
|
1329
|
+
p_report.add_argument("--home", default=None)
|
|
1330
|
+
p_report.set_defaults(func=_cmd_report)
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
p_pilot = sub.add_parser("pilot", help="V3 Decision-to-Outcome Loop")
|
|
1334
|
+
p_pilot.add_argument("action", choices=["register", "import", "analyze-link", "redecide"])
|
|
1335
|
+
p_pilot.add_argument("--project", required=True)
|
|
1336
|
+
p_pilot.add_argument("--decision", default=None, help="decision snapshot id (register)")
|
|
1337
|
+
p_pilot.add_argument("--title", default=None, help="pilot title (register)")
|
|
1338
|
+
p_pilot.add_argument("--start", default=None, help="start ISO date-time (register)")
|
|
1339
|
+
p_pilot.add_argument("--end", default=None, help="end ISO date-time (register)")
|
|
1340
|
+
p_pilot.add_argument("--condition", action="append", default=[], help="implementation condition (register)")
|
|
1341
|
+
p_pilot.add_argument("--sample", type=int, default=None, help="sample size (register)")
|
|
1342
|
+
p_pilot.add_argument("--design", default=None, help="study design id (register)")
|
|
1343
|
+
p_pilot.add_argument("--outcome", action="append", default=[], help="outcome taxonomy token")
|
|
1344
|
+
p_pilot.add_argument("--pilot", default=None, help="pilot id (import/analyze-link/redecide)")
|
|
1345
|
+
p_pilot.add_argument("--file", type=Path, default=None, help="outcome CSV (import)")
|
|
1346
|
+
p_pilot.add_argument("--privacy", default="internal", choices=["public", "internal", "confidential", "restricted"])
|
|
1347
|
+
p_pilot.add_argument("--analysis", default=None, help="analysis run id (analyze-link)")
|
|
1348
|
+
p_pilot.add_argument("--claim", default=None, help="claim id (redecide)")
|
|
1349
|
+
p_pilot.add_argument("--measure", default=None, help="finding measure (redecide)")
|
|
1350
|
+
p_pilot.add_argument("--effect", default="positive", choices=["positive", "negative", "null"])
|
|
1351
|
+
p_pilot.add_argument("--result-text", default=None, help="raw result text (redecide)")
|
|
1352
|
+
p_pilot.add_argument("--relation", default="support", choices=["support", "contradict", "neutral"])
|
|
1353
|
+
p_pilot.add_argument("--effect-value", type=float, default=None)
|
|
1354
|
+
p_pilot.add_argument("--home", default=None)
|
|
1355
|
+
p_pilot.set_defaults(func=_cmd_pilot)
|
|
1356
|
+
|
|
1357
|
+
p_syn = sub.add_parser("synthesize", help="V3 cross-project library synthesis")
|
|
1358
|
+
p_syn.add_argument("--home", default=None)
|
|
1359
|
+
p_syn.add_argument("--out", default=None, type=Path)
|
|
1360
|
+
p_syn.set_defaults(func=_cmd_synthesize)
|
|
1361
|
+
|
|
1362
|
+
p_bench = sub.add_parser("benchmark", help="V3 Layer B empirical benchmark")
|
|
1363
|
+
p_bench.add_argument("action", choices=["run", "eval", "report"])
|
|
1364
|
+
p_bench.add_argument("--baselines", default="B2_standard_agent,B3_eduevidence_single")
|
|
1365
|
+
p_bench.add_argument("--questions", default="benchmarks/questions.jsonl")
|
|
1366
|
+
p_bench.add_argument("--repeats", type=int, default=3)
|
|
1367
|
+
p_bench.add_argument("--driver", default=None, choices=["api", "cli", "sim"],
|
|
1368
|
+
help="api | cli (omp) | sim (harness validation only); default: auto (api > cli > sim)")
|
|
1369
|
+
p_bench.add_argument("--out", default="benchmarks/empirical/run-001")
|
|
1370
|
+
p_bench.add_argument("--budget", type=int, default=1000000)
|
|
1371
|
+
p_bench.add_argument("--run", default=None, help="run dir (eval/report)")
|
|
1372
|
+
p_bench.add_argument("--annotations", default="benchmarks/annotations")
|
|
1373
|
+
p_bench.add_argument("--report", default="benchmarks/empirical/v3-report.md")
|
|
1374
|
+
p_bench.set_defaults(func=_cmd_benchmark)
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
p_domain = sub.add_parser("domain", help="V4 domain registry (EvidenceCore)")
|
|
1378
|
+
p_domain.add_argument("action", choices=["list", "select", "check"])
|
|
1379
|
+
p_domain.add_argument("--domain", default=None, help="domain id (select/check)")
|
|
1380
|
+
p_domain.set_defaults(func=_cmd_domain)
|
|
1381
|
+
|
|
1382
|
+
p_living = sub.add_parser("living", help="V4 Living Evidence (subscribe/refresh/status)")
|
|
1383
|
+
p_living.add_argument("action", choices=["subscribe", "refresh", "status"])
|
|
1384
|
+
p_living.add_argument("--project", required=True)
|
|
1385
|
+
p_living.add_argument("--decision", default=None, help="decision snapshot id (subscribe)")
|
|
1386
|
+
p_living.add_argument("--term", action="append", default=[], help="query term (subscribe)")
|
|
1387
|
+
p_living.add_argument("--subscription", default=None, help="subscription id (refresh/status)")
|
|
1388
|
+
p_living.add_argument("--evidence-file", default=None, type=Path, help="new evidence JSONL (refresh)")
|
|
1389
|
+
p_living.add_argument("--home", default=None)
|
|
1390
|
+
p_living.set_defaults(func=_cmd_living)
|
|
1391
|
+
|
|
1392
|
+
p_judge = sub.add_parser("benchmark-judge", help="V4 LLM judge evaluation")
|
|
1393
|
+
p_judge.add_argument("action", choices=["run", "report"])
|
|
1394
|
+
p_judge.add_argument("--run", default="benchmarks/empirical/run-empirical-01")
|
|
1395
|
+
p_judge.add_argument("--out", default="benchmarks/empirical/judge-evaluation.json")
|
|
1396
|
+
p_judge.add_argument("--report", default="benchmarks/empirical/judge-report.md")
|
|
1397
|
+
p_judge.add_argument("--limit", type=int, default=60)
|
|
1398
|
+
p_judge.set_defaults(func=_cmd_benchmark_judge)
|
|
1399
|
+
|
|
1400
|
+
p_migrate = sub.add_parser("migrate-v1", help="import a V1 pack into a V2 project")
|
|
1401
|
+
p_migrate.add_argument("--pack", required=True, type=Path)
|
|
1402
|
+
p_migrate.add_argument("--title", default=None)
|
|
1403
|
+
p_migrate.add_argument("--home", default=None)
|
|
1404
|
+
p_migrate.set_defaults(func=_cmd_migrate)
|
|
1405
|
+
|
|
1406
|
+
p_dash = sub.add_parser("dashboard", help="start local research & token dashboard")
|
|
1407
|
+
p_dash.add_argument("--host", default="127.0.0.1")
|
|
1408
|
+
p_dash.add_argument("--port", type=int, default=8765)
|
|
1409
|
+
p_dash.set_defaults(func=_cmd_dashboard)
|
|
1410
|
+
|
|
1411
|
+
p_srch = sub.add_parser("search", help="multi-channel hybrid search")
|
|
1412
|
+
p_srch.add_argument("query", help="search query")
|
|
1413
|
+
p_srch.add_argument("--limit", type=int, default=10)
|
|
1414
|
+
p_srch.add_argument("--academic", action="store_true", help="academic only")
|
|
1415
|
+
p_srch.set_defaults(func=_cmd_search)
|
|
1416
|
+
|
|
1417
|
+
p_did = sub.add_parser("did", help="run DID regression on classroom CSV")
|
|
1418
|
+
p_did.add_argument("csv", type=Path, help="CSV file path")
|
|
1419
|
+
p_did.set_defaults(func=_cmd_did)
|
|
1420
|
+
|
|
1421
|
+
p_eff = sub.add_parser("effect", help="calculate Hedges g effect size")
|
|
1422
|
+
p_eff.add_argument("--mean1", type=float, required=True)
|
|
1423
|
+
p_eff.add_argument("--sd1", type=float, required=True)
|
|
1424
|
+
p_eff.add_argument("--n1", type=int, required=True)
|
|
1425
|
+
p_eff.add_argument("--mean2", type=float, required=True)
|
|
1426
|
+
p_eff.add_argument("--sd2", type=float, required=True)
|
|
1427
|
+
p_eff.add_argument("--n2", type=int, required=True)
|
|
1428
|
+
p_eff.set_defaults(func=_cmd_effect)
|
|
1429
|
+
|
|
1430
|
+
p_lnt = sub.add_parser("lint", help="run skill static linter")
|
|
1431
|
+
p_lnt.set_defaults(func=_cmd_lint)
|
|
1432
|
+
|
|
1433
|
+
args = parser.parse_args(argv)
|
|
1434
|
+
if os.environ.get("EDUEVIDENCE_LOG_LEVEL"):
|
|
1435
|
+
# Opt-in engine/retrieval diagnostics (E4): EDUEVIDENCE_LOG_LEVEL=INFO/DEBUG
|
|
1436
|
+
enable_console_logging(getattr(logging, os.environ["EDUEVIDENCE_LOG_LEVEL"].upper(), logging.INFO))
|
|
1437
|
+
try:
|
|
1438
|
+
return args.func(args)
|
|
1439
|
+
except FileNotFoundError as exc:
|
|
1440
|
+
# V2 project lookups fail cleanly like V1 (ERROR + exit 2)
|
|
1441
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1442
|
+
return 2
|
|
1443
|
+
except (ValueError, RuntimeError) as exc:
|
|
1444
|
+
# v3/v4 command handlers validate inputs and raise ValueError on
|
|
1445
|
+
# contract violations; surface cleanly.
|
|
1446
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1447
|
+
return 2
|
|
1448
|
+
except (KeyError, AttributeError, TypeError) as exc:
|
|
1449
|
+
# v4 handlers: unknown domain ids, missing required args etc. must not
|
|
1450
|
+
# leak raw tracebacks (review P2).
|
|
1451
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
1452
|
+
return 2
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
if __name__ == "__main__":
|
|
1456
|
+
sys.exit(main())
|