eduevidence 5.2.0 → 6.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/CONTRIBUTING.md +105 -0
- package/README.md +142 -75
- package/README.zh-CN.md +73 -30
- package/SKILL.md +397 -131
- package/agents/openai.yaml +4 -0
- package/assets/readme/controlled-execution.svg +34 -0
- package/assets/readme/landing-tour.gif +0 -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/assets/readme/studio-tour.gif +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/bin/eduevidence.js +2 -1
- package/docs/architecture.md +496 -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/docs/sciverse-api.md +125 -0
- package/eduevidence_cli.py +29 -13
- 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/decision_policy.py +96 -0
- package/engine/evidence_graph.py +14 -10
- package/engine/evidencecore.py +7 -5
- package/engine/gaps.py +132 -73
- package/engine/ids.py +2 -0
- package/engine/judge_pack.py +65 -0
- package/engine/library.py +6 -2
- package/engine/library_builtin.py +3 -1
- package/engine/living.py +36 -5
- package/engine/meta_synthesis.py +3 -1
- package/engine/migration.py +88 -3
- package/engine/orchestration.py +460 -0
- package/engine/paths.py +2 -0
- package/engine/pilot.py +36 -33
- package/engine/project.py +2 -2
- package/engine/research_service.py +113 -0
- package/engine/studio_read_model.py +400 -0
- package/engine/taxonomy.py +211 -0
- package/engine/tribunal.py +44 -33
- 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 +2934 -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/final_verdict.json +107 -0
- package/examples/ai-coding-assistant-evidence/frame.json +48 -0
- package/examples/ai-coding-assistant-evidence/gate_report.json +101 -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 +230 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +2934 -0
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +2934 -0
- package/examples/ai-coding-assistant-evidence/result.json +1457 -0
- package/examples/ai-coding-assistant-evidence/result.zh.json +1457 -0
- package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
- package/examples/ai-coding-assistant-evidence/sources.jsonl +8 -0
- package/examples/ai-coding-assistant-evidence/verdict.json +107 -0
- package/examples/spaced-retrieval-practice/applicability.json +14 -0
- package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
- package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
- package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
- package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
- package/examples/spaced-retrieval-practice/frame.json +58 -0
- package/examples/spaced-retrieval-practice/gate_report.json +101 -0
- package/examples/spaced-retrieval-practice/methodology.json +78 -0
- package/examples/spaced-retrieval-practice/report_spec.json +212 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
- package/examples/spaced-retrieval-practice/result.json +942 -0
- package/examples/spaced-retrieval-practice/result.zh.json +942 -0
- package/examples/spaced-retrieval-practice/skeptic.json +70 -0
- package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
- package/examples/spaced-retrieval-practice/verdict.json +93 -0
- package/examples/workplace-ai-assistant/artifact_manifest.json +15 -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/final_verdict.json +78 -0
- package/examples/workplace-ai-assistant/frame.json +41 -0
- package/examples/workplace-ai-assistant/gate_report.json +101 -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 +224 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
- package/examples/workplace-ai-assistant/result.json +615 -0
- package/examples/workplace-ai-assistant/result.zh.json +615 -0
- package/examples/workplace-ai-assistant/search_log.json +19 -0
- package/examples/workplace-ai-assistant/skeptic.json +72 -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 +78 -0
- package/install.sh +7 -7
- package/integrations/agent_mcp.py +2 -2
- package/integrations/orchestration_dispatch.py +146 -0
- package/package.json +46 -3
- package/pyproject.toml +14 -22
- package/references/autoresearch.md +30 -0
- package/references/evaluation-policy.md +24 -0
- package/references/orchestration.md +22 -0
- package/references/report-copy-style.md +67 -0
- package/references/retrieval-compliance.md +75 -0
- package/references/retrieval-protocol.md +20 -0
- package/references/scientific-invariants.md +19 -0
- package/retrieval/audit.py +178 -0
- package/retrieval/fetch.py +96 -0
- package/retrieval/sciverse.py +398 -0
- package/retrieval/search.py +47 -7
- package/schemas/applicability.schema.json +94 -0
- package/schemas/chart-spec.schema.json +10 -3
- package/schemas/evidence.schema.json +316 -43
- package/schemas/fetch-result.schema.json +2 -1
- package/schemas/intervention.schema.json +106 -21
- package/schemas/report-result.schema.json +12 -4
- package/schemas/report-spec.schema.json +98 -100
- package/schemas/skeptic.schema.json +86 -0
- package/schemas/source.schema.json +21 -2
- package/schemas/v2/finding.schema.json +5 -1
- package/schemas/v2/methodology-audit.schema.json +5 -1
- package/schemas/v2/outcome.schema.json +28 -5
- package/schemas/v2/project.schema.json +2 -2
- package/schemas/v2/run.schema.json +1 -1
- package/schemas/v2/study.schema.json +5 -1
- package/schemas/vNext/autoevolve-session.schema.json +34 -0
- package/schemas/vNext/eval-snapshot.schema.json +77 -0
- package/schemas/vNext/execution-plan.schema.json +50 -0
- package/schemas/vNext/gap-priority.schema.json +54 -0
- package/schemas/vNext/negative-search-record.schema.json +68 -0
- package/schemas/vNext/research-iteration.schema.json +87 -0
- package/schemas/vNext/research-strategy.schema.json +62 -0
- package/schemas/vNext/skill-experiment.schema.json +90 -0
- package/schemas/vNext/task-spec.schema.json +156 -0
- package/schemas/vNext/worker-result.schema.json +60 -0
- package/schemas/verdict.schema.json +164 -28
- package/scripts/benchmark_judge.py +2 -2
- package/scripts/benchmark_v3.py +26 -43
- package/scripts/build_esl_artifacts.py +4 -4
- 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 +101 -0
- package/scripts/build_result.py +74 -9
- package/scripts/check_autoresearch_invariants.py +95 -0
- package/scripts/check_package_parity.py +85 -0
- package/scripts/check_protocol_alignment.py +375 -0
- package/scripts/check_versioned_schemas.py +254 -0
- package/scripts/claim_audit.py +13 -8
- package/scripts/compute_confidence.py +10 -0
- package/scripts/daily_evolve.py +30 -0
- package/scripts/dashboard_server.py +130 -101
- package/scripts/did_regression.py +17 -32
- package/scripts/enrich_projects_human_and_lieflat.py +1 -1
- package/scripts/evidence_score.py +5 -2
- package/scripts/generate_metrics.py +4 -3
- package/scripts/generate_new_projects.py +5 -5
- package/scripts/orchestrator.py +286 -36
- package/scripts/pre_verdict_gate.py +224 -26
- package/scripts/quickstart.py +18 -2
- package/scripts/rebake_all_5themes.py +1 -2
- package/scripts/research_auto_cli.py +475 -0
- package/scripts/run_workspace.py +24 -8
- 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 +81 -0
- package/scripts/test_adversarial_empirical.py +26 -19
- package/scripts/validate_schema.py +46 -2
- package/scripts/vnext_cli.py +133 -0
- package/setup.py +12 -0
- package/skill/agents/evaluation-designer.md +20 -4
- package/skill/agents/evidence-analyst.md +19 -3
- package/skill/agents/evidence-judge.md +50 -2
- package/skill/agents/evidence-retriever.md +20 -3
- package/skill/agents/intervention-designer.md +20 -4
- package/skill/agents/method-reviewer.md +18 -2
- package/skill/agents/{education-planner.md → research-planner.md} +19 -3
- package/skill/agents/skeptic.md +18 -2
- package/skill/roles/registry.yaml +45 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
- package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
- package/skill/sub-skills/data-analysis/SKILL.md +34 -15
- package/skill/sub-skills/ethics-review/SKILL.md +33 -10
- package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
- package/skill/sub-skills/evidence-review/SKILL.md +31 -12
- package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
- package/skill/sub-skills/literature-review/SKILL.md +35 -14
- package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
- package/skill/sub-skills/report-generation/SKILL.md +40 -6
- package/skill/sub-skills/research-planning/SKILL.md +41 -14
- package/skill/sub-skills/study-design/SKILL.md +30 -9
- package/skill/task-briefs/adjudicate.md +32 -7
- package/skill/task-briefs/applicability.md +38 -0
- package/skill/task-briefs/audit.md +32 -7
- package/skill/task-briefs/challenge.md +34 -5
- package/skill/task-briefs/evaluate.md +30 -5
- package/skill/task-briefs/extract.md +31 -8
- package/skill/task-briefs/frame.md +39 -10
- package/skill/task-briefs/intervene.md +32 -6
- package/skill/task-briefs/present.md +32 -8
- package/skill/task-briefs/projection.md +37 -0
- package/skill/task-briefs/retrieve.md +36 -6
- package/skill/workflows/decision-and-pilot.md +85 -0
- package/skill/workflows/evaluate-and-update.md +93 -0
- package/skill/workflows/evidence-review.md +117 -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_figures.py +25 -3
- package/visualization/eduevidence-report/scripts/build_infographics.py +5 -1
- package/visualization/eduevidence-report/scripts/build_report.py +561 -121
- package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +371 -136
- package/visualization/eduevidence-report/scripts/zh_labels.py +80 -1
- 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/architecture.html +14885 -0
- package/web/index.html +53 -0
- package/web/studio/THIRD_PARTY_LICENSES.txt +146 -0
- package/web/studio/assets/index-B8tkF44Q.css +1 -0
- package/web/studio/assets/index-CQ6Keoyc.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,113 @@
|
|
|
1
|
+
"""Project-scoped durable control plane.
|
|
2
|
+
|
|
3
|
+
This service is the only write API intended for CLI and console integrations.
|
|
4
|
+
It keeps immutable artifact bytes and replayable events in SQLite WAL, while
|
|
5
|
+
preserving the existing ProjectWorkspace and graph store implementations.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from engine.project import ProjectWorkspace
|
|
17
|
+
from engine.run import finish_run, start_run
|
|
18
|
+
|
|
19
|
+
RUN_STATUSES = frozenset({
|
|
20
|
+
"queued", "running", "waiting_for_user", "waiting_for_user_data", "waiting_for_executor",
|
|
21
|
+
"waiting_for_tool", "waiting_for_review", "blocked_scientific_gate", "blocked_contract_error",
|
|
22
|
+
"completed", "failed_recoverable", "failed_terminal", "cancelled",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _now() -> str:
|
|
27
|
+
return datetime.now(timezone.utc).isoformat()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResearchService:
|
|
31
|
+
def __init__(self, home: Path):
|
|
32
|
+
self.home = Path(home).expanduser().resolve()
|
|
33
|
+
self.home.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
self.db_path = self.home / "research-control.sqlite3"
|
|
35
|
+
self._init_db()
|
|
36
|
+
|
|
37
|
+
def _connect(self) -> sqlite3.Connection:
|
|
38
|
+
connection = sqlite3.connect(self.db_path)
|
|
39
|
+
connection.row_factory = sqlite3.Row
|
|
40
|
+
connection.execute("PRAGMA journal_mode=WAL")
|
|
41
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
42
|
+
return connection
|
|
43
|
+
|
|
44
|
+
def _init_db(self) -> None:
|
|
45
|
+
with self._connect() as db:
|
|
46
|
+
db.executescript("""
|
|
47
|
+
CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL, run_id TEXT, type TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS artifacts (artifact_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT, artifact_type TEXT NOT NULL, sha256 TEXT NOT NULL, path TEXT NOT NULL, created_at TEXT NOT NULL, metadata TEXT NOT NULL);
|
|
49
|
+
""")
|
|
50
|
+
|
|
51
|
+
def _event(self, project_id: str, event_type: str, payload: dict[str, Any], run_id: str | None = None) -> int:
|
|
52
|
+
with self._connect() as db:
|
|
53
|
+
cursor = db.execute("INSERT INTO events(project_id,run_id,type,payload,created_at) VALUES(?,?,?,?,?)",
|
|
54
|
+
(project_id, run_id, event_type, json.dumps(payload, ensure_ascii=False, sort_keys=True), _now()))
|
|
55
|
+
return int(cursor.lastrowid)
|
|
56
|
+
|
|
57
|
+
def create_project(self, *, question: str, title: str, research_mode: str = "evidence_review", domain: str = "education") -> ProjectWorkspace:
|
|
58
|
+
project = ProjectWorkspace.create(self.home, question=question, title=title, research_mode=research_mode, domain=domain)
|
|
59
|
+
self._event(project.project_id, "project_created", project.manifest())
|
|
60
|
+
return project
|
|
61
|
+
|
|
62
|
+
def start_run(self, project_id: str, *, purpose: str, capabilities: list[str], execution_backend: str = "sequential_main_agent") -> dict:
|
|
63
|
+
project = ProjectWorkspace.open(self.home, project_id)
|
|
64
|
+
run = start_run(project, purpose=purpose, capabilities=capabilities, execution_backend=execution_backend)
|
|
65
|
+
self._event(project_id, "run_started", run, run["run_id"])
|
|
66
|
+
return run
|
|
67
|
+
|
|
68
|
+
def submit_artifact(self, project_id: str, *, artifact_type: str, content: bytes, run_id: str | None = None,
|
|
69
|
+
metadata: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
70
|
+
digest = hashlib.sha256(content).hexdigest()
|
|
71
|
+
# Bytes are content-addressed; association identity is project/run/type scoped.
|
|
72
|
+
# Identical bytes in two projects must not erase the second association.
|
|
73
|
+
identity = json.dumps([project_id, run_id, artifact_type, digest, metadata or {}],
|
|
74
|
+
ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
75
|
+
artifact_id = f"ART-{hashlib.sha256(identity.encode('utf-8')).hexdigest()[:24]}"
|
|
76
|
+
directory = self.home / "artifacts" / digest[:2]
|
|
77
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
path = directory / digest
|
|
79
|
+
if not path.exists():
|
|
80
|
+
path.write_bytes(content)
|
|
81
|
+
record = {"artifact_id": artifact_id, "project_id": project_id, "run_id": run_id,
|
|
82
|
+
"artifact_type": artifact_type, "sha256": digest, "path": str(path),
|
|
83
|
+
"created_at": _now(), "metadata": metadata or {}}
|
|
84
|
+
with self._connect() as db:
|
|
85
|
+
db.execute("INSERT OR IGNORE INTO artifacts VALUES(?,?,?,?,?,?,?,?)",
|
|
86
|
+
(artifact_id, project_id, run_id, artifact_type, digest, str(path), record["created_at"], json.dumps(record["metadata"], ensure_ascii=False, sort_keys=True)))
|
|
87
|
+
persisted = db.execute("SELECT * FROM artifacts WHERE artifact_id=?", (artifact_id,)).fetchone()
|
|
88
|
+
record = dict(persisted)
|
|
89
|
+
record["metadata"] = json.loads(record["metadata"])
|
|
90
|
+
self._event(project_id, "artifact_submitted", record, run_id)
|
|
91
|
+
return record
|
|
92
|
+
|
|
93
|
+
def events(self, project_id: str, *, after_seq: int = 0) -> list[dict[str, Any]]:
|
|
94
|
+
with self._connect() as db:
|
|
95
|
+
rows = db.execute("SELECT * FROM events WHERE project_id=? AND seq>? ORDER BY seq", (project_id, after_seq)).fetchall()
|
|
96
|
+
return [{**dict(row), "payload": json.loads(row["payload"])} for row in rows]
|
|
97
|
+
|
|
98
|
+
def projects(self) -> list[dict[str, Any]]:
|
|
99
|
+
directory = self.home / "projects"
|
|
100
|
+
if not directory.is_dir():
|
|
101
|
+
return []
|
|
102
|
+
return [json.loads(manifest.read_text(encoding="utf-8"))
|
|
103
|
+
for manifest in sorted(directory.glob("*/project.json"))]
|
|
104
|
+
|
|
105
|
+
def runs(self, project_id: str) -> list[dict[str, Any]]:
|
|
106
|
+
project = ProjectWorkspace.open(self.home, project_id)
|
|
107
|
+
return [json.loads(path.read_text(encoding="utf-8"))
|
|
108
|
+
for path in sorted(project.runs_dir().glob("*/run.json"))]
|
|
109
|
+
|
|
110
|
+
def artifacts(self, project_id: str) -> list[dict[str, Any]]:
|
|
111
|
+
with self._connect() as db:
|
|
112
|
+
rows = db.execute("SELECT * FROM artifacts WHERE project_id=? ORDER BY created_at", (project_id,)).fetchall()
|
|
113
|
+
return [{**dict(row), "metadata": json.loads(row["metadata"])} for row in rows]
|
|
@@ -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'}
|