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,308 @@
|
|
|
1
|
+
"""AnalysisPlan/AnalysisRun + Native Core descriptive analysis.
|
|
2
|
+
|
|
3
|
+
Native Core (stdlib only — no scipy/pandas/statsmodels) supports
|
|
4
|
+
deterministic: row/column/type profile, missingness, group counts,
|
|
5
|
+
mean/median/min/max, explicit pre/post descriptive difference, explicit
|
|
6
|
+
between-group descriptive difference. Advanced analysis (regression,
|
|
7
|
+
multilevel, meta-analysis, thematic analysis) is capability-discovered; an
|
|
8
|
+
unavailable capability returns `ANALYSIS_CAPABILITY_UNAVAILABLE` and never
|
|
9
|
+
fabricates p-values. Advanced results enter only through
|
|
10
|
+
`record_external_analysis()` with explicit provider/software provenance.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import csv
|
|
16
|
+
import json
|
|
17
|
+
import statistics
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from engine.contracts import validate_record
|
|
23
|
+
from engine.datasets import analysis_blocked_by_privacy
|
|
24
|
+
from engine.ids import new_local_id
|
|
25
|
+
from engine.project import ProjectWorkspace
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _now_iso() -> str:
|
|
29
|
+
return datetime.now(timezone.utc).isoformat()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class AnalysisCapabilityResult:
|
|
34
|
+
status: str
|
|
35
|
+
capability_id: str
|
|
36
|
+
output: dict
|
|
37
|
+
warnings: tuple[str, ...]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
NATIVE_CAPABILITIES = frozenset({
|
|
41
|
+
"row_column_type_profile", "missingness", "group_counts",
|
|
42
|
+
"descriptive_statistics", "pre_post_descriptive_difference",
|
|
43
|
+
"between_group_descriptive_difference",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
ADVANCED_CAPABILITIES = frozenset({
|
|
47
|
+
"regression", "multilevel_analysis", "meta_analysis",
|
|
48
|
+
"qualitative_thematic_analysis", "structural_equation_modeling",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_rows(path: Path) -> tuple[list[str], list[list[str]]]:
|
|
53
|
+
with path.open("r", encoding="utf-8-sig", newline="") as f:
|
|
54
|
+
reader = csv.reader(f)
|
|
55
|
+
rows = list(reader)
|
|
56
|
+
if not rows:
|
|
57
|
+
return [], []
|
|
58
|
+
return rows[0], rows[1:]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _num(value: str) -> float | None:
|
|
62
|
+
try:
|
|
63
|
+
return float(value)
|
|
64
|
+
except (TypeError, ValueError):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def validate_analysis_plan(project: ProjectWorkspace, plan: dict) -> list[str]:
|
|
69
|
+
errors = validate_record("analysis-plan", plan)
|
|
70
|
+
return errors
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_native_descriptive(project: ProjectWorkspace, plan: dict) -> dict:
|
|
74
|
+
"""Run deterministic Native Core descriptive analyses for the plan.
|
|
75
|
+
|
|
76
|
+
`plan` carries dataset_ids + primary_analysis; analysis_requirements
|
|
77
|
+
from the design select which native capabilities run. Returns an
|
|
78
|
+
AnalysisRun record (status completed or capability_unavailable).
|
|
79
|
+
"""
|
|
80
|
+
# privacy gate: block on required-but-undone deidentification
|
|
81
|
+
from engine.datasets import ingest_dataset # noqa: F401 (registry use)
|
|
82
|
+
manifests = {}
|
|
83
|
+
for dsid in plan.get("dataset_ids", []):
|
|
84
|
+
manifest = project.path / "datasets" / "raw" / dsid / "manifest.json"
|
|
85
|
+
if not manifest.is_file():
|
|
86
|
+
return {
|
|
87
|
+
"analysis_run_id": new_local_id("ANL", set()),
|
|
88
|
+
"analysis_plan_id": plan["analysis_plan_id"],
|
|
89
|
+
"dataset_ids": plan.get("dataset_ids", []),
|
|
90
|
+
"status": "failed",
|
|
91
|
+
"outputs": {"error": f"dataset {dsid} not found in project"},
|
|
92
|
+
"assumption_checks": [],
|
|
93
|
+
"created_at": _now_iso(),
|
|
94
|
+
"extensions": {},
|
|
95
|
+
}
|
|
96
|
+
asset = json.loads(manifest.read_text(encoding="utf-8"))
|
|
97
|
+
reasons = analysis_blocked_by_privacy(asset)
|
|
98
|
+
if reasons:
|
|
99
|
+
return {
|
|
100
|
+
"analysis_run_id": new_local_id("ANL", set()),
|
|
101
|
+
"analysis_plan_id": plan["analysis_plan_id"],
|
|
102
|
+
"dataset_ids": plan.get("dataset_ids", []),
|
|
103
|
+
"status": "failed",
|
|
104
|
+
"outputs": {"privacy_block": reasons},
|
|
105
|
+
"assumption_checks": [],
|
|
106
|
+
"created_at": _now_iso(),
|
|
107
|
+
"extensions": {},
|
|
108
|
+
}
|
|
109
|
+
manifests[dsid] = asset
|
|
110
|
+
|
|
111
|
+
outputs: dict = {}
|
|
112
|
+
warnings: list[str] = []
|
|
113
|
+
for dsid, asset in manifests.items():
|
|
114
|
+
path = Path(asset["path"])
|
|
115
|
+
cols, rows = _load_rows(path)
|
|
116
|
+
|
|
117
|
+
# type profile
|
|
118
|
+
types: dict[str, str] = {}
|
|
119
|
+
numeric_cols: dict[str, list[float]] = {c: [] for c in cols}
|
|
120
|
+
for row in rows:
|
|
121
|
+
for idx, c in enumerate(cols):
|
|
122
|
+
if idx < len(row):
|
|
123
|
+
v = row[idx].strip()
|
|
124
|
+
if v == "":
|
|
125
|
+
continue
|
|
126
|
+
if _num(v) is not None:
|
|
127
|
+
types.setdefault(c, "number")
|
|
128
|
+
numeric_cols[c].append(_num(v))
|
|
129
|
+
else:
|
|
130
|
+
types[c] = "string"
|
|
131
|
+
missing = {
|
|
132
|
+
c: sum(1 for row in rows if len(row) <= idx or row[idx].strip() == "")
|
|
133
|
+
for idx, c in enumerate(cols)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# group counts (all columns with <= 10 distinct values)
|
|
137
|
+
group_counts: dict[str, dict] = {}
|
|
138
|
+
for idx, c in enumerate(cols):
|
|
139
|
+
counts: dict[str, int] = {}
|
|
140
|
+
for row in rows:
|
|
141
|
+
if idx < len(row) and row[idx].strip():
|
|
142
|
+
counts[row[idx].strip()] = counts.get(row[idx].strip(), 0) + 1
|
|
143
|
+
if 0 < len(counts) <= 10:
|
|
144
|
+
group_counts[c] = counts
|
|
145
|
+
|
|
146
|
+
# descriptive statistics: ONLY for columns whose type is fully
|
|
147
|
+
# 'number'; a mixed column (some non-numeric cells) is marked
|
|
148
|
+
# explicitly instead of silently summarizing the numeric subset
|
|
149
|
+
desc: dict[str, dict] = {}
|
|
150
|
+
for c, vals in numeric_cols.items():
|
|
151
|
+
if types.get(c) == "number" and vals:
|
|
152
|
+
desc[c] = {
|
|
153
|
+
"count": len(vals),
|
|
154
|
+
"mean": round(statistics.fmean(vals), 4),
|
|
155
|
+
"median": statistics.median(vals),
|
|
156
|
+
"min": min(vals),
|
|
157
|
+
"max": max(vals),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
outputs[dsid] = {
|
|
161
|
+
"columns": cols,
|
|
162
|
+
"row_count": len(rows),
|
|
163
|
+
"types": types,
|
|
164
|
+
"missingness": missing,
|
|
165
|
+
"group_counts": group_counts,
|
|
166
|
+
"descriptive_statistics": desc,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# explicit pre/post descriptive difference (same dataset, mapped columns)
|
|
170
|
+
pre_post = (plan.get("extensions") or {}).get("pre_post_mapping")
|
|
171
|
+
if pre_post:
|
|
172
|
+
dsid = pre_post.get("dataset_id")
|
|
173
|
+
if dsid in outputs:
|
|
174
|
+
pre_col = pre_post["pre_column"]
|
|
175
|
+
post_col = pre_post["post_column"]
|
|
176
|
+
pre_vals = outputs[dsid]["descriptive_statistics"].get(pre_col, {})
|
|
177
|
+
post_vals = outputs[dsid]["descriptive_statistics"].get(post_col, {})
|
|
178
|
+
if pre_vals and post_vals:
|
|
179
|
+
outputs[dsid]["pre_post_descriptive_difference"] = {
|
|
180
|
+
"pre_mean": pre_vals["mean"],
|
|
181
|
+
"post_mean": post_vals["mean"],
|
|
182
|
+
"mean_difference": round(post_vals["mean"] - pre_vals["mean"], 4),
|
|
183
|
+
"note": "descriptive only; no p-value inferred",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# explicit between-group descriptive difference
|
|
187
|
+
between = (plan.get("extensions") or {}).get("between_group_mapping")
|
|
188
|
+
if between:
|
|
189
|
+
dsid = between.get("dataset_id")
|
|
190
|
+
if dsid in outputs:
|
|
191
|
+
group_col = between["group_column"]
|
|
192
|
+
value_col = between["value_column"]
|
|
193
|
+
path = Path(manifests[dsid]["path"])
|
|
194
|
+
cols2, rows2 = _load_rows(path)
|
|
195
|
+
if group_col in cols2 and value_col in cols2:
|
|
196
|
+
gi = cols2.index(group_col)
|
|
197
|
+
vi = cols2.index(value_col)
|
|
198
|
+
groups: dict[str, list[float]] = {}
|
|
199
|
+
for row in rows2:
|
|
200
|
+
if gi < len(row) and vi < len(row):
|
|
201
|
+
v = _num(row[vi])
|
|
202
|
+
g = row[gi].strip()
|
|
203
|
+
if v is not None and g:
|
|
204
|
+
groups.setdefault(g, []).append(v)
|
|
205
|
+
means = {g: round(statistics.fmean(vs), 4) for g, vs in groups.items() if vs}
|
|
206
|
+
outputs[dsid]["between_group_descriptive_difference"] = {
|
|
207
|
+
"group_means": means,
|
|
208
|
+
"note": "descriptive only; no p-value inferred",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if warnings:
|
|
212
|
+
outputs["warnings"] = list(warnings)
|
|
213
|
+
|
|
214
|
+
run = {
|
|
215
|
+
"analysis_run_id": new_local_id("ANL", set()),
|
|
216
|
+
"analysis_plan_id": plan["analysis_plan_id"],
|
|
217
|
+
"dataset_ids": plan.get("dataset_ids", []),
|
|
218
|
+
"status": "completed",
|
|
219
|
+
"outputs": outputs,
|
|
220
|
+
"assumption_checks": [],
|
|
221
|
+
"created_at": _now_iso(),
|
|
222
|
+
"extensions": {"engine": "native_core", "capabilities": sorted(NATIVE_CAPABILITIES)},
|
|
223
|
+
}
|
|
224
|
+
errors = validate_record("analysis-run", run)
|
|
225
|
+
if errors:
|
|
226
|
+
raise ValueError(f"invalid analysis run: {errors}")
|
|
227
|
+
return run
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def record_external_analysis(project: ProjectWorkspace, *, plan: dict,
|
|
231
|
+
provider: str, software: dict,
|
|
232
|
+
outputs: dict, assumption_checks: list[dict],
|
|
233
|
+
status: str) -> dict:
|
|
234
|
+
"""Record an analysis produced by a discovered external capability."""
|
|
235
|
+
run = {
|
|
236
|
+
"analysis_run_id": new_local_id("ANL", set()),
|
|
237
|
+
"analysis_plan_id": plan["analysis_plan_id"],
|
|
238
|
+
"dataset_ids": plan.get("dataset_ids", []),
|
|
239
|
+
"status": "completed" if status == "completed" else "failed",
|
|
240
|
+
"outputs": outputs,
|
|
241
|
+
"assumption_checks": assumption_checks,
|
|
242
|
+
"created_at": _now_iso(),
|
|
243
|
+
"extensions": {
|
|
244
|
+
"provider": provider,
|
|
245
|
+
"software": software,
|
|
246
|
+
"external": True,
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
errors = validate_record("analysis-run", run)
|
|
250
|
+
if errors:
|
|
251
|
+
raise ValueError(f"invalid external analysis run: {errors}")
|
|
252
|
+
return run
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def save_analysis_run(project: ProjectWorkspace, run: dict) -> Path:
|
|
256
|
+
"""Persist an AnalysisRun under analyses/ (atomic; never rewritten)."""
|
|
257
|
+
errors = validate_record("analysis-run", run)
|
|
258
|
+
if errors:
|
|
259
|
+
raise ValueError(f"invalid analysis run: {errors}")
|
|
260
|
+
path = project.path / "analyses" / f"{run['analysis_run_id']}.json"
|
|
261
|
+
if path.exists():
|
|
262
|
+
raise FileExistsError(f"analysis run already exists: {path}")
|
|
263
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
tmp = path.with_suffix(".tmp")
|
|
265
|
+
tmp.write_text(__import__("json").dumps(run, ensure_ascii=False, indent=2) + "\n",
|
|
266
|
+
encoding="utf-8")
|
|
267
|
+
tmp.replace(path)
|
|
268
|
+
return path
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def mark_analysis_validated(project: ProjectWorkspace, run_id: str) -> dict:
|
|
272
|
+
"""Promote a persisted AnalysisRun to `validated`.
|
|
273
|
+
|
|
274
|
+
`validated` is the ONLY status that closes the Full Research Cycle gate
|
|
275
|
+
in engine/update.py (ANALYSIS_INVALID otherwise). A human or automated
|
|
276
|
+
review gate calls this after checking the run's outputs.
|
|
277
|
+
"""
|
|
278
|
+
path = project.path / "analyses" / f"{run_id}.json"
|
|
279
|
+
if not path.is_file():
|
|
280
|
+
raise FileNotFoundError(f"analysis run {run_id} not persisted under {path}")
|
|
281
|
+
run = json.loads(path.read_text(encoding="utf-8"))
|
|
282
|
+
if run.get("status") not in ("completed", "validated"):
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"cannot validate analysis run {run_id} with status "
|
|
285
|
+
f"{run.get('status')!r}; only completed runs may be validated"
|
|
286
|
+
)
|
|
287
|
+
run["status"] = "validated"
|
|
288
|
+
errors = validate_record("analysis-run", run)
|
|
289
|
+
if errors:
|
|
290
|
+
raise ValueError(f"invalid validated run: {errors}")
|
|
291
|
+
tmp = path.with_suffix(".tmp")
|
|
292
|
+
tmp.write_text(json.dumps(run, ensure_ascii=False, indent=2) + "\n",
|
|
293
|
+
encoding="utf-8")
|
|
294
|
+
tmp.replace(path)
|
|
295
|
+
return run
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def capability_unavailable(capability_id: str, plan: dict) -> AnalysisCapabilityResult:
|
|
299
|
+
"""Honest degradation for undiscovered advanced capabilities."""
|
|
300
|
+
return AnalysisCapabilityResult(
|
|
301
|
+
status="ANALYSIS_CAPABILITY_UNAVAILABLE",
|
|
302
|
+
capability_id=capability_id,
|
|
303
|
+
output={},
|
|
304
|
+
warnings=(
|
|
305
|
+
f"capability {capability_id!r} not discovered; no p-values or "
|
|
306
|
+
f"effect estimates were fabricated",
|
|
307
|
+
),
|
|
308
|
+
)
|
package/engine/bias.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""engine/bias.py - publication-bias diagnostics for v4 meta-analysis.
|
|
2
|
+
|
|
3
|
+
egger_regression Egger (1997) funnel-asymmetry test: regress the
|
|
4
|
+
standard normal deviate SND = d/se on precision
|
|
5
|
+
prec = 1/se; the intercept estimates small-study bias
|
|
6
|
+
(Borenstein 2009, ch. 41, eq. 41.1-41.3).
|
|
7
|
+
fail_safe_n Rosenthal (1979) fail-safe N: how many null-effect
|
|
8
|
+
studies would push the combined z below significance
|
|
9
|
+
(Borenstein 2009, ch. 42): N_fs = (ΣZ)² / 2.706 − k,
|
|
10
|
+
with 2.706 = 1.645² (one-tailed z at α = 0.05).
|
|
11
|
+
|
|
12
|
+
Pure stdlib (math only). Egger's intercept is tested with a two-tailed
|
|
13
|
+
Student t (df = k − 2); the t CDF uses the regularized incomplete beta
|
|
14
|
+
function (Numerical Recipes betacf) so no scipy is required.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
_Z_CRIT_ONE_TAIL = 1.6448536269514722 # z at α = 0.05, one-tailed
|
|
22
|
+
_Z_CRIT_SQUARED = _Z_CRIT_ONE_TAIL ** 2 # 2.7055434540954042
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_number(value: Any) -> bool:
|
|
26
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _usable(rows: list[dict]) -> list[dict]:
|
|
30
|
+
import math as _m
|
|
31
|
+
return [r for r in rows or []
|
|
32
|
+
if _is_number(r.get("d")) and _is_number(r.get("se")) and r["se"] > 0
|
|
33
|
+
and _m.isfinite(float(r["d"])) and _m.isfinite(float(r["se"]))]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Student-t survival via regularized incomplete beta (stdlib only)
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def _betacf(a: float, b: float, x: float, max_iter: int = 200,
|
|
41
|
+
eps: float = 3.0e-12) -> float:
|
|
42
|
+
"""Continued-fraction evaluation of the incomplete beta function."""
|
|
43
|
+
tiny = 1.0e-30
|
|
44
|
+
qab, qap, qam = a + b, a + 1.0, a - 1.0
|
|
45
|
+
c = 1.0
|
|
46
|
+
d = 1.0 - qab * x / qap
|
|
47
|
+
if abs(d) < tiny:
|
|
48
|
+
d = tiny
|
|
49
|
+
d = 1.0 / d
|
|
50
|
+
h = d
|
|
51
|
+
for m in range(1, max_iter + 1):
|
|
52
|
+
m2 = 2 * m
|
|
53
|
+
aa = m * (b - m) * x / ((qam + m2) * (a + m2))
|
|
54
|
+
d = 1.0 + aa * d
|
|
55
|
+
if abs(d) < tiny:
|
|
56
|
+
d = tiny
|
|
57
|
+
c = 1.0 + aa / c
|
|
58
|
+
if abs(c) < tiny:
|
|
59
|
+
c = tiny
|
|
60
|
+
d = 1.0 / d
|
|
61
|
+
h *= d * c
|
|
62
|
+
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
|
|
63
|
+
d = 1.0 + aa * d
|
|
64
|
+
if abs(d) < tiny:
|
|
65
|
+
d = tiny
|
|
66
|
+
c = 1.0 + aa / c
|
|
67
|
+
if abs(c) < tiny:
|
|
68
|
+
c = tiny
|
|
69
|
+
d = 1.0 / d
|
|
70
|
+
delta = d * c
|
|
71
|
+
h *= delta
|
|
72
|
+
if abs(delta - 1.0) < eps:
|
|
73
|
+
break
|
|
74
|
+
return h
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _betai(a: float, b: float, x: float) -> float:
|
|
78
|
+
"""Regularized incomplete beta I_x(a, b)."""
|
|
79
|
+
if x <= 0.0:
|
|
80
|
+
return 0.0
|
|
81
|
+
if x >= 1.0:
|
|
82
|
+
return 1.0
|
|
83
|
+
bt = math.exp(math.lgamma(a + b) - math.lgamma(a) - math.lgamma(b)
|
|
84
|
+
+ a * math.log(x) + b * math.log1p(-x))
|
|
85
|
+
if x < (a + 1.0) / (a + b + 2.0):
|
|
86
|
+
return bt * _betacf(a, b, x) / a
|
|
87
|
+
return 1.0 - bt * _betacf(b, a, 1.0 - x) / b
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _t_two_tailed_p(t: float, df: int) -> float:
|
|
91
|
+
"""Two-tailed p for a Student t with ``df`` degrees of freedom.
|
|
92
|
+
|
|
93
|
+
Uses the identity P(T > t) = 0.5 · I_{df/(df+t²)}(df/2, 1/2).
|
|
94
|
+
"""
|
|
95
|
+
if df <= 0:
|
|
96
|
+
return 1.0
|
|
97
|
+
if not math.isfinite(t):
|
|
98
|
+
return 0.0
|
|
99
|
+
x = df / (df + t * t)
|
|
100
|
+
return 2.0 * 0.5 * _betai(df / 2.0, 0.5, x)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Egger's regression test
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def egger_regression(rows: list[dict]) -> dict:
|
|
108
|
+
"""Egger (1997) funnel-asymmetry test via OLS of SND on precision.
|
|
109
|
+
|
|
110
|
+
Symmetric data (effect independent of precision) yields intercept ≈ 0;
|
|
111
|
+
a nonzero intercept signals small-study bias. Significance: two-tailed
|
|
112
|
+
t-test on the intercept, df = k − 2. With k < 3, or constant precision
|
|
113
|
+
across studies, the test is not applicable. A perfect fit (RSS = 0) is
|
|
114
|
+
treated as no detectable bias (t = 0, p = 1).
|
|
115
|
+
"""
|
|
116
|
+
usable = _usable(rows)
|
|
117
|
+
k = len(usable)
|
|
118
|
+
if k < 3:
|
|
119
|
+
return {"applicable": False, "reason": "need >= 3 studies",
|
|
120
|
+
"k": k}
|
|
121
|
+
x = [1.0 / float(r["se"]) for r in usable] # precision
|
|
122
|
+
y = [float(r["d"]) / float(r["se"]) for r in usable] # SND
|
|
123
|
+
x_bar = sum(x) / k
|
|
124
|
+
y_bar = sum(y) / k
|
|
125
|
+
sxx = sum((xi - x_bar) ** 2 for xi in x)
|
|
126
|
+
if sxx == 0:
|
|
127
|
+
return {"applicable": False, "reason": "constant precision",
|
|
128
|
+
"k": k}
|
|
129
|
+
sxy = sum((xi - x_bar) * (yi - y_bar) for xi, yi in zip(x, y))
|
|
130
|
+
slope = sxy / sxx
|
|
131
|
+
intercept = y_bar - slope * x_bar
|
|
132
|
+
residuals = [yi - (intercept + slope * xi) for xi, yi in zip(x, y)]
|
|
133
|
+
rss = sum(rr * rr for rr in residuals)
|
|
134
|
+
mse = rss / (k - 2) if k > 2 else 0.0
|
|
135
|
+
se_intercept = math.sqrt(mse * (1.0 / k + x_bar * x_bar / sxx))
|
|
136
|
+
if se_intercept == 0 or not math.isfinite(se_intercept): # perfect fit
|
|
137
|
+
t, p = 0.0, 1.0
|
|
138
|
+
else:
|
|
139
|
+
t = intercept / se_intercept
|
|
140
|
+
p = _t_two_tailed_p(t, k - 2)
|
|
141
|
+
return {
|
|
142
|
+
"applicable": True,
|
|
143
|
+
"k": k,
|
|
144
|
+
"intercept": intercept,
|
|
145
|
+
"se_intercept": se_intercept,
|
|
146
|
+
"slope": slope,
|
|
147
|
+
"t": t,
|
|
148
|
+
"p_value": p,
|
|
149
|
+
"significant": p < 0.05,
|
|
150
|
+
"bias_detected": p < 0.05,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Rosenthal's fail-safe N
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def fail_safe_n(rows: list[dict]) -> dict:
|
|
159
|
+
"""Rosenthal (1979) fail-safe N: null studies needed to nullify the result.
|
|
160
|
+
|
|
161
|
+
N_fs = (ΣZ)² / 2.706 − k with Z = d/se (floored at 0). ``tolerates_5``
|
|
162
|
+
reports whether the finding survives the addition of 5 null studies
|
|
163
|
+
(N_fs ≥ 5) — the task's minimum robustness bar.
|
|
164
|
+
"""
|
|
165
|
+
usable = _usable(rows)
|
|
166
|
+
k = len(usable)
|
|
167
|
+
zs = [float(r["d"]) / float(r["se"]) for r in usable]
|
|
168
|
+
sum_z = sum(zs)
|
|
169
|
+
n_fail_safe = max(0.0, (sum_z * sum_z) / _Z_CRIT_SQUARED - k)
|
|
170
|
+
return {
|
|
171
|
+
"method": "rosenthal",
|
|
172
|
+
"k": k,
|
|
173
|
+
"sum_z": sum_z,
|
|
174
|
+
"z_crit": _Z_CRIT_ONE_TAIL,
|
|
175
|
+
"z_crit_squared": _Z_CRIT_SQUARED,
|
|
176
|
+
"n_fail_safe": n_fail_safe,
|
|
177
|
+
"tolerates_5": n_fail_safe >= 5.0,
|
|
178
|
+
}
|
package/engine/briefs.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Schema-bound task briefs.
|
|
2
|
+
|
|
3
|
+
A task brief is generated from the registered schema metadata — required
|
|
4
|
+
fields and enums are read from the schema, never hard-coded twice in this
|
|
5
|
+
module. `build_task_brief()` accepts only `PlanStep(kind="capability")`;
|
|
6
|
+
calling it on a wait step raises a clear ValueError instead of generating a
|
|
7
|
+
fake agent task.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from engine.contracts import load_schema, schema_path
|
|
16
|
+
from engine.planner import PlanStep
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _schema_section(schema: dict) -> str:
|
|
20
|
+
lines: list[str] = []
|
|
21
|
+
props = schema.get("properties", {})
|
|
22
|
+
required = set(schema.get("required", []))
|
|
23
|
+
for key in sorted(props):
|
|
24
|
+
prop = props[key]
|
|
25
|
+
line = f"- `{key}`"
|
|
26
|
+
if key in required:
|
|
27
|
+
line += " (REQUIRED)"
|
|
28
|
+
if "enum" in prop:
|
|
29
|
+
line += f" enum={prop['enum']}"
|
|
30
|
+
if "pattern" in prop:
|
|
31
|
+
line += f" pattern={prop['pattern']!r}"
|
|
32
|
+
if "const" in prop:
|
|
33
|
+
line += f" const={prop['const']!r}"
|
|
34
|
+
lines.append(line)
|
|
35
|
+
return "\n".join(lines) if lines else "(no properties)"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_task_brief(step: PlanStep, *, project: ProjectWorkspace,
|
|
39
|
+
input_payload: dict, output_path: Path) -> str:
|
|
40
|
+
"""Render a capability step into an executable agent brief.
|
|
41
|
+
|
|
42
|
+
`project` is the ProjectWorkspace (for context ids); `input_payload`
|
|
43
|
+
carries the input entity ids/records for this step; `output_path` is
|
|
44
|
+
where the step's output artifact must be written.
|
|
45
|
+
"""
|
|
46
|
+
if step.kind != "capability" or step.capability_id is None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"cannot build a task brief for non-capability step "
|
|
49
|
+
f"{step.step_id!r} (kind={step.kind}); wait steps are real "
|
|
50
|
+
f"states, not fake agent tasks"
|
|
51
|
+
)
|
|
52
|
+
cap = step.capability_id
|
|
53
|
+
schema_name = step.output_contract
|
|
54
|
+
if schema_name is None:
|
|
55
|
+
raise ValueError(f"capability {cap} has no output contract")
|
|
56
|
+
|
|
57
|
+
sch = load_schema(schema_name)
|
|
58
|
+
sch_file = schema_path(schema_name)
|
|
59
|
+
|
|
60
|
+
lines = [
|
|
61
|
+
f"# Task brief: {cap}",
|
|
62
|
+
"",
|
|
63
|
+
f"Project: {project.project_id}",
|
|
64
|
+
f"Output schema: {sch_file.relative_to(Path.cwd()) if sch_file.is_relative_to(Path.cwd()) else sch_file}",
|
|
65
|
+
"",
|
|
66
|
+
"## Output contract (schema-derived)",
|
|
67
|
+
_schema_section(sch),
|
|
68
|
+
"",
|
|
69
|
+
"## Required fields",
|
|
70
|
+
", ".join(sch.get("required", [])) or "(none)",
|
|
71
|
+
"",
|
|
72
|
+
"## Validation",
|
|
73
|
+
f"Validate the output with `engine.contracts.validate_record({schema_name!r}, record)` — "
|
|
74
|
+
f"it must return [] (empty errors).",
|
|
75
|
+
"",
|
|
76
|
+
f"## Output path",
|
|
77
|
+
str(output_path),
|
|
78
|
+
"",
|
|
79
|
+
"## Inputs",
|
|
80
|
+
json.dumps(input_payload, ensure_ascii=False, indent=2),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# capability-specific hard rules
|
|
84
|
+
rules: dict[str, str] = {
|
|
85
|
+
"finding_extraction": (
|
|
86
|
+
"HARD RULE: `relation_to_claim` does NOT belong in a Finding. "
|
|
87
|
+
"Findings record only what the study observed (`effect_direction`); "
|
|
88
|
+
"the relation to a Claim lives on the EvidenceLink."),
|
|
89
|
+
"source_validation": (
|
|
90
|
+
"HARD RULE: a search snippet is never evidence content (RULE 2). "
|
|
91
|
+
"Only fetched + validated source content may be extracted."),
|
|
92
|
+
"counter_evidence_search": (
|
|
93
|
+
"HARD RULE: seek null/negative/contradictory evidence, AI "
|
|
94
|
+
"dependency, reduced transfer, novelty effects, self-selection "
|
|
95
|
+
"bias, alternative explanations. Never fabricate counter-evidence."),
|
|
96
|
+
"evidence_synthesis": (
|
|
97
|
+
"HARD RULE: independent-study counting only. 5 Findings from one "
|
|
98
|
+
"Study = 1 independent study; never count Findings as Studies."),
|
|
99
|
+
"tribunal": (
|
|
100
|
+
"HARD RULE: pass the Pre-Verdict Gate before adjudicating; "
|
|
101
|
+
"critical failures forbid high-confidence verdicts."),
|
|
102
|
+
}
|
|
103
|
+
if cap in rules:
|
|
104
|
+
lines += ["", "## Hard rules", rules[cap]]
|
|
105
|
+
|
|
106
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Capability registry — the scientific capabilities a research plan can use.
|
|
2
|
+
|
|
3
|
+
The registry is independent of Agent names and model names: planning routes
|
|
4
|
+
by capability, execution routing (Agent MCP vs native) happens later and
|
|
5
|
+
orthogonally. A capability may be deterministic-local (Native Core) or
|
|
6
|
+
capability-discovered (SCP), and may carry a scientific gate.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class CapabilitySpec:
|
|
16
|
+
capability_id: str
|
|
17
|
+
input_contracts: tuple[str, ...]
|
|
18
|
+
output_contracts: tuple[str, ...]
|
|
19
|
+
deterministic_local: bool
|
|
20
|
+
scientific_gate: str | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_REGISTRY: dict[str, CapabilitySpec] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _register(capability_id: str, *, input_contracts: tuple[str, ...],
|
|
27
|
+
output_contracts: tuple[str, ...], deterministic_local: bool,
|
|
28
|
+
scientific_gate: str | None = None) -> None:
|
|
29
|
+
_REGISTRY[capability_id] = CapabilitySpec(
|
|
30
|
+
capability_id=capability_id,
|
|
31
|
+
input_contracts=input_contracts,
|
|
32
|
+
output_contracts=output_contracts,
|
|
33
|
+
deterministic_local=deterministic_local,
|
|
34
|
+
scientific_gate=scientific_gate,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Evidence Review pipeline
|
|
39
|
+
_register("research_framing", input_contracts=("research-intent",),
|
|
40
|
+
output_contracts=("education-frame",), deterministic_local=False)
|
|
41
|
+
_register("literature_search", input_contracts=("education-frame",),
|
|
42
|
+
output_contracts=("source",), deterministic_local=False)
|
|
43
|
+
_register("counter_evidence_search", input_contracts=("education-frame",),
|
|
44
|
+
output_contracts=("source",), deterministic_local=False,
|
|
45
|
+
scientific_gate="RULE 4: independent counter-evidence search")
|
|
46
|
+
_register("source_fetch", input_contracts=("source",),
|
|
47
|
+
output_contracts=("fetch-result",), deterministic_local=False)
|
|
48
|
+
_register("source_validation", input_contracts=("source", "fetch-result"),
|
|
49
|
+
output_contracts=("source",), deterministic_local=True,
|
|
50
|
+
scientific_gate="snippet != evidence (RULE 2)")
|
|
51
|
+
_register("study_extraction", input_contracts=("source",),
|
|
52
|
+
output_contracts=("study",), deterministic_local=False)
|
|
53
|
+
_register("finding_extraction", input_contracts=("study",),
|
|
54
|
+
output_contracts=("finding",), deterministic_local=False)
|
|
55
|
+
_register("methodology_appraisal", input_contracts=("study",),
|
|
56
|
+
output_contracts=("methodology-audit",), deterministic_local=False)
|
|
57
|
+
_register("claim_linking", input_contracts=("finding", "claim"),
|
|
58
|
+
output_contracts=("evidence-link",), deterministic_local=False,
|
|
59
|
+
scientific_gate="relation_to_claim lives on the link, never the finding")
|
|
60
|
+
_register("evidence_synthesis", input_contracts=("evidence-link", "methodology-audit"),
|
|
61
|
+
output_contracts=("synthesis",), deterministic_local=True,
|
|
62
|
+
scientific_gate="independent Study counting, never Finding counting")
|
|
63
|
+
_register("tribunal", input_contracts=("synthesis",),
|
|
64
|
+
output_contracts=("decision-snapshot",), deterministic_local=False,
|
|
65
|
+
scientific_gate="Pre-Verdict Gate before Tribunal")
|
|
66
|
+
_register("applicability_analysis", input_contracts=("evidence-link",),
|
|
67
|
+
output_contracts=("applicability",), deterministic_local=False)
|
|
68
|
+
_register("knowledge_gap_detection", input_contracts=("education-frame", "claim"),
|
|
69
|
+
output_contracts=("knowledge-gap",), deterministic_local=True)
|
|
70
|
+
_register("report_projection", input_contracts=("graph-revision",),
|
|
71
|
+
output_contracts=("projection",), deterministic_local=True)
|
|
72
|
+
_register("report_rendering", input_contracts=("projection",),
|
|
73
|
+
output_contracts=("report",), deterministic_local=True)
|
|
74
|
+
|
|
75
|
+
# Full Research Cycle additions
|
|
76
|
+
_register("study_design", input_contracts=("knowledge-gap",),
|
|
77
|
+
output_contracts=("study-design",), deterministic_local=False,
|
|
78
|
+
scientific_gate="No new study design without evidence grounding")
|
|
79
|
+
_register("measurement_design", input_contracts=("study-design",),
|
|
80
|
+
output_contracts=("analysis-plan",), deterministic_local=False)
|
|
81
|
+
_register("data_validation", input_contracts=("dataset-asset",),
|
|
82
|
+
output_contracts=("dataset-manifest",), deterministic_local=True,
|
|
83
|
+
scientific_gate="provenance/hash/missingness before analysis")
|
|
84
|
+
_register("data_analysis", input_contracts=("dataset-manifest", "analysis-plan"),
|
|
85
|
+
output_contracts=("analysis-run",), deterministic_local=True,
|
|
86
|
+
scientific_gate="never fabricate p-values; ANALYSIS_CAPABILITY_UNAVAILABLE")
|
|
87
|
+
_register("intervention_design", input_contracts=("decision-snapshot",),
|
|
88
|
+
output_contracts=("intervention",), deterministic_local=False)
|
|
89
|
+
_register("evaluation_design", input_contracts=("decision-snapshot",),
|
|
90
|
+
output_contracts=("evaluation",), deterministic_local=False)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def capability_registry() -> dict[str, CapabilitySpec]:
|
|
94
|
+
"""Return the frozen capability registry."""
|
|
95
|
+
return dict(_REGISTRY)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def capability(capability_id: str) -> CapabilitySpec | None:
|
|
99
|
+
return _REGISTRY.get(capability_id)
|