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,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from engine.autoevolve import DailyEvolutionRunner, DailyProfile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(argv=None):
|
|
8
|
+
parser = argparse.ArgumentParser(description="Run bounded branch-only EduEvidence evolution")
|
|
9
|
+
parser.add_argument("--repo", default=".")
|
|
10
|
+
parser.add_argument("--agent-command", required=True)
|
|
11
|
+
parser.add_argument("--eval-command", required=True)
|
|
12
|
+
parser.add_argument("--run-tag")
|
|
13
|
+
parser.add_argument("--max-experiments", type=int, default=20)
|
|
14
|
+
parser.add_argument("--max-cost-usd", type=float, default=5)
|
|
15
|
+
parser.add_argument("--max-wall-minutes", type=int, default=180)
|
|
16
|
+
parser.add_argument("--push-branch", action="store_true")
|
|
17
|
+
args = parser.parse_args(argv)
|
|
18
|
+
profile = DailyProfile(args.max_experiments, args.max_cost_usd, args.max_wall_minutes)
|
|
19
|
+
report = DailyEvolutionRunner(args.repo, profile=profile).run(
|
|
20
|
+
agent_command=args.agent_command,
|
|
21
|
+
eval_command=args.eval_command,
|
|
22
|
+
run_tag=args.run_tag,
|
|
23
|
+
push_branch=args.push_branch,
|
|
24
|
+
)
|
|
25
|
+
print(json.dumps(report, indent=2))
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
raise SystemExit(main())
|
|
@@ -18,20 +18,29 @@ Usage:
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import argparse
|
|
21
|
+
import errno
|
|
22
|
+
import mimetypes
|
|
21
23
|
import http.server
|
|
22
24
|
import json
|
|
25
|
+
import os
|
|
23
26
|
import sys
|
|
24
27
|
import urllib.parse
|
|
25
28
|
from pathlib import Path
|
|
29
|
+
from typing import Any, Dict, List, Optional
|
|
26
30
|
|
|
27
31
|
ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
-
WEB_DIR = ROOT / "web"
|
|
29
|
-
EXAMPLES_DIR = ROOT / "examples"
|
|
30
32
|
|
|
31
33
|
if str(ROOT) not in sys.path:
|
|
32
34
|
sys.path.insert(0, str(ROOT))
|
|
33
35
|
|
|
36
|
+
from engine._resources import resource_root # noqa: E402
|
|
37
|
+
ROOT = resource_root()
|
|
38
|
+
WEB_DIR = ROOT / "web"
|
|
39
|
+
EXAMPLES_DIR = ROOT / "examples"
|
|
40
|
+
RESEARCH_HOME = Path(os.environ.get("EDUEVIDENCE_HOME", Path.cwd() / ".eduevidence"))
|
|
41
|
+
|
|
34
42
|
from engine.evidence_graph import EvidenceGraph # noqa: E402
|
|
43
|
+
from engine.studio_read_model import StudioReader, numeric_effect # noqa: E402
|
|
35
44
|
|
|
36
45
|
import sys as _sys
|
|
37
46
|
_VIZ_SCRIPTS = ROOT / "visualization" / "eduevidence-report" / "scripts"
|
|
@@ -39,8 +48,6 @@ if str(_VIZ_SCRIPTS) not in _sys.path:
|
|
|
39
48
|
_sys.path.insert(0, str(_VIZ_SCRIPTS))
|
|
40
49
|
from zh_labels import OUTCOME_ZH, ACTION_ZH, STUDY_ZH, AUTHORITY_ZH, CONFIDENCE_ZH # noqa: E402
|
|
41
50
|
|
|
42
|
-
# Friendly display names for known demo projects; everything else falls back to
|
|
43
|
-
# the question text inside result.json so new projects need no manual entry.
|
|
44
51
|
PROJECT_TITLES: Dict[str, str] = {
|
|
45
52
|
"ai-coding-assistant-evidence": "高校大一引入 AI 编程助手(真实文献旗舰示例)",
|
|
46
53
|
"highschool-math-ai-tutor": "高中数学引入大模型自适应 AI Tutor 评估",
|
|
@@ -51,7 +58,6 @@ PROJECT_TITLES: Dict[str, str] = {
|
|
|
51
58
|
|
|
52
59
|
|
|
53
60
|
def unquote_path(path: str) -> str:
|
|
54
|
-
"""Decode a URL path once and reject encoded separators / NUL bytes."""
|
|
55
61
|
decoded = urllib.parse.unquote(path)
|
|
56
62
|
if "\x00" in decoded or "\\" in decoded:
|
|
57
63
|
raise ValueError("unsafe path characters")
|
|
@@ -66,7 +72,6 @@ def _read_json(path: Path) -> Optional[Any]:
|
|
|
66
72
|
|
|
67
73
|
|
|
68
74
|
def _decision_fields(decision: Any) -> tuple[Any, Any]:
|
|
69
|
-
"""Return (verdict, confidence) tolerating the three decision shapes in the wild."""
|
|
70
75
|
if not isinstance(decision, dict):
|
|
71
76
|
return None, None
|
|
72
77
|
verdict = decision.get("verdict") or decision.get("recommended_action") or decision.get("decision")
|
|
@@ -77,21 +82,11 @@ def _decision_fields(decision: Any) -> tuple[Any, Any]:
|
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
def _extract_effect(ev: Dict[str, Any]) -> tuple[Any, Any, Any]:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if isinstance(es, dict):
|
|
83
|
-
return es.get("value"), (es.get("ci_lower") or es.get("ci_lo")), (es.get("ci_upper") or es.get("ci_hi"))
|
|
84
|
-
value = es if isinstance(es, (int, float)) else None
|
|
85
|
-
if value is None:
|
|
86
|
-
for key in ("hedges_g", "g", "effect_size_value"):
|
|
87
|
-
if isinstance(ev.get(key), (int, float)):
|
|
88
|
-
value = ev[key]
|
|
89
|
-
break
|
|
90
|
-
return value, ev.get("ci_lower"), ev.get("ci_upper")
|
|
85
|
+
numeric = numeric_effect(ev)
|
|
86
|
+
return numeric["value"], numeric["ci_lower"], numeric["ci_upper"]
|
|
91
87
|
|
|
92
88
|
|
|
93
89
|
def _graph_node_count(path: Path) -> int:
|
|
94
|
-
"""与 evidence_graph.export_echarts_graph() 同口径的节点数。"""
|
|
95
90
|
try:
|
|
96
91
|
graph = EvidenceGraph.from_json(path.read_text(encoding="utf-8"))
|
|
97
92
|
export = graph.export_echarts_graph()
|
|
@@ -108,13 +103,12 @@ def _graph_node_count(path: Path) -> int:
|
|
|
108
103
|
|
|
109
104
|
|
|
110
105
|
def _direction_counts(evidence: list) -> Dict[str, int]:
|
|
111
|
-
"""按 evidence 的 relation_to_claim / direction / effect_direction 聚合。"""
|
|
112
106
|
counts = {"support": 0, "contradict": 0, "neutral": 0}
|
|
113
107
|
for ev in evidence:
|
|
114
108
|
if not isinstance(ev, dict):
|
|
115
109
|
continue
|
|
116
110
|
d = (ev.get("relation_to_claim") or ev.get("direction")
|
|
117
|
-
or
|
|
111
|
+
or "").lower()
|
|
118
112
|
if d in ("support", "supports", "positive", "pos"):
|
|
119
113
|
counts["support"] += 1
|
|
120
114
|
elif d in ("contradict", "contradicts", "negative", "neg"):
|
|
@@ -125,14 +119,13 @@ def _direction_counts(evidence: list) -> Dict[str, int]:
|
|
|
125
119
|
|
|
126
120
|
|
|
127
121
|
def _outcome_rollup(evidence: list) -> List[Dict[str, Any]]:
|
|
128
|
-
"""无 outcome_mapping 时按 outcome_type 聚合方向计数(回退链)。"""
|
|
129
122
|
roll: Dict[str, Dict[str, int]] = {}
|
|
130
123
|
for ev in evidence:
|
|
131
124
|
if not isinstance(ev, dict):
|
|
132
125
|
continue
|
|
133
126
|
ot = ev.get("outcome_type") or ev.get("outcome") or "other"
|
|
134
127
|
d = (ev.get("relation_to_claim") or ev.get("direction")
|
|
135
|
-
or
|
|
128
|
+
or "").lower()
|
|
136
129
|
bucket = roll.setdefault(ot, {"support": 0, "contradict": 0, "neutral": 0})
|
|
137
130
|
if d in ("support", "supports", "positive", "pos"):
|
|
138
131
|
bucket["support"] += 1
|
|
@@ -144,7 +137,6 @@ def _outcome_rollup(evidence: list) -> List[Dict[str, Any]]:
|
|
|
144
137
|
|
|
145
138
|
|
|
146
139
|
def _known_project_ids() -> set:
|
|
147
|
-
"""Project ids = directories with a real result.json (not the deduped display list)."""
|
|
148
140
|
if not EXAMPLES_DIR.exists():
|
|
149
141
|
return set()
|
|
150
142
|
return {d.name for d in EXAMPLES_DIR.iterdir()
|
|
@@ -157,18 +149,19 @@ def scan_local_projects() -> List[Dict[str, Any]]:
|
|
|
157
149
|
return projects
|
|
158
150
|
|
|
159
151
|
for proj_dir in sorted(EXAMPLES_DIR.iterdir()):
|
|
152
|
+
# List canonical examples once; legacy aliases remain valid lookup IDs.
|
|
153
|
+
if proj_dir.is_symlink():
|
|
154
|
+
continue
|
|
160
155
|
if not proj_dir.is_dir():
|
|
161
156
|
continue
|
|
162
157
|
result_path = proj_dir / "result.json"
|
|
163
158
|
if not result_path.exists():
|
|
164
159
|
continue
|
|
165
|
-
|
|
166
160
|
result = _read_json(result_path) or {}
|
|
167
161
|
meta = result.get("meta") or {}
|
|
168
162
|
frame = result.get("research_frame") or {}
|
|
169
163
|
question = meta.get("question") or frame.get("question") or ""
|
|
170
164
|
verdict, confidence = _decision_fields(result.get("decision"))
|
|
171
|
-
|
|
172
165
|
evidence = result.get("evidence") or []
|
|
173
166
|
forest = result.get("forest_plot_data") or []
|
|
174
167
|
effect_values: List[float] = []
|
|
@@ -179,48 +172,48 @@ def scan_local_projects() -> List[Dict[str, Any]]:
|
|
|
179
172
|
for f in forest:
|
|
180
173
|
if isinstance(f.get("effect_size"), (int, float)):
|
|
181
174
|
effect_values.append(float(f["effect_size"]))
|
|
182
|
-
|
|
183
175
|
graph_path = proj_dir / "evidence_graph.json"
|
|
184
|
-
html_path = proj_dir / "
|
|
176
|
+
html_path = proj_dir / "reports-5themes" / "EduEvidence_Report_claude.html"
|
|
177
|
+
if not html_path.is_file():
|
|
178
|
+
html_path = proj_dir / "EduEvidence_Report.html"
|
|
185
179
|
report_variants: List[Dict[str, str]] = []
|
|
186
180
|
themes_dir = proj_dir / "reports-5themes"
|
|
187
181
|
if themes_dir.is_dir():
|
|
188
182
|
for variant_file in sorted(list(themes_dir.glob("EduEvidence_Report_*.html")) + list(themes_dir.glob("report_*.html"))):
|
|
189
183
|
theme_name = variant_file.stem.replace("EduEvidence_Report_", "").replace("report_", "")
|
|
190
184
|
if not any(v["theme"] == theme_name for v in report_variants):
|
|
191
|
-
report_variants.append({
|
|
192
|
-
"theme": theme_name,
|
|
193
|
-
"path": str(variant_file),
|
|
194
|
-
})
|
|
195
|
-
|
|
185
|
+
report_variants.append({"theme": theme_name, "path": str(variant_file)})
|
|
196
186
|
zh_result = _read_json(proj_dir / "result.zh.json") or {}
|
|
197
187
|
zh_question = ((zh_result.get("meta") or {}).get("question")
|
|
198
188
|
or (zh_result.get("research_frame") or {}).get("question") or "")
|
|
199
|
-
|
|
200
189
|
projects.append({
|
|
201
190
|
"id": proj_dir.name,
|
|
202
191
|
"title": PROJECT_TITLES.get(proj_dir.name) or (zh_question[:72] if zh_question else (question[:72] or proj_dir.name)),
|
|
203
192
|
"title_zh": zh_question[:72] if zh_question else None,
|
|
204
|
-
"domain": meta.get("domain") or "education",
|
|
193
|
+
"domain": meta.get("domain") or result.get("research_frame", {}).get("extensions", {}).get("domain") or "education",
|
|
205
194
|
"question": question,
|
|
206
195
|
"verdict": verdict,
|
|
207
196
|
"confidence": confidence,
|
|
208
197
|
"evidence_count": len(evidence) if isinstance(evidence, list) else 0,
|
|
209
198
|
"forest_count": len(forest) if isinstance(forest, list) else 0,
|
|
210
|
-
"effect_count": len(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
199
|
+
"effect_count": len({
|
|
200
|
+
ev.get("evidence_id") or ev.get("finding_id") or f"e-{i}"
|
|
201
|
+
for i, ev in enumerate(evidence) if _extract_effect(ev)[0] is not None
|
|
202
|
+
} | {
|
|
203
|
+
f.get("evidence_id") or f.get("finding_id") or f"f-{i}"
|
|
204
|
+
for i, f in enumerate(forest) if _extract_effect(f)[0] is not None
|
|
205
|
+
}),
|
|
206
|
+
"mean_effect_size": None, # Distinct outcomes/metrics cannot be averaged by a viewer.
|
|
207
|
+
"direction_counts": _direction_counts(evidence) if isinstance(evidence, list) else {"support": 0, "contradict": 0, "neutral": 0},
|
|
214
208
|
"has_graph": graph_path.exists(),
|
|
215
209
|
"node_count": _graph_node_count(graph_path) if graph_path.exists() else 0,
|
|
216
210
|
"html_report_path": str(html_path) if html_path.exists() else None,
|
|
217
211
|
"report_variants": report_variants,
|
|
218
212
|
})
|
|
219
|
-
# Canonical question deduplication: keep the project with the highest evidence count
|
|
220
213
|
deduped: Dict[str, Dict[str, Any]] = {}
|
|
221
214
|
for p in projects:
|
|
222
215
|
norm_q = (p.get("question") or p["id"]).strip().lower()
|
|
223
|
-
if norm_q not in deduped or
|
|
216
|
+
if norm_q not in deduped or p["evidence_count"] > deduped[norm_q]["evidence_count"]:
|
|
224
217
|
deduped[norm_q] = p
|
|
225
218
|
return list(deduped.values())
|
|
226
219
|
|
|
@@ -235,12 +228,6 @@ def build_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
235
228
|
|
|
236
229
|
|
|
237
230
|
def get_aggregate_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
238
|
-
"""V2 兼容封装(旧 API/测试沿用):build_stats 字段。
|
|
239
|
-
|
|
240
|
-
Token / 成本矩阵已移除(provenance 纠偏):历史版本在此返回硬编码的
|
|
241
|
-
假 token 用量与模型成本,没有任何真实运行记录支撑。运行时未采集
|
|
242
|
-
usage 前,这些指标一律如实标注 NOT_CAPTURED,不再虚构数值。
|
|
243
|
-
"""
|
|
244
231
|
stats = build_stats(projects)
|
|
245
232
|
stats["total_tokens"] = None
|
|
246
233
|
stats["usage_measurement_status"] = "NOT_CAPTURED"
|
|
@@ -255,7 +242,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
|
255
242
|
question = meta.get("question") or frame.get("question") or ""
|
|
256
243
|
verdict, confidence = _decision_fields(result.get("decision"))
|
|
257
244
|
title = PROJECT_TITLES.get(proj_id) or question[:72] or proj_id
|
|
258
|
-
|
|
259
245
|
forest_items: List[Dict[str, Any]] = []
|
|
260
246
|
for f in (result.get("forest_plot_data") or []):
|
|
261
247
|
forest_items.append({
|
|
@@ -269,7 +255,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
|
269
255
|
"direction": f.get("direction"),
|
|
270
256
|
"wwc_rating": f.get("wwc_rating") or "",
|
|
271
257
|
})
|
|
272
|
-
|
|
273
258
|
effect_items: List[Dict[str, Any]] = []
|
|
274
259
|
for ev in (result.get("evidence") or []):
|
|
275
260
|
value, lo, hi = _extract_effect(ev)
|
|
@@ -293,7 +278,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
|
293
278
|
"outcome_dimension": f.get("outcome_dimension") or f.get("outcome_metric") or "",
|
|
294
279
|
"direction": f.get("direction"),
|
|
295
280
|
})
|
|
296
|
-
|
|
297
281
|
evidence_list = result.get("evidence") or []
|
|
298
282
|
outcome_items: List[Dict[str, Any]] = []
|
|
299
283
|
om = result.get("outcome_mapping") or {}
|
|
@@ -308,13 +292,8 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
|
308
292
|
"neutral_count": e.get("neutral_count") or 0,
|
|
309
293
|
})
|
|
310
294
|
else:
|
|
311
|
-
# 回退链:无 outcome_mapping 时按 evidence 方向聚合
|
|
312
295
|
outcome_items = _outcome_rollup(evidence_list)
|
|
313
|
-
|
|
314
|
-
# 方向分布(任何课题都有值)
|
|
315
|
-
direction_counts = _direction_counts(evidence_list) if isinstance(evidence_list, list) else \
|
|
316
|
-
{"support": 0, "contradict": 0, "neutral": 0}
|
|
317
|
-
|
|
296
|
+
direction_counts = _direction_counts(evidence_list) if isinstance(evidence_list, list) else {"support": 0, "contradict": 0, "neutral": 0}
|
|
318
297
|
graph = None
|
|
319
298
|
graph_path = proj_dir / "evidence_graph.json"
|
|
320
299
|
if graph_path.exists():
|
|
@@ -322,7 +301,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
|
322
301
|
graph = EvidenceGraph.from_json(graph_path.read_text(encoding="utf-8")).export_echarts_graph()
|
|
323
302
|
except Exception:
|
|
324
303
|
graph = None
|
|
325
|
-
|
|
326
304
|
return {
|
|
327
305
|
"id": proj_id,
|
|
328
306
|
"title": title,
|
|
@@ -344,26 +322,18 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
|
|
|
344
322
|
}
|
|
345
323
|
|
|
346
324
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
325
|
class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
354
|
-
"""Serves web/ statically and exposes the artifact JSON API."""
|
|
355
|
-
|
|
356
326
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
357
327
|
super().__init__(*args, directory=str(WEB_DIR), **kwargs)
|
|
358
328
|
|
|
359
|
-
# -- response helpers -------------------------------------------------
|
|
360
329
|
def _send_bytes(self, data: bytes, content_type: str, status: int = 200,
|
|
361
330
|
extra_headers: Optional[Dict[str, str]] = None) -> None:
|
|
362
331
|
self.send_response(status)
|
|
363
332
|
self.send_header("Content-Type", content_type)
|
|
364
333
|
self.send_header("Content-Length", str(len(data)))
|
|
365
|
-
|
|
366
|
-
self.send_header("
|
|
334
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
335
|
+
self.send_header("Referrer-Policy", "no-referrer")
|
|
336
|
+
self.send_header("Cache-Control", "no-store")
|
|
367
337
|
if extra_headers:
|
|
368
338
|
for name, value in extra_headers.items():
|
|
369
339
|
self.send_header(name, value)
|
|
@@ -371,8 +341,6 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
371
341
|
self.wfile.write(data)
|
|
372
342
|
|
|
373
343
|
def _send_report_bytes(self, data: bytes) -> None:
|
|
374
|
-
# Reports are static, self-contained documents: no remote origins, no
|
|
375
|
-
# same-origin access; sandbox-friendly CSP + X-Content-Type-Options.
|
|
376
344
|
self._send_bytes(data, "text/html; charset=utf-8", extra_headers={
|
|
377
345
|
"Content-Security-Policy":
|
|
378
346
|
"default-src 'none'; script-src 'unsafe-inline'; "
|
|
@@ -391,27 +359,101 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
391
359
|
else:
|
|
392
360
|
self._send_json({"error": "not found"}, 404)
|
|
393
361
|
|
|
394
|
-
|
|
395
|
-
# -- POST: disabled (read-only Studio) --------------------------------
|
|
396
362
|
def do_POST(self) -> None:
|
|
397
363
|
self._send_json({"error": "method not allowed"}, 405)
|
|
398
364
|
|
|
399
|
-
# -- GET --------------------------------------------------------------
|
|
400
365
|
def do_GET(self) -> None:
|
|
401
366
|
parsed = urllib.parse.urlparse(self.path)
|
|
402
367
|
path = parsed.path
|
|
403
368
|
query = urllib.parse.parse_qs(parsed.query)
|
|
404
|
-
|
|
405
|
-
|
|
369
|
+
try:
|
|
370
|
+
host = urllib.parse.urlsplit("//" + self.headers.get("Host", "")).hostname
|
|
371
|
+
except ValueError:
|
|
372
|
+
self._send_json({"error": "invalid host"}, 403)
|
|
373
|
+
return
|
|
374
|
+
allowed_hosts = {"localhost", "127.0.0.1", "::1", str(self.server.server_address[0])}
|
|
375
|
+
if path.startswith("/api/") and host not in allowed_hosts:
|
|
376
|
+
self._send_json({"error": "untrusted host"}, 403)
|
|
377
|
+
return
|
|
378
|
+
if path.startswith("/api/") and self.headers.get("Sec-Fetch-Site") == "cross-site":
|
|
379
|
+
self._send_json({"error": "cross-origin access denied"}, 403)
|
|
380
|
+
return
|
|
381
|
+
if path == "/api/studio/catalog":
|
|
382
|
+
self._send_json(StudioReader(EXAMPLES_DIR, RESEARCH_HOME).catalog())
|
|
383
|
+
return
|
|
384
|
+
if path == "/api/studio/evolution":
|
|
385
|
+
self._send_json(StudioReader(EXAMPLES_DIR, RESEARCH_HOME).evolution())
|
|
386
|
+
return
|
|
387
|
+
if path.startswith("/api/studio/projects/"):
|
|
388
|
+
reader = StudioReader(EXAMPLES_DIR, RESEARCH_HOME)
|
|
389
|
+
suffix = urllib.parse.unquote(path[len("/api/studio/projects/"):])
|
|
390
|
+
parts = suffix.split("/")
|
|
391
|
+
try:
|
|
392
|
+
if len(parts) == 1:
|
|
393
|
+
self._send_json(reader.detail(parts[0]))
|
|
394
|
+
elif len(parts) == 2 and parts[1] == "report":
|
|
395
|
+
report = reader.report_path(parts[0], theme=query.get("theme", ["claude"])[0],
|
|
396
|
+
filename=query.get("file", [None])[0])
|
|
397
|
+
self._send_report_bytes(report.read_bytes())
|
|
398
|
+
else:
|
|
399
|
+
self._send_json({"error": "not found"}, 404)
|
|
400
|
+
except FileNotFoundError:
|
|
401
|
+
self._send_json({"error": "not found"}, 404)
|
|
402
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
403
|
+
self._send_json({"error": "research projection unavailable"}, 422)
|
|
404
|
+
return
|
|
405
|
+
if path.startswith("/studio/"):
|
|
406
|
+
try:
|
|
407
|
+
asset = (WEB_DIR / unquote_path(path).lstrip("/")).resolve()
|
|
408
|
+
if not asset.is_relative_to((WEB_DIR / "studio").resolve()):
|
|
409
|
+
raise ValueError("unsafe path")
|
|
410
|
+
if asset.is_dir():
|
|
411
|
+
asset = asset / "index.html"
|
|
412
|
+
if asset.suffix not in {".html", ".js", ".css", ".json", ".svg", ".png", ".woff2", ".txt"}:
|
|
413
|
+
raise ValueError("unsupported asset")
|
|
414
|
+
self._serve_file(asset, mimetypes.guess_type(asset)[0] or "application/octet-stream")
|
|
415
|
+
except (ValueError, OSError):
|
|
416
|
+
self._send_json({"error": "not found"}, 404)
|
|
417
|
+
return
|
|
406
418
|
if path in ("/", "/index.html", "/dashboard", "/studio", "/console"):
|
|
407
|
-
|
|
419
|
+
built = WEB_DIR / "studio" / "index.html"
|
|
420
|
+
if built.is_file():
|
|
421
|
+
page = built.read_text(encoding="utf-8").replace("<head>", '<head><base href="/studio/">', 1)
|
|
422
|
+
self._send_bytes(page.encode("utf-8"), "text/html; charset=utf-8")
|
|
423
|
+
else:
|
|
424
|
+
self._serve_file(WEB_DIR / "index.html", "text/html; charset=utf-8")
|
|
408
425
|
return
|
|
409
|
-
|
|
410
426
|
if path == "/api/projects":
|
|
411
427
|
projects = scan_local_projects()
|
|
412
428
|
self._send_json({"projects": projects, "stats": build_stats(projects)})
|
|
413
429
|
return
|
|
414
|
-
|
|
430
|
+
if path == "/api/research/projects":
|
|
431
|
+
self._send_json({"projects": [p for p in StudioReader(EXAMPLES_DIR, RESEARCH_HOME).catalog()["projects"] if p["kind"] == "project"]})
|
|
432
|
+
return
|
|
433
|
+
if path.startswith("/api/research/projects/"):
|
|
434
|
+
suffix = urllib.parse.unquote(path[len("/api/research/projects/"):]).strip("/")
|
|
435
|
+
parts = suffix.split("/")
|
|
436
|
+
project_id = parts[0]
|
|
437
|
+
if not project_id.startswith("PRJ-"):
|
|
438
|
+
self._send_json({"error": "unknown project"}, 404)
|
|
439
|
+
return
|
|
440
|
+
try:
|
|
441
|
+
detail = StudioReader(EXAMPLES_DIR, RESEARCH_HOME).detail("project--" + project_id)
|
|
442
|
+
if len(parts) == 2 and parts[1] == "runs":
|
|
443
|
+
self._send_json({"runs": detail["runs"]})
|
|
444
|
+
return
|
|
445
|
+
if len(parts) == 2 and parts[1] == "artifacts":
|
|
446
|
+
self._send_json({"artifacts": detail["artifacts"]})
|
|
447
|
+
return
|
|
448
|
+
if len(parts) == 2 and parts[1] == "events":
|
|
449
|
+
after = int(query.get("after_seq", ["0"])[0])
|
|
450
|
+
self._send_json({"events": [e for e in reversed(detail["events"]) if e.get("seq", 0) > max(after, 0)]})
|
|
451
|
+
return
|
|
452
|
+
except (FileNotFoundError, ValueError):
|
|
453
|
+
self._send_json({"error": "unknown project"}, 404)
|
|
454
|
+
return
|
|
455
|
+
self._send_json({"error": "not found"}, 404)
|
|
456
|
+
return
|
|
415
457
|
if path == "/api/labels":
|
|
416
458
|
self._send_json({
|
|
417
459
|
"outcomes": OUTCOME_ZH,
|
|
@@ -421,7 +463,6 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
421
463
|
"confidence": CONFIDENCE_ZH,
|
|
422
464
|
})
|
|
423
465
|
return
|
|
424
|
-
|
|
425
466
|
if path.startswith("/api/projects/"):
|
|
426
467
|
suffix = urllib.parse.unquote(path[len("/api/projects/"):]).rstrip("/")
|
|
427
468
|
if suffix.endswith("/viz"):
|
|
@@ -433,21 +474,15 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
433
474
|
return
|
|
434
475
|
self._send_json({"error": "not found"}, 404)
|
|
435
476
|
return
|
|
436
|
-
|
|
437
477
|
if path == "/report":
|
|
438
478
|
self._serve_report(query)
|
|
439
479
|
return
|
|
440
|
-
|
|
441
|
-
# Static assets only from web/ (allowlisted), with URL-decode once and
|
|
442
|
-
# containment; everything else is a JSON 404 (never super().do_GET()).
|
|
443
|
-
if path.startswith("/js/") or path.startswith("/css/") or path == "/styles.css":
|
|
480
|
+
if path.startswith("/js/") or path.startswith("/css/") or path in ("/styles.css", "/mobile-studio.css"):
|
|
444
481
|
self._serve_web_asset(path)
|
|
445
482
|
return
|
|
446
|
-
|
|
447
483
|
self._send_json({"error": "not found"}, 404)
|
|
448
484
|
|
|
449
485
|
def _serve_web_asset(self, path: str) -> None:
|
|
450
|
-
"""Serve a static asset strictly inside WEB_DIR (decode once, no escapes)."""
|
|
451
486
|
try:
|
|
452
487
|
candidate = (WEB_DIR / unquote_path(path).lstrip("/")).resolve()
|
|
453
488
|
except (ValueError, OSError):
|
|
@@ -460,22 +495,20 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
460
495
|
if not candidate.is_file() or candidate.suffix not in (".js", ".css"):
|
|
461
496
|
self._send_json({"error": "not found"}, 404)
|
|
462
497
|
return
|
|
463
|
-
ctype = "application/javascript; charset=utf-8" if candidate.suffix == ".js"
|
|
464
|
-
else "text/css; charset=utf-8"
|
|
498
|
+
ctype = "application/javascript; charset=utf-8" if candidate.suffix == ".js" else "text/css; charset=utf-8"
|
|
465
499
|
self._send_bytes(candidate.read_bytes(), ctype)
|
|
466
500
|
|
|
467
501
|
def _serve_report(self, query: Dict[str, List[str]]) -> None:
|
|
468
|
-
"""Serve a baked report for a known project and explicit theme."""
|
|
469
502
|
proj_id = query.get("id", [""])[0]
|
|
470
503
|
theme = query.get("theme", ["default"])[0]
|
|
471
|
-
|
|
472
504
|
if proj_id not in _known_project_ids():
|
|
473
505
|
self._send_json({"error": "unknown project"}, 404)
|
|
474
506
|
return
|
|
475
|
-
|
|
476
507
|
proj_dir = EXAMPLES_DIR / proj_id
|
|
477
508
|
if theme == "default":
|
|
478
|
-
html_path = proj_dir / "
|
|
509
|
+
html_path = proj_dir / "reports-5themes" / "EduEvidence_Report_claude.html"
|
|
510
|
+
if not html_path.is_file():
|
|
511
|
+
html_path = proj_dir / "EduEvidence_Report.html"
|
|
479
512
|
if not html_path.is_file():
|
|
480
513
|
self._send_json({"error": "report not found"}, 404)
|
|
481
514
|
return
|
|
@@ -485,13 +518,12 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
485
518
|
return
|
|
486
519
|
variants_dir = proj_dir / "reports-5themes"
|
|
487
520
|
html_path = variants_dir / f"EduEvidence_Report_{theme}.html"
|
|
488
|
-
if
|
|
489
|
-
html_path.resolve().parent != variants_dir.resolve()):
|
|
521
|
+
if not html_path.is_file() or html_path.resolve().parent != variants_dir.resolve():
|
|
490
522
|
self._send_json({"error": "unknown report theme"}, 404)
|
|
491
523
|
return
|
|
492
|
-
|
|
493
524
|
self._send_report_bytes(html_path.read_bytes())
|
|
494
525
|
|
|
526
|
+
|
|
495
527
|
def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
496
528
|
server = None
|
|
497
529
|
actual_port = port
|
|
@@ -503,20 +535,17 @@ def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
|
503
535
|
actual_port = candidate
|
|
504
536
|
break
|
|
505
537
|
except OSError as e:
|
|
506
|
-
if e.errno ==
|
|
538
|
+
if e.errno == errno.EADDRINUSE:
|
|
507
539
|
continue
|
|
508
540
|
raise
|
|
509
|
-
|
|
510
541
|
if server is None:
|
|
511
542
|
print(f"❌ 端口 {port}-{port + 9} 均被占用。")
|
|
512
543
|
return
|
|
513
|
-
|
|
514
544
|
print("============================================================")
|
|
515
545
|
print(f"🚀 EduEvidence Web Studio running at http://{host}:{actual_port}/")
|
|
516
|
-
print("
|
|
517
|
-
print("
|
|
518
|
-
print("
|
|
519
|
-
print(" 📦 数据契约 /api/projects · /api/projects/<id>/viz")
|
|
546
|
+
print(" Research Studio /studio/ (read-only)")
|
|
547
|
+
print(" Projects, evidence, revisions and five-theme reports")
|
|
548
|
+
print(" Projection API /api/studio/catalog")
|
|
520
549
|
print("============================================================")
|
|
521
550
|
try:
|
|
522
551
|
server.serve_forever()
|