eduevidence 5.2.0 → 6.0.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/README.md +54 -42
- package/README.zh-CN.md +51 -28
- package/SKILL.md +390 -133
- package/agents/openai.yaml +4 -0
- package/assets/readme/controlled-execution.svg +34 -0
- package/assets/readme/logo.png +0 -0
- package/assets/readme/research-workflow.svg +56 -0
- package/assets/readme/studio-graph.png +0 -0
- package/assets/readme/studio-overview.png +0 -0
- package/assets/readme/studio-reports.png +0 -0
- package/autoevolve/config.yaml +17 -0
- package/autoevolve/program.md +25 -0
- package/autoevolve/protected.manifest.yaml +34 -0
- package/benchmarks/adversarial/cases.jsonl +7 -0
- package/benchmarks/evidence-library.json +5268 -0
- package/benchmarks/partitions.json +8 -0
- package/docs/architecture.md +220 -0
- package/docs/autoresearch-evolution-plan.md +2903 -0
- package/docs/autoresearch-implementation-status.md +101 -0
- package/docs/demo-storyboard.md +20 -0
- package/docs/demo-workplace-ai.md +92 -0
- package/docs/demo.md +32 -0
- package/docs/install-guide.md +150 -0
- package/docs/orchestration-role-model.md +1254 -0
- package/docs/release-closeout/README.md +17 -0
- package/docs/release-closeout/frontend-acceptance.md +23 -0
- package/docs/release-closeout/issues.md +19 -0
- package/docs/release-closeout/verification.md +28 -0
- package/docs/release-contract.md +108 -0
- package/docs/research-studio-guide.zh-CN.md +166 -0
- package/eduevidence_cli.py +17 -11
- package/engine/_resources.py +13 -0
- package/engine/autoevolve/__init__.py +3 -0
- package/engine/autoevolve/agent_view.py +167 -0
- package/engine/autoevolve/core.py +357 -0
- package/engine/autoevolve/events.py +11 -0
- package/engine/autoevolve/git_workspace.py +77 -0
- package/engine/autoevolve/projection.py +23 -0
- package/engine/autoevolve/runner.py +413 -0
- package/engine/autoevolve/trust.py +146 -0
- package/engine/autoresearch/__init__.py +6 -0
- package/engine/autoresearch/commit.py +132 -0
- package/engine/autoresearch/contracts.py +126 -0
- package/engine/autoresearch/controller.py +207 -0
- package/engine/autoresearch/events.py +12 -0
- package/engine/autoresearch/gap_priority.py +168 -0
- package/engine/autoresearch/projection.py +30 -0
- package/engine/autoresearch/research_memory.py +59 -0
- package/engine/autoresearch/saturation.py +91 -0
- package/engine/briefs.py +2 -1
- package/engine/capabilities.py +1 -0
- package/engine/contracts.py +3 -1
- package/engine/evidencecore.py +7 -5
- package/engine/gaps.py +90 -51
- package/engine/judge_pack.py +65 -0
- package/engine/library_builtin.py +3 -1
- package/engine/living.py +2 -1
- package/engine/meta_synthesis.py +3 -1
- package/engine/orchestration.py +460 -0
- package/engine/pilot.py +2 -1
- package/engine/project.py +2 -2
- package/engine/research_service.py +113 -0
- package/engine/studio_read_model.py +400 -0
- package/engine/tribunal.py +1 -2
- package/engine/update.py +1 -0
- package/engine/versions.py +1 -1
- package/engine/worker_result.py +109 -0
- package/engine/workflows.py +70 -0
- package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +1720 -0
- package/examples/ai-coding-assistant-evidence/artifact_manifest.json +15 -0
- package/examples/ai-coding-assistant-evidence/citation_check.json +79 -0
- package/examples/ai-coding-assistant-evidence/claims.jsonl +12 -0
- package/examples/ai-coding-assistant-evidence/evaluation.json +35 -0
- package/examples/ai-coding-assistant-evidence/evidence.jsonl +12 -0
- package/examples/ai-coding-assistant-evidence/frame.json +48 -0
- package/examples/ai-coding-assistant-evidence/intervention.json +51 -0
- package/examples/ai-coding-assistant-evidence/methodology.json +36 -0
- package/examples/ai-coding-assistant-evidence/raw_verdict.json +86 -0
- package/examples/ai-coding-assistant-evidence/report_spec.json +219 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +2614 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +2614 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +2614 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +2614 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +2614 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +1720 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +1720 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +1720 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +1720 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +1720 -0
- package/examples/ai-coding-assistant-evidence/result.json +1453 -0
- package/examples/ai-coding-assistant-evidence/result.zh.json +1453 -0
- package/examples/ai-coding-assistant-evidence/sources.jsonl +8 -0
- package/examples/ai-coding-assistant-evidence/verdict.json +103 -0
- package/examples/workplace-ai-assistant/claims.jsonl +4 -0
- package/examples/workplace-ai-assistant/evaluation.json +19 -0
- package/examples/workplace-ai-assistant/evidence.jsonl +4 -0
- package/examples/workplace-ai-assistant/evidence_graph.json +444 -0
- package/examples/workplace-ai-assistant/frame.json +41 -0
- package/examples/workplace-ai-assistant/intervention.json +27 -0
- package/examples/workplace-ai-assistant/legacy-link-check.json +16 -0
- package/examples/workplace-ai-assistant/methodology.json +60 -0
- package/examples/workplace-ai-assistant/report_spec.json +55 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +2484 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +2484 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +2484 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +2484 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +2484 -0
- package/examples/workplace-ai-assistant/result.json +553 -0
- package/examples/workplace-ai-assistant/result.zh.json +553 -0
- package/examples/workplace-ai-assistant/search_log.json +19 -0
- package/examples/workplace-ai-assistant/sources.jsonl +3 -0
- package/examples/workplace-ai-assistant/validation_result.json +9 -0
- package/examples/workplace-ai-assistant/verdict.json +52 -0
- package/install.sh +7 -7
- package/integrations/orchestration_dispatch.py +146 -0
- package/package.json +37 -3
- package/pyproject.toml +11 -20
- package/references/autoresearch.md +30 -0
- package/references/evaluation-policy.md +24 -0
- package/references/orchestration.md +22 -0
- package/references/scientific-invariants.md +19 -0
- package/retrieval/audit.py +154 -0
- package/schemas/intervention.schema.json +106 -21
- package/schemas/report-result.schema.json +9 -1
- package/schemas/v2/project.schema.json +2 -2
- package/schemas/v2/run.schema.json +1 -1
- package/schemas/vNext/autoevolve-session.schema.json +1 -0
- package/schemas/vNext/eval-snapshot.schema.json +1 -0
- package/schemas/vNext/execution-plan.schema.json +1 -0
- package/schemas/vNext/gap-priority.schema.json +1 -0
- package/schemas/vNext/negative-search-record.schema.json +1 -0
- package/schemas/vNext/research-iteration.schema.json +1 -0
- package/schemas/vNext/research-strategy.schema.json +1 -0
- package/schemas/vNext/skill-experiment.schema.json +1 -0
- package/schemas/vNext/task-spec.schema.json +1 -0
- package/schemas/vNext/worker-result.schema.json +1 -0
- package/scripts/benchmark_judge.py +2 -2
- package/scripts/benchmark_v3.py +26 -43
- package/scripts/build_esl_artifacts.py +2 -2
- package/scripts/build_evidence_library.py +2 -2
- package/scripts/build_gh_pages.py +98 -0
- package/scripts/build_readme_diagrams.py +72 -0
- package/scripts/build_report_variants.py +85 -0
- package/scripts/check_autoresearch_invariants.py +95 -0
- package/scripts/daily_evolve.py +30 -0
- package/scripts/dashboard_server.py +130 -101
- package/scripts/did_regression.py +5 -30
- package/scripts/enrich_projects_human_and_lieflat.py +1 -1
- package/scripts/generate_metrics.py +4 -3
- package/scripts/generate_new_projects.py +1 -1
- package/scripts/orchestrator.py +172 -18
- package/scripts/rebake_all_5themes.py +1 -2
- package/scripts/research_auto_cli.py +475 -0
- package/scripts/run_workspace.py +17 -7
- package/scripts/search_provenance.py +64 -0
- package/scripts/serve_web.py +9 -10
- package/scripts/skill_lint.py +1 -1
- package/scripts/skill_payload.py +78 -0
- package/scripts/validate_schema.py +15 -1
- package/scripts/vnext_cli.py +133 -0
- package/setup.py +12 -0
- package/skill/roles/registry.yaml +45 -0
- package/skill/sub-skills/report-generation/SKILL.md +12 -6
- package/skill/task-briefs/applicability.md +3 -0
- package/skill/task-briefs/projection.md +3 -0
- package/skill/workflows/decision-and-pilot.md +10 -0
- package/skill/workflows/evaluate-and-update.md +10 -0
- package/skill/workflows/evidence-review.md +13 -0
- package/visualization/eduevidence-report/assets/base.css +2 -2
- package/visualization/eduevidence-report/assets/reader.css +752 -0
- package/visualization/eduevidence-report/assets/reader.js +132 -0
- package/visualization/eduevidence-report/references/chart-selection-catalog.md +109 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +3 -1
- package/visualization/eduevidence-report/scripts/build_report.py +58 -65
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +24 -100
- package/visualization/eduevidence-report/themes/academic.css +1 -1
- package/visualization/eduevidence-report/themes/claude.css +1 -1
- package/visualization/eduevidence-report/themes/datalab-dark.css +2 -2
- package/visualization/eduevidence-report/themes/datalab.css +2 -2
- package/visualization/eduevidence-report/themes/presentation.css +2 -2
- package/web/README.md +18 -0
- package/web/index.html +53 -0
- package/web/studio/THIRD_PARTY_LICENSES.txt +146 -0
- package/web/studio/assets/index-CzXocaGv.css +1 -0
- package/web/studio/assets/index-pa7jD7n4.js +230 -0
- package/web/studio/config.json +1 -0
- package/web/studio/index.html +14 -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/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/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/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/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
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Read-only, portable projections for Research Studio.
|
|
2
|
+
|
|
3
|
+
No ProjectWorkspace.create, GraphStore.create, service initialization or state
|
|
4
|
+
repair is allowed here. Missing/corrupt inputs are visible diagnostics, not
|
|
5
|
+
successful empty studies. Static export includes examples only.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import re
|
|
12
|
+
import sqlite3
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
THEMES = (
|
|
19
|
+
('claude', 'Claude Research', 'light'),
|
|
20
|
+
('academic', 'Academic Paper', 'light'),
|
|
21
|
+
('datalab', 'DataLab', 'light'),
|
|
22
|
+
('datalab-dark', 'DataLab Dark', 'dark'),
|
|
23
|
+
('presentation', 'Presentation / Judge', 'dark'),
|
|
24
|
+
)
|
|
25
|
+
TABLES = ('sources', 'studies', 'findings', 'outcomes', 'claims', 'evidence_links', 'audits')
|
|
26
|
+
MAX_BYTES = 16 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def finite(value: Any) -> float | None:
|
|
30
|
+
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def first_value(*values: Any) -> Any:
|
|
34
|
+
return next((value for value in values if value is not None), None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def json_safe(value: Any) -> Any:
|
|
38
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
39
|
+
return None
|
|
40
|
+
if isinstance(value, dict):
|
|
41
|
+
return {str(k): json_safe(v) for k, v in value.items()}
|
|
42
|
+
if isinstance(value, (list, tuple)):
|
|
43
|
+
return [json_safe(v) for v in value]
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def numeric_effect(row: dict) -> dict:
|
|
48
|
+
effect = first_value(row.get('effect_estimate'), row.get('effect_size'))
|
|
49
|
+
if isinstance(effect, dict):
|
|
50
|
+
value = finite(effect.get('value'))
|
|
51
|
+
lo = finite(first_value(effect.get('ci_lower'), effect.get('ci_lo'), effect.get('ci_low')))
|
|
52
|
+
hi = finite(first_value(effect.get('ci_upper'), effect.get('ci_hi'), effect.get('ci_high')))
|
|
53
|
+
metric = effect.get('metric') or effect.get('type') or row.get('effect_metric')
|
|
54
|
+
else:
|
|
55
|
+
value = finite(first_value(effect, row.get('hedges_g'), row.get('g'), row.get('effect_size_value')))
|
|
56
|
+
lo, hi = finite(row.get('ci_lower')), finite(row.get('ci_upper'))
|
|
57
|
+
metric = row.get('effect_metric') or row.get('metric')
|
|
58
|
+
# Never fabricate an interval or erase a legitimate zero endpoint.
|
|
59
|
+
interval = 'not_reported'
|
|
60
|
+
if lo is not None and hi is not None:
|
|
61
|
+
interval = 'reported' if lo <= hi and (value is None or lo <= value <= hi) else 'invalid'
|
|
62
|
+
elif lo is not None or hi is not None:
|
|
63
|
+
interval = 'incomplete'
|
|
64
|
+
return {'value': value, 'ci_lower': lo, 'ci_upper': hi, 'interval_status': interval,
|
|
65
|
+
'metric': str(metric or 'unspecified')}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class StudioReader:
|
|
69
|
+
"""Build a bounded public DTO from committed, project-scoped inputs."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, examples: Path, home: Path, *, static: bool = False):
|
|
72
|
+
self.examples = Path(examples).resolve()
|
|
73
|
+
self.home = Path(home).expanduser().resolve()
|
|
74
|
+
self.static = static
|
|
75
|
+
self.issues: list[dict] = []
|
|
76
|
+
|
|
77
|
+
def _read(self, path: Path, default: Any = None) -> Any:
|
|
78
|
+
if not path.exists():
|
|
79
|
+
return default
|
|
80
|
+
try:
|
|
81
|
+
if not path.resolve().is_relative_to(self.examples) and not path.resolve().is_relative_to(self.home):
|
|
82
|
+
raise ValueError('path escapes the read scope')
|
|
83
|
+
if path.stat().st_size > MAX_BYTES:
|
|
84
|
+
raise ValueError('file exceeds the Studio read limit')
|
|
85
|
+
if path.suffix == '.jsonl':
|
|
86
|
+
return [json.loads(line) for line in path.read_text(encoding='utf-8').splitlines() if line.strip()]
|
|
87
|
+
return json.loads(path.read_text(encoding='utf-8'))
|
|
88
|
+
except (OSError, ValueError) as exc:
|
|
89
|
+
self.issues.append({'code': 'unreadable_artifact', 'artifact': path.name, 'message': str(exc).split(':')[0]})
|
|
90
|
+
return default
|
|
91
|
+
|
|
92
|
+
def _directory(self, key: str) -> tuple[Path, str]:
|
|
93
|
+
if key.startswith('example--'):
|
|
94
|
+
name = key[len('example--'):]
|
|
95
|
+
base, kind = self.examples, 'example'
|
|
96
|
+
elif key.startswith('project--') and not self.static:
|
|
97
|
+
name = key[len('project--'):]
|
|
98
|
+
base, kind = self.home / 'projects', 'project'
|
|
99
|
+
if not re.fullmatch(r'PRJ-[A-Za-z0-9_-]+', name):
|
|
100
|
+
raise FileNotFoundError('unknown project')
|
|
101
|
+
else:
|
|
102
|
+
raise FileNotFoundError('unknown project')
|
|
103
|
+
if not re.fullmatch(r'[A-Za-z0-9_-]+', name):
|
|
104
|
+
raise FileNotFoundError('unknown project')
|
|
105
|
+
path = base / name
|
|
106
|
+
if path.is_symlink() or not path.is_dir() or not path.resolve().is_relative_to(base.resolve()):
|
|
107
|
+
raise FileNotFoundError('unknown project')
|
|
108
|
+
return path, kind
|
|
109
|
+
|
|
110
|
+
def _catalog_keys(self) -> list[str]:
|
|
111
|
+
keys = []
|
|
112
|
+
if self.examples.is_dir():
|
|
113
|
+
keys.extend('example--' + p.name for p in sorted(self.examples.iterdir())
|
|
114
|
+
if p.is_dir() and not p.is_symlink() and (p / 'result.json').is_file())
|
|
115
|
+
if not self.static and (self.home / 'projects').is_dir():
|
|
116
|
+
keys.extend('project--' + p.name for p in sorted((self.home / 'projects').iterdir())
|
|
117
|
+
if p.is_dir() and not p.is_symlink() and (p / 'project.json').is_file())
|
|
118
|
+
return keys
|
|
119
|
+
|
|
120
|
+
def _db_rows(self, table: str, project_id: str) -> list[dict]:
|
|
121
|
+
if table not in {'artifacts', 'events'}:
|
|
122
|
+
raise ValueError('unknown read table')
|
|
123
|
+
path = self.home / 'research-control.sqlite3'
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
return []
|
|
126
|
+
if path.is_symlink():
|
|
127
|
+
self.issues.append({'code': 'database_unavailable', 'message': 'symlink database refused'})
|
|
128
|
+
return []
|
|
129
|
+
try:
|
|
130
|
+
uri = path.as_uri() + '?mode=ro'
|
|
131
|
+
with sqlite3.connect(uri, uri=True, timeout=2) as connection:
|
|
132
|
+
connection.row_factory = sqlite3.Row
|
|
133
|
+
order = 'seq' if table == 'events' else 'created_at'
|
|
134
|
+
rows = connection.execute(f'SELECT * FROM {table} WHERE project_id=? ORDER BY {order} DESC LIMIT 500', (project_id,)).fetchall()
|
|
135
|
+
output = []
|
|
136
|
+
for record in rows:
|
|
137
|
+
item = dict(record)
|
|
138
|
+
item.pop('path', None) # Never expose host filesystem paths.
|
|
139
|
+
for field in ('payload', 'metadata'):
|
|
140
|
+
if isinstance(item.get(field), str):
|
|
141
|
+
item[field] = json.loads(item[field])
|
|
142
|
+
if isinstance(item.get('payload'), dict):
|
|
143
|
+
item['payload'].pop('path', None)
|
|
144
|
+
output.append(item)
|
|
145
|
+
return output
|
|
146
|
+
except (sqlite3.Error, ValueError):
|
|
147
|
+
self.issues.append({'code': 'database_unavailable', 'message': 'Research event index is unavailable'})
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
def _reports(self, directory: Path, key: str, kind: str) -> list[dict]:
|
|
151
|
+
rows = []
|
|
152
|
+
default = directory / 'EduEvidence_Report.html'
|
|
153
|
+
default_theme = None
|
|
154
|
+
if default.is_file() and not default.is_symlink():
|
|
155
|
+
with default.open(encoding='utf-8', errors='replace') as handle:
|
|
156
|
+
match = re.search(r'<html[^>]*data-theme=[\"\']([a-z-]+)', handle.read(4096))
|
|
157
|
+
default_theme = match.group(1) if match else None
|
|
158
|
+
for theme, title, tone in THEMES:
|
|
159
|
+
path = directory / 'reports-5themes' / f'EduEvidence_Report_{theme}.html'
|
|
160
|
+
if not path.is_file() and theme == default_theme:
|
|
161
|
+
path = directory / 'EduEvidence_Report.html'
|
|
162
|
+
available = path.is_file() and path.resolve().is_relative_to(directory.resolve())
|
|
163
|
+
url = None
|
|
164
|
+
if available:
|
|
165
|
+
url = (f'../reports/{directory.name}/{path.name}' if self.static else
|
|
166
|
+
f'/api/studio/projects/{quote(key, safe="")}/report?theme={theme}')
|
|
167
|
+
rows.append({'theme': theme, 'title': title, 'tone': tone, 'available': available, 'url': url})
|
|
168
|
+
if kind == 'project':
|
|
169
|
+
rows = []
|
|
170
|
+
report_dir = directory / 'reports'
|
|
171
|
+
if not report_dir.resolve().is_relative_to(directory.resolve()):
|
|
172
|
+
return rows
|
|
173
|
+
for path in sorted(report_dir.glob('*.html')):
|
|
174
|
+
if path.is_symlink():
|
|
175
|
+
continue
|
|
176
|
+
rows.append({'theme': path.stem, 'title': path.stem, 'tone': 'light', 'available': True,
|
|
177
|
+
'url': f'/api/studio/projects/{quote(key, safe="")}/report?file={quote(path.name)}'})
|
|
178
|
+
return rows
|
|
179
|
+
|
|
180
|
+
def report_path(self, key: str, *, theme: str = 'claude', filename: str | None = None) -> Path:
|
|
181
|
+
directory, kind = self._directory(key)
|
|
182
|
+
if kind == 'example':
|
|
183
|
+
if theme not in {row[0] for row in THEMES}:
|
|
184
|
+
raise FileNotFoundError('unknown report theme')
|
|
185
|
+
path = directory / 'reports-5themes' / f'EduEvidence_Report_{theme}.html'
|
|
186
|
+
if not path.is_file():
|
|
187
|
+
available = {row['theme'] for row in self._reports(directory, key, kind) if row['available']}
|
|
188
|
+
if theme in available:
|
|
189
|
+
path = directory / 'EduEvidence_Report.html'
|
|
190
|
+
else:
|
|
191
|
+
if not filename or not re.fullmatch(r'[A-Za-z0-9_.-]+\.html', filename):
|
|
192
|
+
raise FileNotFoundError('unknown report')
|
|
193
|
+
path = directory / 'reports' / filename
|
|
194
|
+
if not path.is_file() or path.is_symlink() or not path.resolve().is_relative_to(directory):
|
|
195
|
+
raise FileNotFoundError('report not generated')
|
|
196
|
+
return path
|
|
197
|
+
|
|
198
|
+
def detail(self, key: str) -> dict:
|
|
199
|
+
self.issues = []
|
|
200
|
+
directory, kind = self._directory(key)
|
|
201
|
+
en = self._read(directory / 'result.json', {}) if kind == 'example' else {}
|
|
202
|
+
zh = self._read(directory / 'result.zh.json', {}) if kind == 'example' else {}
|
|
203
|
+
if not isinstance(en, dict) or not isinstance(zh, dict):
|
|
204
|
+
raise ValueError('invalid result object')
|
|
205
|
+
result = en or {}
|
|
206
|
+
manifest = self._read(directory / 'project.json', {}) if kind == 'project' else {}
|
|
207
|
+
meta = result.get('meta') or {}
|
|
208
|
+
sources = result.get('sources') or []
|
|
209
|
+
findings = result.get('evidence') or []
|
|
210
|
+
claims = result.get('claims') or []
|
|
211
|
+
outcomes = result.get('outcomes') or []
|
|
212
|
+
audits, designs, analyses = [], [], []
|
|
213
|
+
studies, links, revisions, decisions, runs, artifacts, events, gaps, iterations = [], [], [], [], [], [], [], [], []
|
|
214
|
+
active_revision = None
|
|
215
|
+
if kind == 'project':
|
|
216
|
+
head = directory / 'graph' / 'HEAD'
|
|
217
|
+
if head.is_file():
|
|
218
|
+
try:
|
|
219
|
+
active_revision = int(head.read_text().strip())
|
|
220
|
+
if active_revision < 0:
|
|
221
|
+
raise ValueError('negative revision')
|
|
222
|
+
except (OSError, ValueError):
|
|
223
|
+
self.issues.append({'code': 'invalid_graph_head', 'message': 'Graph HEAD cannot be read'})
|
|
224
|
+
if active_revision is not None and active_revision != manifest.get('graph_revision'):
|
|
225
|
+
self.issues.append({'code': 'revision_mirror_mismatch', 'message': 'Project revision mirror differs from Graph HEAD'})
|
|
226
|
+
if active_revision:
|
|
227
|
+
revdir = directory / 'graph' / 'revisions' / f'rev-{active_revision:06d}'
|
|
228
|
+
tables = {t: self._read(revdir / f'{t}.jsonl', []) for t in TABLES}
|
|
229
|
+
for t in TABLES:
|
|
230
|
+
if not (revdir / f'{t}.jsonl').exists():
|
|
231
|
+
self.issues.append({'code': 'missing_graph_table', 'artifact': t, 'message': 'Committed graph table missing'})
|
|
232
|
+
sources, studies, findings, claims, links, outcomes, audits = (tables[t] for t in ('sources', 'studies', 'findings', 'claims', 'evidence_links', 'outcomes', 'audits'))
|
|
233
|
+
# Follow only the committed ancestry. Orphan directories are not history.
|
|
234
|
+
revision = active_revision
|
|
235
|
+
visited: set[int] = set()
|
|
236
|
+
while revision and revision not in visited:
|
|
237
|
+
visited.add(revision)
|
|
238
|
+
revision_meta = self._read(directory / 'graph' / 'revisions' / f'rev-{revision:06d}' / 'manifest.json', {})
|
|
239
|
+
if not isinstance(revision_meta, dict) or revision_meta.get('revision') != revision:
|
|
240
|
+
self.issues.append({'code': 'invalid_revision_manifest', 'message': 'Revision ancestry is incomplete'})
|
|
241
|
+
break
|
|
242
|
+
revisions.append({**revision_meta, 'active': revision == active_revision})
|
|
243
|
+
parent = revision_meta.get('parent_revision')
|
|
244
|
+
if type(parent) is not int or not 0 <= parent < revision:
|
|
245
|
+
self.issues.append({'code': 'invalid_revision_parent', 'message': 'Revision parent is invalid'})
|
|
246
|
+
break
|
|
247
|
+
revision = parent
|
|
248
|
+
revisions.reverse()
|
|
249
|
+
decisions = [d for p in sorted((directory / 'decisions').glob('DEC-*.json')) if isinstance((d := self._read(p)), dict)]
|
|
250
|
+
decisions.sort(key=lambda d: (d.get('graph_revision', 0), d.get('created_at', '')), reverse=True)
|
|
251
|
+
runs = [d for p in sorted((directory / 'runs').glob('*/run.json')) if isinstance((d := self._read(p)), dict)]
|
|
252
|
+
runs.sort(key=lambda d: d.get('started_at', ''), reverse=True)
|
|
253
|
+
for run in runs:
|
|
254
|
+
run_id = str(run.get('run_id', ''))
|
|
255
|
+
if re.fullmatch(r'[A-Za-z0-9_-]+', run_id):
|
|
256
|
+
run['stage_state'] = self._read(directory / 'runs' / run_id / 'state.json', {})
|
|
257
|
+
run['execution_plan'] = self._read(directory / 'runs' / run_id / 'execution_plan.json', {})
|
|
258
|
+
run['gate_report'] = self._read(directory / 'runs' / run_id / 'gate_report.json', {})
|
|
259
|
+
artifacts = self._db_rows('artifacts', manifest.get('project_id', directory.name))
|
|
260
|
+
events = self._db_rows('events', manifest.get('project_id', directory.name))
|
|
261
|
+
if active_revision is not None:
|
|
262
|
+
gaps = self._read(directory / 'gaps' / f'gaps-rev-{active_revision:06d}.jsonl', [])
|
|
263
|
+
iterations = self._read(directory / 'autoresearch' / 'research-iterations.jsonl', [])
|
|
264
|
+
designs = [d for p in sorted((directory / 'study-designs').glob('DSN-*.json')) if isinstance((d := self._read(p)), dict)]
|
|
265
|
+
analyses = [d for p in sorted((directory / 'analyses').glob('*.json')) if isinstance((d := self._read(p)), dict)]
|
|
266
|
+
for records in (sources, studies, findings, claims, links, outcomes, runs, events, artifacts, gaps, iterations):
|
|
267
|
+
if not isinstance(records, list) or any(not isinstance(r, dict) for r in records):
|
|
268
|
+
raise ValueError('invalid record collection')
|
|
269
|
+
sources = [{**r, 'title': r.get('title') or (r.get('extensions') or {}).get('title') or r.get('source_id'),
|
|
270
|
+
'canonical_url': r.get('canonical_url') or r.get('canonical_locator') or r.get('source_location')}
|
|
271
|
+
for r in sources]
|
|
272
|
+
localized_findings = {r.get('evidence_id'): r for r in zh.get('evidence', [])}
|
|
273
|
+
evidence = []
|
|
274
|
+
seen = set()
|
|
275
|
+
studies_by_id = {r.get('study_id'): r for r in studies}
|
|
276
|
+
outcomes_by_id = {r.get('outcome_id'): r for r in outcomes}
|
|
277
|
+
|
|
278
|
+
for row in findings:
|
|
279
|
+
eid = str(row.get('finding_id') or row.get('evidence_id') or '')
|
|
280
|
+
if not eid or eid in seen:
|
|
281
|
+
continue
|
|
282
|
+
seen.add(eid)
|
|
283
|
+
local = localized_findings.get(eid, {})
|
|
284
|
+
# A Finding's observed effect is NOT its relation to a Claim.
|
|
285
|
+
study = studies_by_id.get(row.get('study_id'), {})
|
|
286
|
+
outcome = outcomes_by_id.get(row.get('outcome_id'), {})
|
|
287
|
+
claim_links = [l for l in links if l.get('finding_id') == eid]
|
|
288
|
+
relationships = sorted({l.get('relation_to_claim') or l.get('relation') or 'unassigned' for l in claim_links})
|
|
289
|
+
relation = (relationships[0] if len(relationships) == 1 else 'multiple') if relationships else row.get('relation_to_claim') or row.get('direction') or 'unassigned'
|
|
290
|
+
source_ids = ([row['source_id']] if row.get('source_id') else study.get('source_ids', []))
|
|
291
|
+
evidence.append({**row, 'id': eid,
|
|
292
|
+
'title': row.get('title') or row.get('measure') or row.get('finding') or eid,
|
|
293
|
+
'title_zh': local.get('title'), 'claim_zh': local.get('claim'),
|
|
294
|
+
'claim': row.get('claim') or row.get('raw_result_text'),
|
|
295
|
+
'source_id': source_ids[0] if source_ids else None, 'source_ids': source_ids,
|
|
296
|
+
'study_type': row.get('study_type') or study.get('study_design'),
|
|
297
|
+
'sample_size': first_value(row.get('sample_size'), study.get('sample_size')),
|
|
298
|
+
'audits': [a for a in audits if a.get('study_id') == row.get('study_id')],
|
|
299
|
+
'outcome_type': row.get('outcome_type') or outcome.get('outcome_type') or row.get('measure'),
|
|
300
|
+
'relation': relation, 'claim_links': claim_links,
|
|
301
|
+
'effect_direction': row.get('effect_direction') or 'not_reported',
|
|
302
|
+
'numeric': numeric_effect(row)})
|
|
303
|
+
decision = decisions[0] if decisions else result.get('decision') or {}
|
|
304
|
+
bound_revision = decision.get('graph_revision')
|
|
305
|
+
stale = bool(kind == 'project' and decision and bound_revision != active_revision)
|
|
306
|
+
decision_view = {
|
|
307
|
+
**decision,
|
|
308
|
+
'action': decision.get('decision') or decision.get('recommended_action'),
|
|
309
|
+
'confidence': decision.get('confidence_label') or decision.get('confidence'),
|
|
310
|
+
'graph_revision': bound_revision,
|
|
311
|
+
'stale': stale,
|
|
312
|
+
'supported': decision.get('supported_claims') or [],
|
|
313
|
+
'uncertain': decision.get('uncertain_claims') or decision.get('missing_evidence') or [],
|
|
314
|
+
'contradicted': decision.get('contradicted_claims') or [],
|
|
315
|
+
'rationale': decision.get('decision_rationale') or decision.get('rationale') or '',
|
|
316
|
+
}
|
|
317
|
+
if stale:
|
|
318
|
+
self.issues.append({'code': 'stale_decision', 'message': 'Latest decision is not bound to active GraphRevision'})
|
|
319
|
+
claim_nodes = [{**r, 'id': str(r.get('claim_id')), 'label': r.get('claim') or r.get('text') or r.get('claim_id'), 'kind': 'claim'} for r in claims if r.get('claim_id')]
|
|
320
|
+
nodes = [{'id': str(r.get('source_id')), 'label': r.get('title') or r.get('source_id'), 'kind': 'source'} for r in sources if r.get('source_id')]
|
|
321
|
+
nodes += [{'id': r['id'], 'label': r['title'], 'kind': 'finding'} for r in evidence]
|
|
322
|
+
nodes += claim_nodes
|
|
323
|
+
edges = []
|
|
324
|
+
studies_by_id = {r.get('study_id'): r for r in studies}
|
|
325
|
+
for r in evidence:
|
|
326
|
+
study = studies_by_id.get(r.get('study_id'), {})
|
|
327
|
+
source_ids = r.get('source_ids') or study.get('source_ids', [])
|
|
328
|
+
for sid in source_ids:
|
|
329
|
+
edges.append({'source': sid, 'target': r['id'], 'relation': 'provenance'})
|
|
330
|
+
for c in claims:
|
|
331
|
+
for eid in c.get('evidence_ids', []):
|
|
332
|
+
ev = next((r for r in evidence if r['id'] == eid), {})
|
|
333
|
+
edges.append({'source': eid, 'target': c.get('claim_id'), 'relation': ev.get('relation', 'unassigned')})
|
|
334
|
+
for link in links:
|
|
335
|
+
edges.append({'source': link.get('finding_id'), 'target': link.get('claim_id'), 'relation': link.get('relation') or link.get('relation_to_claim') or 'unassigned'})
|
|
336
|
+
node_ids = {n['id'] for n in nodes}
|
|
337
|
+
invalid_edges = [e for e in edges if e['source'] not in node_ids or e['target'] not in node_ids]
|
|
338
|
+
if invalid_edges:
|
|
339
|
+
self.issues.append({'code': 'unresolved_graph_links', 'message': f'{len(invalid_edges)} graph relationships have missing endpoints'})
|
|
340
|
+
edges = [e for e in edges if e not in invalid_edges]
|
|
341
|
+
reports = self._reports(directory, key, kind)
|
|
342
|
+
info = {
|
|
343
|
+
'id': key, 'project_id': manifest.get('project_id'), 'kind': kind,
|
|
344
|
+
'title': manifest.get('title') or (zh.get('meta') or {}).get('question') or (zh.get('decision') or {}).get('decision_question') or meta.get('question') or directory.name,
|
|
345
|
+
'title_en': manifest.get('title') or decision.get('decision_question') or meta.get('question') or directory.name,
|
|
346
|
+
'question': manifest.get('question') or meta.get('question') or '',
|
|
347
|
+
'domain': manifest.get('domain') or meta.get('domain') or result.get('research_frame', {}).get('extensions', {}).get('domain') or 'education',
|
|
348
|
+
'status': (runs[0].get('status') if runs else None) or manifest.get('status') or ('example' if kind == 'example' else 'not_started'),
|
|
349
|
+
'data_origin': meta.get('data_origin') or ('local_project' if kind == 'project' else 'not_reported'),
|
|
350
|
+
'updated_at': manifest.get('updated_at') or meta.get('generated_at'),
|
|
351
|
+
'active_revision': active_revision, 'decision': decision_view,
|
|
352
|
+
'counts': {'sources': len(sources), 'findings': len(evidence), 'claims': len(claim_nodes),
|
|
353
|
+
'studies': len({r.get('study_id') for r in findings if r.get('study_id')}),
|
|
354
|
+
'runs': len(runs)},
|
|
355
|
+
'reports': reports,
|
|
356
|
+
}
|
|
357
|
+
if kind == 'project' and active_revision is not None:
|
|
358
|
+
try:
|
|
359
|
+
if int((directory / 'graph' / 'HEAD').read_text().strip()) != active_revision:
|
|
360
|
+
raise ValueError('Graph HEAD changed during projection; retry')
|
|
361
|
+
except OSError as exc:
|
|
362
|
+
raise ValueError('Graph HEAD unavailable after read') from exc
|
|
363
|
+
return json_safe({
|
|
364
|
+
'schema_version': 1, 'project': info, 'sources': sources, 'studies': studies,
|
|
365
|
+
'evidence': evidence, 'claims': claims, 'graph': {'nodes': nodes, 'edges': edges, 'origin': 'canonical_revision' if kind == 'project' else 'result_projection'},
|
|
366
|
+
'decisions': decisions, 'revisions': revisions, 'runs': runs, 'events': events,
|
|
367
|
+
'artifacts': artifacts, 'gaps': gaps, 'iterations': iterations,
|
|
368
|
+
'applicability': decision.get('applicability_boundary') or result.get('applicability') or decision.get('applicability') or {},
|
|
369
|
+
'intervention': result.get('intervention') or ({'study_designs': designs} if designs else {}),
|
|
370
|
+
'evaluation': result.get('evaluation') or ({'analyses': analyses} if analyses else {}),
|
|
371
|
+
'decision_zh': zh.get('decision') or {}, 'issues': list(self.issues),
|
|
372
|
+
'snapshot_taken_at': datetime.now(timezone.utc).isoformat(),
|
|
373
|
+
'measurement_policy': 'No pooled effect or mean is computed by Studio. Missing values remain null.',
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
def catalog(self) -> dict:
|
|
377
|
+
projects, issues = [], []
|
|
378
|
+
for key in self._catalog_keys():
|
|
379
|
+
try:
|
|
380
|
+
detail = self.detail(key)
|
|
381
|
+
projects.append(detail['project'])
|
|
382
|
+
issues.extend({'project': key, **issue} for issue in detail['issues'])
|
|
383
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
384
|
+
issues.append({'project': key, 'code': 'invalid_project', 'message': 'Project could not be read'})
|
|
385
|
+
return {'schema_version': 1, 'mode': 'static' if self.static else 'local', 'readonly': True,
|
|
386
|
+
'generated_at': datetime.now(timezone.utc).isoformat(), 'projects': projects, 'issues': issues}
|
|
387
|
+
|
|
388
|
+
def evolution(self) -> dict:
|
|
389
|
+
# Deliberately distinct from project ResearchIterations; no arbitrary
|
|
390
|
+
# session directory or rejected candidate files are exposed.
|
|
391
|
+
rows = []
|
|
392
|
+
if not self.static:
|
|
393
|
+
path = self.examples.parent / 'autoevolve' / 'experiments.jsonl'
|
|
394
|
+
if path.is_file() and not path.is_symlink() and path.stat().st_size <= MAX_BYTES:
|
|
395
|
+
try:
|
|
396
|
+
allowed = ('experiment_id', 'session_id', 'hypothesis', 'status', 'promotion_reason', 'candidate_commit', 'parent_skill_revision')
|
|
397
|
+
rows = [{k: r.get(k) for k in allowed} for line in path.read_text().splitlines() if line.strip() for r in [json.loads(line)]]
|
|
398
|
+
except (ValueError, OSError):
|
|
399
|
+
return {'experiments': [], 'status': 'unavailable'}
|
|
400
|
+
return {'experiments': rows[-100:], 'status': 'recorded' if rows else 'not_recorded'}
|
package/engine/tribunal.py
CHANGED
|
@@ -40,6 +40,7 @@ from pathlib import Path
|
|
|
40
40
|
from engine.contracts import validate_record
|
|
41
41
|
from engine.graph_store import GraphStore
|
|
42
42
|
from engine.ids import new_local_id
|
|
43
|
+
from engine.project import ProjectWorkspace
|
|
43
44
|
from engine.semantics import claim_relation, decision_implication
|
|
44
45
|
from engine.synthesis import ClaimSynthesis, synthesize_project
|
|
45
46
|
from engine.versions import (
|
|
@@ -287,8 +288,6 @@ def _decision_action(syn_statuses: dict[str, str], confidence: dict,
|
|
|
287
288
|
|
|
288
289
|
|
|
289
290
|
|
|
290
|
-
|
|
291
|
-
|
|
292
291
|
def adjudicate(store: GraphStore, *, project: ProjectWorkspace,
|
|
293
292
|
claim_syntheses: tuple[ClaimSynthesis, ...] | None = None,
|
|
294
293
|
applicability: dict | None = None,
|
package/engine/update.py
CHANGED
package/engine/versions.py
CHANGED
|
@@ -4,7 +4,7 @@ Policy versions are frozen identifiers, not free-form strings: changing a
|
|
|
4
4
|
policy requires a new version, never silent mutation of an existing one.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
ENGINE_VERSION = "
|
|
7
|
+
ENGINE_VERSION = "6.0.0"
|
|
8
8
|
GRAPH_SCHEMA_VERSION = "2.0"
|
|
9
9
|
SOURCE_VALIDATION_POLICY_VERSION = "2026-08-12.v2"
|
|
10
10
|
METHODOLOGY_POLICY_VERSION = "2026-08-12.v2"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Validation boundary between delegated workers and canonical reasoning.
|
|
2
|
+
|
|
3
|
+
A worker may return candidate staging artifacts and prose, but it cannot mark
|
|
4
|
+
its own output trustworthy. The lead/main process reconstructs WorkerResult,
|
|
5
|
+
checks it against the originating TaskSpec and artifact validators, and only
|
|
6
|
+
then may validated artifacts enter a Judge context.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from engine.orchestration import CANONICAL_STATE_ARTIFACTS, TaskSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ArtifactValidator = Callable[[dict[str, Any]], list[str]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class WorkerResult:
|
|
21
|
+
task_id: str
|
|
22
|
+
status: str
|
|
23
|
+
staging_artifacts: tuple[dict[str, Any], ...]
|
|
24
|
+
validated: bool
|
|
25
|
+
validation_issues: tuple[str, ...] = ()
|
|
26
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
summary: str = ""
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
return {
|
|
31
|
+
"task_id": self.task_id,
|
|
32
|
+
"status": self.status,
|
|
33
|
+
"staging_artifacts": [dict(item) for item in self.staging_artifacts],
|
|
34
|
+
"validated": self.validated,
|
|
35
|
+
"validation_issues": list(self.validation_issues),
|
|
36
|
+
"metrics": dict(self.metrics),
|
|
37
|
+
"summary": self.summary,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_worker_output(
|
|
42
|
+
task: TaskSpec,
|
|
43
|
+
raw: dict[str, Any],
|
|
44
|
+
*,
|
|
45
|
+
artifact_validator: ArtifactValidator | None = None,
|
|
46
|
+
) -> WorkerResult:
|
|
47
|
+
"""Validate untrusted worker output in the lead process.
|
|
48
|
+
|
|
49
|
+
Any worker-supplied `validated` field is deliberately ignored.
|
|
50
|
+
"""
|
|
51
|
+
task.validate_for_dispatch()
|
|
52
|
+
issues: list[str] = []
|
|
53
|
+
if not isinstance(raw, dict):
|
|
54
|
+
raise ValueError("worker output must be an object")
|
|
55
|
+
if raw.get("task_id") != task.task_id:
|
|
56
|
+
issues.append("task_id mismatch")
|
|
57
|
+
status = str(raw.get("status", "failed"))
|
|
58
|
+
if status not in {"completed", "failed", "blocked"}:
|
|
59
|
+
issues.append(f"invalid worker status: {status}")
|
|
60
|
+
status = "failed"
|
|
61
|
+
artifacts = raw.get("staging_artifacts", [])
|
|
62
|
+
if not isinstance(artifacts, list) or any(not isinstance(item, dict) for item in artifacts):
|
|
63
|
+
issues.append("staging_artifacts must be a list of objects")
|
|
64
|
+
artifacts = []
|
|
65
|
+
|
|
66
|
+
expected = set(task.expected_outputs)
|
|
67
|
+
for index, artifact in enumerate(artifacts):
|
|
68
|
+
artifact_type = str(artifact.get("artifact_type", ""))
|
|
69
|
+
if not artifact_type:
|
|
70
|
+
issues.append(f"artifact[{index}] missing artifact_type")
|
|
71
|
+
continue
|
|
72
|
+
if artifact_type in CANONICAL_STATE_ARTIFACTS:
|
|
73
|
+
issues.append(f"artifact[{index}] attempts canonical output {artifact_type}")
|
|
74
|
+
if expected and artifact_type not in expected:
|
|
75
|
+
issues.append(
|
|
76
|
+
f"artifact[{index}] type {artifact_type} not allowed by TaskSpec output contract"
|
|
77
|
+
)
|
|
78
|
+
if artifact_validator is not None:
|
|
79
|
+
issues.extend(
|
|
80
|
+
f"artifact[{index}]: {problem}"
|
|
81
|
+
for problem in artifact_validator(artifact)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
validated = status == "completed" and not issues
|
|
85
|
+
return WorkerResult(
|
|
86
|
+
task_id=task.task_id,
|
|
87
|
+
status=status,
|
|
88
|
+
staging_artifacts=tuple(dict(item) for item in artifacts),
|
|
89
|
+
validated=validated,
|
|
90
|
+
validation_issues=tuple(issues),
|
|
91
|
+
metrics=dict(raw.get("metrics") or {}),
|
|
92
|
+
summary=str(raw.get("summary", "")),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def require_validated_artifacts_for_judge(
|
|
97
|
+
results: list[WorkerResult] | tuple[WorkerResult, ...],
|
|
98
|
+
) -> list[dict[str, Any]]:
|
|
99
|
+
"""Fail closed if a Judge context contains any unvalidated worker result."""
|
|
100
|
+
invalid = [result.task_id for result in results if not result.validated]
|
|
101
|
+
if invalid:
|
|
102
|
+
raise PermissionError(
|
|
103
|
+
"Judge may consume validated staging artifacts only; rejected task results: "
|
|
104
|
+
+ ",".join(invalid)
|
|
105
|
+
)
|
|
106
|
+
artifacts: list[dict[str, Any]] = []
|
|
107
|
+
for result in results:
|
|
108
|
+
artifacts.extend(dict(item) for item in result.staging_artifacts)
|
|
109
|
+
return artifacts
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Canonical workflow registry for the Decision-Grade Evidence Engine.
|
|
2
|
+
|
|
3
|
+
The scientific protocol is deliberately independent from execution adapters and
|
|
4
|
+
projections. A renderer may fail or be replaced without changing what counts
|
|
5
|
+
as completed scientific work.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SCIENTIFIC_STAGE_IDS = (
|
|
13
|
+
"frame", "retrieve", "extract", "challenge", "audit", "adjudicate",
|
|
14
|
+
"applicability", "intervene", "evaluate",
|
|
15
|
+
)
|
|
16
|
+
PROJECTION_STAGE_ID = "projection"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class WorkflowSpec:
|
|
21
|
+
workflow_id: str
|
|
22
|
+
title: str
|
|
23
|
+
stage_ids: tuple[str, ...]
|
|
24
|
+
capability_ids: tuple[str, ...]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_EVIDENCE_REVIEW = (
|
|
28
|
+
"research_framing", "literature_search", "counter_evidence_search",
|
|
29
|
+
"source_fetch", "source_validation", "study_extraction",
|
|
30
|
+
"finding_extraction", "methodology_appraisal", "claim_linking",
|
|
31
|
+
"evidence_synthesis", "tribunal", "applicability_analysis",
|
|
32
|
+
"knowledge_gap_detection",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_WORKFLOWS = {
|
|
36
|
+
"evidence_review": WorkflowSpec(
|
|
37
|
+
"evidence_review", "Evidence Review",
|
|
38
|
+
SCIENTIFIC_STAGE_IDS[:7], _EVIDENCE_REVIEW,
|
|
39
|
+
),
|
|
40
|
+
"decision_and_pilot": WorkflowSpec(
|
|
41
|
+
"decision_and_pilot", "Decision & Pilot",
|
|
42
|
+
SCIENTIFIC_STAGE_IDS[:8], _EVIDENCE_REVIEW + ("intervention_design",),
|
|
43
|
+
),
|
|
44
|
+
"evaluate_and_update": WorkflowSpec(
|
|
45
|
+
"evaluate_and_update", "Evaluate & Update",
|
|
46
|
+
("evaluate",), ("evaluation_design", "data_validation", "data_analysis"),
|
|
47
|
+
),
|
|
48
|
+
"full_research_cycle": WorkflowSpec(
|
|
49
|
+
"full_research_cycle", "Full Research Cycle",
|
|
50
|
+
SCIENTIFIC_STAGE_IDS,
|
|
51
|
+
_EVIDENCE_REVIEW + ("intervention_design", "evaluation_design", "data_validation", "data_analysis"),
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def workflow_registry() -> dict[str, WorkflowSpec]:
|
|
57
|
+
"""Return the immutable workflow catalogue keyed by user intent."""
|
|
58
|
+
return dict(_WORKFLOWS)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def workflow(workflow_id: str) -> WorkflowSpec:
|
|
62
|
+
try:
|
|
63
|
+
return _WORKFLOWS[workflow_id]
|
|
64
|
+
except KeyError as exc:
|
|
65
|
+
raise ValueError(f"unknown workflow {workflow_id!r}") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def execution_stages() -> tuple[str, ...]:
|
|
69
|
+
"""Canonical execution order, with Projection explicitly outside science."""
|
|
70
|
+
return SCIENTIFIC_STAGE_IDS + (PROJECTION_STAGE_ID,)
|