eduevidence 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +506 -0
  3. package/README.zh-CN.md +434 -0
  4. package/SKILL.md +195 -0
  5. package/bin/eduevidence.js +127 -0
  6. package/domains/education/manifest.json +183 -0
  7. package/domains/education/outcome_taxonomy.json +127 -0
  8. package/domains/manifest.json +26 -0
  9. package/domains/policy/frame.schema.json +234 -0
  10. package/domains/policy/manifest.json +10 -0
  11. package/domains/policy/methodology_checklist.json +109 -0
  12. package/domains/policy/outcome_taxonomy.json +53 -0
  13. package/domains/policy/references/causal-identification.md +45 -0
  14. package/domains/policy/references/cost-evidence.md +44 -0
  15. package/domains/policy/references/equity.md +42 -0
  16. package/domains/policy/references/evidence-hierarchy.md +41 -0
  17. package/domains/policy/references/implementation-evidence.md +47 -0
  18. package/eduevidence_cli.py +26 -0
  19. package/engine/__init__.py +11 -0
  20. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  22. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  23. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  24. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  25. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  26. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  27. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  28. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  29. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  30. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  31. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  32. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  33. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  34. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  35. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  36. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  37. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  38. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  39. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  40. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  41. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  42. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  43. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  44. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  45. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  46. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  47. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  48. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  49. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  50. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  51. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  52. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  53. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  54. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  55. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  56. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  57. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  58. package/engine/analysis.py +308 -0
  59. package/engine/bias.py +178 -0
  60. package/engine/briefs.py +106 -0
  61. package/engine/capabilities.py +99 -0
  62. package/engine/citation_check.py +192 -0
  63. package/engine/contracts.py +117 -0
  64. package/engine/datasets.py +165 -0
  65. package/engine/events.py +67 -0
  66. package/engine/evidence_graph.py +571 -0
  67. package/engine/evidence_review.py +88 -0
  68. package/engine/evidencecore.py +182 -0
  69. package/engine/gap_lens.py +132 -0
  70. package/engine/gaps.py +169 -0
  71. package/engine/graph_store.py +335 -0
  72. package/engine/graph_validate.py +87 -0
  73. package/engine/ids.py +77 -0
  74. package/engine/library.py +268 -0
  75. package/engine/library_builtin.py +301 -0
  76. package/engine/living.py +671 -0
  77. package/engine/log.py +39 -0
  78. package/engine/meta_analysis.py +333 -0
  79. package/engine/meta_synthesis.py +111 -0
  80. package/engine/migration.py +397 -0
  81. package/engine/mode_router.py +72 -0
  82. package/engine/paths.py +15 -0
  83. package/engine/pilot.py +368 -0
  84. package/engine/planner.py +126 -0
  85. package/engine/project.py +118 -0
  86. package/engine/projections.py +240 -0
  87. package/engine/robustness.py +109 -0
  88. package/engine/run.py +85 -0
  89. package/engine/semantics.py +135 -0
  90. package/engine/study_design.py +87 -0
  91. package/engine/synthesis.py +187 -0
  92. package/engine/tribunal.py +408 -0
  93. package/engine/update.py +113 -0
  94. package/engine/versions.py +12 -0
  95. package/install.sh +510 -0
  96. package/integrations/__init__.py +1 -0
  97. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  98. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  99. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  100. package/integrations/agent_mcp.py +856 -0
  101. package/integrations/smart_web_fetch.py +59 -0
  102. package/package.json +50 -0
  103. package/pyproject.toml +55 -0
  104. package/references/applicability-policy.md +88 -0
  105. package/references/education-framing.md +132 -0
  106. package/references/effect_size_formulas.md +35 -0
  107. package/references/evaluation-design.md +111 -0
  108. package/references/evidence-quality.md +79 -0
  109. package/references/grade_framework.md +29 -0
  110. package/references/intervention-design.md +98 -0
  111. package/references/methodology-audit.md +103 -0
  112. package/references/outcome-taxonomy.md +106 -0
  113. package/references/retrieval-protocol.md +142 -0
  114. package/references/skeptic-protocol.md +93 -0
  115. package/references/social_science_pitfalls.md +48 -0
  116. package/references/source-validity.md +140 -0
  117. package/references/tribunal-policy.md +112 -0
  118. package/references/wwc_standards.md +29 -0
  119. package/retrieval/__init__.py +1 -0
  120. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  121. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  122. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  123. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  124. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  125. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  126. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  127. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  128. package/retrieval/corpus_store.py +181 -0
  129. package/retrieval/dedupe.py +127 -0
  130. package/retrieval/failures.py +90 -0
  131. package/retrieval/fetch.py +435 -0
  132. package/retrieval/search.py +493 -0
  133. package/retrieval/source.py +160 -0
  134. package/retrieval/validate.py +257 -0
  135. package/schemas/agent-mcp-approval.schema.json +57 -0
  136. package/schemas/chart-spec.schema.json +88 -0
  137. package/schemas/cross-model-review.schema.json +28 -0
  138. package/schemas/education-frame.schema.json +106 -0
  139. package/schemas/evaluation.schema.json +35 -0
  140. package/schemas/evidence.schema.json +81 -0
  141. package/schemas/fetch-result.schema.json +119 -0
  142. package/schemas/intervention.schema.json +46 -0
  143. package/schemas/methodology.schema.json +102 -0
  144. package/schemas/report-result.schema.json +381 -0
  145. package/schemas/report-spec.schema.json +130 -0
  146. package/schemas/source.schema.json +311 -0
  147. package/schemas/v2/analysis-plan.schema.json +28 -0
  148. package/schemas/v2/analysis-run.schema.json +33 -0
  149. package/schemas/v2/claim.schema.json +26 -0
  150. package/schemas/v2/dataset-asset.schema.json +40 -0
  151. package/schemas/v2/decision-snapshot.schema.json +53 -0
  152. package/schemas/v2/evidence-link.schema.json +38 -0
  153. package/schemas/v2/finding.schema.json +47 -0
  154. package/schemas/v2/graph-revision.schema.json +30 -0
  155. package/schemas/v2/knowledge-gap.schema.json +40 -0
  156. package/schemas/v2/methodology-audit.schema.json +30 -0
  157. package/schemas/v2/outcome.schema.json +18 -0
  158. package/schemas/v2/project.schema.json +31 -0
  159. package/schemas/v2/research-intent.schema.json +24 -0
  160. package/schemas/v2/run.schema.json +43 -0
  161. package/schemas/v2/source.schema.json +24 -0
  162. package/schemas/v2/study-design.schema.json +67 -0
  163. package/schemas/v2/study.schema.json +37 -0
  164. package/schemas/v3/pilot-outcome.schema.json +132 -0
  165. package/schemas/v3/run-manifest.schema.json +193 -0
  166. package/schemas/v3/synthesis.schema.json +86 -0
  167. package/schemas/v4/drift-report.schema.json +66 -0
  168. package/schemas/v4/evidence-library.schema.json +42 -0
  169. package/schemas/v4/living-subscription.schema.json +55 -0
  170. package/schemas/v4/meta-analysis.schema.json +152 -0
  171. package/schemas/verdict.schema.json +56 -0
  172. package/scripts/__init__.py +0 -0
  173. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  175. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  176. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  177. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  178. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  179. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  180. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  181. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  182. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  183. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  184. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  185. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  186. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  187. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  188. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  189. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  190. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  191. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  192. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  193. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  194. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  195. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  196. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  197. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  198. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  199. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  200. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  201. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  202. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  203. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  204. package/scripts/audit_dois.py +292 -0
  205. package/scripts/bake_pack.sh +37 -0
  206. package/scripts/benchmark.py +183 -0
  207. package/scripts/benchmark_evaluator.py +371 -0
  208. package/scripts/benchmark_judge.py +535 -0
  209. package/scripts/benchmark_routing.py +120 -0
  210. package/scripts/benchmark_v2.py +304 -0
  211. package/scripts/benchmark_v3.py +552 -0
  212. package/scripts/build_esl_artifacts.py +1921 -0
  213. package/scripts/build_evidence_library.py +307 -0
  214. package/scripts/build_killer_demo.py +295 -0
  215. package/scripts/build_result.py +311 -0
  216. package/scripts/check_version_consistency.py +96 -0
  217. package/scripts/citation_check.py +123 -0
  218. package/scripts/claim_audit.py +157 -0
  219. package/scripts/complexity_gate.py +180 -0
  220. package/scripts/compute_confidence.py +176 -0
  221. package/scripts/dashboard_server.py +536 -0
  222. package/scripts/did_regression.py +315 -0
  223. package/scripts/effect_calculator.py +99 -0
  224. package/scripts/enrich_projects_human_and_lieflat.py +315 -0
  225. package/scripts/evidence_matrix.py +129 -0
  226. package/scripts/evidence_score.py +234 -0
  227. package/scripts/evidence_semantics.py +87 -0
  228. package/scripts/fetch_benchmark.py +177 -0
  229. package/scripts/generate_metrics.py +99 -0
  230. package/scripts/generate_new_projects.py +686 -0
  231. package/scripts/generate_promo.py +192 -0
  232. package/scripts/lint_report_layout.py +182 -0
  233. package/scripts/orchestrator.py +1456 -0
  234. package/scripts/pre_verdict_gate.py +513 -0
  235. package/scripts/quickstart.py +121 -0
  236. package/scripts/rebake_all_5themes.py +88 -0
  237. package/scripts/recompute_demo_quality.py +205 -0
  238. package/scripts/render_report.py +270 -0
  239. package/scripts/render_report_html.py +356 -0
  240. package/scripts/retraction_watch.py +110 -0
  241. package/scripts/run_workspace.py +337 -0
  242. package/scripts/serve_web.py +54 -0
  243. package/scripts/skill_lint.py +150 -0
  244. package/scripts/startup_probe.py +265 -0
  245. package/scripts/sync_killer_demo_report.py +270 -0
  246. package/scripts/test_adversarial_empirical.py +541 -0
  247. package/scripts/validate_schema.py +256 -0
  248. package/skill/agents/education-planner.md +80 -0
  249. package/skill/agents/evaluation-designer.md +74 -0
  250. package/skill/agents/evidence-analyst.md +106 -0
  251. package/skill/agents/evidence-judge.md +111 -0
  252. package/skill/agents/evidence-retriever.md +80 -0
  253. package/skill/agents/intervention-designer.md +82 -0
  254. package/skill/agents/method-reviewer.md +104 -0
  255. package/skill/agents/skeptic.md +89 -0
  256. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
  257. package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
  258. package/skill/sub-skills/data-analysis/SKILL.md +23 -0
  259. package/skill/sub-skills/ethics-review/SKILL.md +25 -0
  260. package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
  261. package/skill/sub-skills/evidence-review/SKILL.md +18 -0
  262. package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
  263. package/skill/sub-skills/literature-review/SKILL.md +21 -0
  264. package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
  265. package/skill/sub-skills/report-generation/SKILL.md +51 -0
  266. package/skill/sub-skills/research-planning/SKILL.md +21 -0
  267. package/skill/sub-skills/study-design/SKILL.md +16 -0
  268. package/skill/task-briefs/adjudicate.md +17 -0
  269. package/skill/task-briefs/audit.md +15 -0
  270. package/skill/task-briefs/challenge.md +15 -0
  271. package/skill/task-briefs/evaluate.md +13 -0
  272. package/skill/task-briefs/extract.md +16 -0
  273. package/skill/task-briefs/frame.md +17 -0
  274. package/skill/task-briefs/intervene.md +14 -0
  275. package/skill/task-briefs/present.md +16 -0
  276. package/skill/task-briefs/retrieve.md +15 -0
  277. package/visualization/eduevidence-report/assets/base.css +337 -0
  278. package/visualization/eduevidence-report/motion/motion.css +157 -0
  279. package/visualization/eduevidence-report/motion/motion.js +107 -0
  280. package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
  281. package/visualization/eduevidence-report/references/component-catalog.md +145 -0
  282. package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
  283. package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
  284. package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
  285. package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
  286. package/visualization/eduevidence-report/references/motion-system.md +31 -0
  287. package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
  288. package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
  289. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  290. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  291. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  292. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  293. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  294. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  295. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  296. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  297. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
  298. package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
  299. package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
  300. package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
  301. package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
  302. package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
  303. package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
  304. package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
  305. package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
  306. package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
  307. package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
  308. package/visualization/eduevidence-report/themes/academic.css +94 -0
  309. package/visualization/eduevidence-report/themes/claude.css +96 -0
  310. package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
  311. package/visualization/eduevidence-report/themes/datalab.css +151 -0
  312. package/visualization/eduevidence-report/themes/presentation.css +140 -0
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env python3
2
+ """pre_verdict_gate.py — Pre-Verdict Gate (Phase 15).
3
+
4
+ An 11-item checklist that must pass (or be explicitly degraded) before a
5
+ verdict may carry a high confidence label. The gate is deterministic and
6
+ reads ONLY the run workspace — no model call, no network.
7
+
8
+ Checklist:
9
+
10
+ 1. research_frame_valid frame.json validates against education-frame.schema.json
11
+ 2. sources_valid sources.jsonl non-empty and schema-valid
12
+ 3. evidence_schema_valid evidence.jsonl non-empty and schema-valid
13
+ 4. source_dedupe no duplicate sources remain (dedupe applied)
14
+ 5. counter_evidence_search skeptic.json present with search_performed=true
15
+ 6. methodology_audit methodology.json schema-valid, verdict != FAIL,
16
+ task_vs_learning_guard not equating task with learning
17
+ 7. claim_evidence_audit every evidence id referenced by verdict claims exists;
18
+ no claim is bound to evidence that contradicts its category
19
+ 8. outcome_mapping verdict outcome keys are known taxonomy tokens;
20
+ frame-declared outcomes covered by evidence
21
+ 9. scope_calibration verdict carries what_can/cannot_be_claimed +
22
+ exceeds_evidence_boundary
23
+ 10. independent_study_count independent studies/samples counted; >= 2 for High
24
+ 11. deterministic_confidence final verdict carries confidence_score +
25
+ confidence_policy_version + raw model value preserved
26
+
27
+ Failure model (per item): status pass | warn | fail. An item may be
28
+ ``critical`` (its failure forbids any High confidence) and/or ``blocks_high``
29
+ (any non-pass state forbids High confidence; e.g. a single independent study).
30
+ Overall result:
31
+
32
+ passed no critical item failed
33
+ high_confidence_allowed passed AND no blocks_high item is non-pass
34
+ max_confidence High / Moderate / Low (never High when blocked)
35
+
36
+ Usage:
37
+ python scripts/pre_verdict_gate.py --workspace runs/<run_id>
38
+ python scripts/pre_verdict_gate.py --workspace runs/<run_id> \\
39
+ --apply-verdict final_verdict.json --out gate_report.json
40
+ python scripts/pre_verdict_gate.py --workspace runs/<run_id> --json
41
+
42
+ Exit code 0 = gate passed; 1 = critical failures (verdict must be capped);
43
+ 2 = usage error.
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import json
49
+ import re
50
+ import sys
51
+ from pathlib import Path
52
+ from typing import Any
53
+
54
+ ROOT = Path(__file__).resolve().parent.parent
55
+ for _p in (str(ROOT), str(ROOT / "scripts")):
56
+ if _p not in sys.path:
57
+ sys.path.insert(0, _p)
58
+
59
+ from validate_schema import SchemaError, Validator # noqa: E402
60
+ from evidence_score import independent_samples, independent_studies # noqa: E402
61
+ from evidence_semantics import claim_relation # noqa: E402
62
+ from run_workspace import utc_now # noqa: E402
63
+
64
+ GATE_VERSION = "2026-08-13.v1"
65
+
66
+ CONFIDENCE_RANK = {"Insufficient": 0, "Low": 1, "Moderate": 2, "High": 3}
67
+
68
+ #: Advisory taxonomy for verdict outcome keys (shared with claim_audit).
69
+ SUPPORTED_OUTCOMES = {
70
+ "knowledge_gain", "concept_understanding", "retention", "transfer",
71
+ "independent_problem_solving", "completion_time", "accuracy",
72
+ "code_quality", "assignment_score", "engagement", "motivation",
73
+ "cognitive_load", "help_seeking", "metacognition", "ai_dependency",
74
+ "over_reliance", "reduced_effort", "reduced_transfer",
75
+ "academic_integrity_risk", "false_confidence",
76
+ }
77
+
78
+ _CLAIM_ID_RE = re.compile(r"\b[A-Z]{1,3}-\d{2,4}\b")
79
+
80
+ _SCHEMA_CACHE: dict[str, dict[str, Any]] = {}
81
+
82
+
83
+ def _schema(name: str) -> dict[str, Any]:
84
+ if name not in _SCHEMA_CACHE:
85
+ _SCHEMA_CACHE[name] = json.loads((ROOT / "schemas" / name).read_text(encoding="utf-8"))
86
+ return _SCHEMA_CACHE[name]
87
+
88
+
89
+ def _validate_records(records: list[dict[str, Any]], schema_name: str, path: str) -> list[str]:
90
+ schema = _schema(schema_name)
91
+ validator = Validator(schema, base_dir=(ROOT / "schemas"))
92
+ errors = []
93
+ for idx, record in enumerate(records):
94
+ try:
95
+ validator.validate(record, schema, f"{path}[{idx}]")
96
+ except SchemaError as exc:
97
+ errors.append(str(exc))
98
+ return errors
99
+
100
+
101
+ def _load_ws_json(workspace: Path, name: str) -> dict[str, Any]:
102
+ try:
103
+ data = json.loads((workspace / name).read_text(encoding="utf-8"))
104
+ except (OSError, json.JSONDecodeError):
105
+ return {}
106
+ return data if isinstance(data, dict) else {}
107
+
108
+
109
+ def _load_ws_jsonl(workspace: Path, name: str) -> list[dict[str, Any]]:
110
+ path = workspace / name
111
+ if not path.is_file():
112
+ return []
113
+ records = []
114
+ for line in path.read_text(encoding="utf-8").splitlines():
115
+ line = line.strip()
116
+ if line:
117
+ try:
118
+ data = json.loads(line)
119
+ except json.JSONDecodeError:
120
+ continue
121
+ if isinstance(data, dict):
122
+ records.append(data)
123
+ return records
124
+
125
+
126
+ # ------------------------------------------------------------- item checks
127
+
128
+
129
+ def _item_res(status: str, detail: str, *, blocks_high: bool | None = None) -> dict[str, str]:
130
+ res = {"status": status, "detail": detail}
131
+ if blocks_high is not None:
132
+ res["blocks_high"] = blocks_high
133
+ return res
134
+
135
+
136
+ def check_research_frame(ws: Path) -> dict[str, str]:
137
+ frame = _load_ws_json(ws, "frame.json")
138
+ if not frame:
139
+ return _item_res("fail", "frame.json missing or empty (research question not framed)")
140
+ errors = _validate_records([frame], "education-frame.schema.json", "frame")
141
+ if errors:
142
+ return _item_res("fail", f"frame.json schema invalid: {errors[0]}")
143
+ return _item_res("pass", f"frame.json valid (question={frame.get('question', '')[:80]})")
144
+
145
+
146
+ def check_sources(ws: Path) -> dict[str, str]:
147
+ sources = _load_ws_jsonl(ws, "sources.jsonl")
148
+ if not sources:
149
+ return _item_res("fail", "sources.jsonl missing or empty (no sources retrieved)")
150
+ errors = _validate_records(sources, "source.schema.json", "sources")
151
+ if errors:
152
+ return _item_res("fail", f"{len(errors)} invalid source record(s): {errors[0]}")
153
+ return _item_res("pass", f"{len(sources)} source record(s) schema-valid")
154
+
155
+
156
+ def check_evidence_schema(ws: Path) -> dict[str, str]:
157
+ evidence = _load_ws_jsonl(ws, "evidence.jsonl")
158
+ if not evidence:
159
+ return _item_res("fail", "evidence.jsonl missing or empty (no evidence extracted)")
160
+ errors = _validate_records(evidence, "evidence.schema.json", "evidence")
161
+ if errors:
162
+ return _item_res("fail", f"{len(errors)} invalid evidence record(s): {errors[0]}")
163
+ return _item_res("pass", f"{len(evidence)} evidence record(s) schema-valid")
164
+
165
+
166
+ def check_source_dedupe(ws: Path) -> dict[str, str]:
167
+ from retrieval.dedupe import dedupe_sources # repo package, bootstrapped above
168
+
169
+ sources = _load_ws_jsonl(ws, "sources.jsonl")
170
+ if not sources:
171
+ return _item_res("fail", "sources.jsonl empty (dedupe cannot run)")
172
+ unique = dedupe_sources(sources)
173
+ duplicates = len(sources) - len(unique)
174
+ if duplicates:
175
+ return _item_res("fail", f"{duplicates} duplicate source(s) remain; dedupe not applied")
176
+ return _item_res("pass", f"{len(unique)} unique source(s), no duplicates")
177
+
178
+
179
+ def check_counter_evidence(ws: Path) -> dict[str, str]:
180
+ skeptic = _load_ws_json(ws, "skeptic.json")
181
+ if not skeptic:
182
+ return _item_res("fail", "skeptic.json missing or empty (counter-evidence search not performed)")
183
+ if skeptic.get("search_performed") is not True:
184
+ return _item_res("fail", "skeptic.json lacks search_performed=true")
185
+ contradictions = skeptic.get("contradictions", []) or []
186
+ null_results = skeptic.get("null_results", []) or []
187
+ detail = f"search_performed=true; contradictions={len(contradictions)}, null_results={len(null_results)}"
188
+ if skeptic.get("no_contradictory_evidence_found"):
189
+ detail += "; no contradictory evidence found"
190
+ return _item_res("pass", detail)
191
+
192
+
193
+ def check_methodology(ws: Path) -> dict[str, str]:
194
+ methodology = _load_ws_json(ws, "methodology.json")
195
+ if not methodology:
196
+ return _item_res("fail", "methodology.json missing or empty (no method audit)")
197
+ errors = _validate_records([methodology], "methodology.schema.json", "methodology")
198
+ if errors:
199
+ return _item_res("fail", f"methodology.json schema invalid: {errors[0]}")
200
+ verdict = methodology.get("verdict", "")
201
+ guard = methodology.get("task_vs_learning_guard", {}) or {}
202
+ if verdict == "FAIL":
203
+ return _item_res("fail", "methodology verdict is FAIL (evidence base does not stand)")
204
+ if guard.get("equates_task_with_learning") is True:
205
+ return _item_res("fail", "task_vs_learning_guard equates task performance with learning")
206
+ return _item_res("pass", f"methodology verdict={verdict}; task/learning separated")
207
+
208
+
209
+ def _verdict_for_audit(ws: Path) -> dict[str, Any]:
210
+ final = _load_ws_json(ws, "final_verdict.json")
211
+ if final:
212
+ return final
213
+ return _load_ws_json(ws, "raw_verdict.json")
214
+
215
+
216
+ #: Explicit markers a verdict claim may carry to declare "no direct evidence
217
+ #: exists" instead of silently omitting a binding (OPEN-1). A hedged claim
218
+ #: that carries no evidence id AND no such marker blocks High confidence.
219
+ NO_DIRECT_EVIDENCE_MARKERS = ("[无直接证据]", "no direct evidence", "NO_DIRECT_EVIDENCE")
220
+
221
+
222
+ def _claim_has_no_evidence_marker(claim_text: str) -> bool:
223
+ return any(marker in claim_text for marker in NO_DIRECT_EVIDENCE_MARKERS)
224
+
225
+
226
+ def check_claim_evidence(ws: Path) -> dict[str, str]:
227
+ verdict = _verdict_for_audit(ws)
228
+ if not verdict:
229
+ return _item_res("fail", "no verdict artifact (raw_verdict.json or final_verdict.json)")
230
+ evidence = {e.get("evidence_id"): e for e in _load_ws_jsonl(ws, "evidence.jsonl")}
231
+ if not evidence:
232
+ return _item_res("fail", "evidence.jsonl empty (claim-evidence binding cannot be audited)")
233
+
234
+ issues: list[str] = []
235
+ warnings: list[str] = []
236
+ for category in ("supported_claims", "uncertain_claims", "contradicted_claims"):
237
+ for claim_text in verdict.get(category, []) or []:
238
+ if not isinstance(claim_text, str) or not claim_text.strip():
239
+ continue
240
+ ids = _CLAIM_ID_RE.findall(claim_text)
241
+ if not ids:
242
+ if _claim_has_no_evidence_marker(claim_text):
243
+ continue # explicitly declared "no direct evidence": acceptable
244
+ warnings.append(f"{category}: claim carries no evidence id: {claim_text[:60]}")
245
+ continue
246
+ for eid in ids:
247
+ ev = evidence.get(eid)
248
+ if ev is None:
249
+ issues.append(f"{category}: evidence {eid} referenced but not found in corpus")
250
+ continue
251
+ relation = claim_relation(ev)
252
+ if category == "supported_claims" and relation == "contradict":
253
+ warnings.append(f"supported claim cites contradicting evidence {eid} "
254
+ "(may be an intentional negative finding)")
255
+ if category == "contradicted_claims" and relation == "support":
256
+ warnings.append(f"contradicted claim cites supporting evidence {eid} "
257
+ "(may be an intentional positive finding)")
258
+
259
+ if issues:
260
+ return _item_res("fail", "; ".join(issues[:3]) + (f" (+{len(issues)-3} more)" if len(issues) > 3 else ""))
261
+ if warnings:
262
+ # ANY claim category without an evidence id (and without an explicit
263
+ # "no direct evidence" marker) is unverifiable and blocks High
264
+ # confidence — uncertain claims must bind evidence or declare the gap
265
+ # (OPEN-1); hedged/contradicted claims without ids were previously
266
+ # only noted but could still mislead the confidence label.
267
+ unverifiable = [w for w in warnings if "carries no evidence id" in w]
268
+ note = "; ".join(warnings[:2]) + (f" (+{len(warnings)-2} more)" if len(warnings) > 2 else "")
269
+ return _item_res("warn", note, blocks_high=bool(unverifiable))
270
+ return _item_res("pass", "all verdict claims bind to existing evidence with consistent categories")
271
+
272
+
273
+ def check_outcome_mapping(ws: Path) -> dict[str, str]:
274
+ verdict = _verdict_for_audit(ws)
275
+ frame = _load_ws_json(ws, "frame.json")
276
+ issues: list[str] = []
277
+ notes: list[str] = []
278
+
279
+ findings = verdict.get("outcome_specific_findings", {}) or {}
280
+ unknown = [k for k in findings if k not in SUPPORTED_OUTCOMES]
281
+ if unknown:
282
+ issues.append(f"unknown outcome key(s) in verdict: {', '.join(sorted(unknown))}")
283
+
284
+ evidence_outcomes = {e.get("outcome_type") for e in _load_ws_jsonl(ws, "evidence.jsonl")}
285
+ declared = set()
286
+ for group in ("primary", "secondary", "risk"):
287
+ declared.update((frame.get("outcomes", {}) or {}).get(group, []) or [])
288
+ if declared:
289
+ missing = sorted(d for d in declared if d and d not in evidence_outcomes)
290
+ if missing:
291
+ notes.append(f"frame-declared outcomes without evidence: {', '.join(missing)}")
292
+
293
+ if issues:
294
+ return _item_res("fail", "; ".join(issues))
295
+ if notes:
296
+ return _item_res("warn", "outcome keys known; " + notes[0])
297
+ if not declared:
298
+ return _item_res("warn", "outcome keys known; frame declares no outcomes to map")
299
+ return _item_res("pass", f"outcome mapping complete ({len(evidence_outcomes)} outcome type(s) covered)")
300
+
301
+
302
+ def check_scope_calibration(ws: Path) -> dict[str, str]:
303
+ verdict = _verdict_for_audit(ws)
304
+ if not verdict:
305
+ return _item_res("fail", "no verdict artifact to calibrate")
306
+ missing = []
307
+ for field in ("what_can_be_claimed", "what_cannot_be_claimed", "exceeds_evidence_boundary"):
308
+ if field not in verdict or not isinstance(verdict.get(field), list):
309
+ missing.append(field)
310
+ if missing:
311
+ return _item_res("fail", f"scope calibration incomplete: missing {', '.join(missing)}")
312
+ boundary = len(verdict["exceeds_evidence_boundary"])
313
+ detail = (f"claims bounded: can={len(verdict['what_can_be_claimed'])}, "
314
+ f"cannot={len(verdict['what_cannot_be_claimed'])}, exceeds_boundary={boundary}")
315
+ return _item_res("pass", detail)
316
+
317
+
318
+ def check_study_count(ws: Path) -> dict[str, str]:
319
+ evidence = _load_ws_jsonl(ws, "evidence.jsonl")
320
+ if not evidence:
321
+ return _item_res("fail", "no evidence to count independent studies/samples")
322
+ studies = independent_studies(evidence)
323
+ samples = independent_samples(evidence)
324
+ detail = f"independent studies={studies}, samples={samples}"
325
+ if studies == 0:
326
+ return _item_res("fail", detail + "; zero independent studies")
327
+ if studies < 2:
328
+ return _item_res("warn", detail + "; single study cannot support High confidence")
329
+ return _item_res("pass", detail)
330
+
331
+
332
+ def check_deterministic_confidence(ws: Path, *, require_final: bool) -> dict[str, str]:
333
+ final = _load_ws_json(ws, "final_verdict.json")
334
+ if not final:
335
+ if require_final:
336
+ return _item_res("fail", "final_verdict.json missing (deterministic confidence not applied)")
337
+ return _item_res("warn", "final_verdict.json pending (run adjudicate to apply deterministic confidence)")
338
+
339
+ errors = _validate_records([final], "verdict.schema.json", "final_verdict")
340
+ if errors:
341
+ return _item_res("fail", f"final_verdict.json schema invalid: {errors[0]}")
342
+ problems = []
343
+ if not isinstance(final.get("confidence_score"), (int, float)) or isinstance(final.get("confidence_score"), bool):
344
+ problems.append("confidence_score missing/non-numeric")
345
+ if not final.get("confidence_policy_version"):
346
+ problems.append("confidence_policy_version missing")
347
+ if final.get("confidence") not in ("High", "Moderate", "Low", "Insufficient"):
348
+ problems.append(f"confidence {final.get('confidence')!r} not in allowed bands")
349
+ if problems:
350
+ return _item_res("fail", "final_verdict.json " + "; ".join(problems))
351
+ note = ""
352
+ if "raw_model_confidence" not in final:
353
+ note = "; raw model confidence not preserved"
354
+ return _item_res("pass" if not note else "warn",
355
+ f"deterministic confidence={final['confidence']} "
356
+ f"(score={final.get('confidence_score')}, "
357
+ f"policy={final.get('confidence_policy_version')}){note}")
358
+
359
+
360
+ # ---------------------------------------------------------------- gate spec
361
+
362
+ GATE_ITEMS: list[dict[str, Any]] = [
363
+ {"id": "research_frame_valid", "title": "Research Frame valid", "critical": True,
364
+ "blocks_high": True, "check": check_research_frame},
365
+ {"id": "sources_valid", "title": "Sources valid", "critical": True,
366
+ "blocks_high": True, "check": check_sources},
367
+ {"id": "evidence_schema_valid", "title": "Evidence Schema valid", "critical": True,
368
+ "blocks_high": True, "check": check_evidence_schema},
369
+ {"id": "source_dedupe", "title": "Source dedupe", "critical": True,
370
+ "blocks_high": True, "check": check_source_dedupe},
371
+ {"id": "counter_evidence_search", "title": "Counter-evidence search", "critical": True,
372
+ "blocks_high": True, "check": check_counter_evidence},
373
+ {"id": "methodology_audit", "title": "Methodology audit", "critical": True,
374
+ "blocks_high": True, "check": check_methodology},
375
+ {"id": "claim_evidence_audit", "title": "Claim-Evidence Audit", "critical": True,
376
+ "blocks_high": False, "check": check_claim_evidence},
377
+ {"id": "outcome_mapping", "title": "Outcome mapping", "critical": False,
378
+ "blocks_high": False, "check": check_outcome_mapping},
379
+ {"id": "scope_calibration", "title": "Scope calibration", "critical": False,
380
+ "blocks_high": False, "check": check_scope_calibration},
381
+ {"id": "independent_study_count", "title": "Independent study-sample count", "critical": True,
382
+ "blocks_high": True, "check": check_study_count},
383
+ {"id": "deterministic_confidence", "title": "Deterministic confidence", "critical": True,
384
+ "blocks_high": True, "check": None},
385
+ ]
386
+
387
+
388
+ def evaluate_workspace(workspace: Path, *, require_final: bool = True) -> dict[str, Any]:
389
+ """Run the 11-item gate over a run workspace. Pure, deterministic, read-only."""
390
+ ws = Path(workspace)
391
+ items: dict[str, dict[str, Any]] = {}
392
+ for spec in GATE_ITEMS:
393
+ if spec["id"] == "deterministic_confidence":
394
+ result = check_deterministic_confidence(ws, require_final=require_final)
395
+ else:
396
+ result = spec["check"](ws)
397
+ items[spec["id"]] = {
398
+ "title": spec["title"],
399
+ "status": result["status"],
400
+ "detail": result["detail"],
401
+ "critical": spec["critical"],
402
+ "blocks_high": result.get("blocks_high", spec["blocks_high"]),
403
+ }
404
+
405
+ critical_failures = [iid for iid, it in items.items() if it["critical"] and it["status"] == "fail"]
406
+ passed = not critical_failures
407
+ high_blockers = [iid for iid, it in items.items() if it["blocks_high"] and it["status"] != "pass"]
408
+ high_confidence_allowed = passed and not high_blockers
409
+ if high_confidence_allowed:
410
+ max_confidence = "High"
411
+ elif passed:
412
+ max_confidence = "Moderate"
413
+ else:
414
+ max_confidence = "Low"
415
+
416
+ return {
417
+ "gate_version": GATE_VERSION,
418
+ "checked_at": utc_now(),
419
+ "workspace": str(ws),
420
+ "items": items,
421
+ "passed": passed,
422
+ "critical_failures": critical_failures,
423
+ "high_confidence_allowed": high_confidence_allowed,
424
+ "max_confidence": max_confidence,
425
+ "enforcement": {
426
+ "rule": ("confidence capped at {max}; High confidence requires a fully passing gate "
427
+ "and >= 2 independent studies"),
428
+ "max_confidence": max_confidence,
429
+ "requires_action_change": not passed,
430
+ "action_override": "pilot" if not passed else None,
431
+ },
432
+ }
433
+
434
+
435
+ def apply_enforcement(verdict: dict[str, Any], gate: dict[str, Any]) -> dict[str, Any]:
436
+ """Cap a verdict's confidence per the gate result (in-place on a copy).
437
+
438
+ - gate failed -> confidence at most Low; adopt downgraded to pilot
439
+ - gate passed but High blocked -> confidence at most Moderate
440
+ Always records the enforcement inside verdict.extensions.gate_enforcement.
441
+ """
442
+ import copy
443
+
444
+ out = copy.deepcopy(verdict)
445
+ cap = gate.get("max_confidence", "Low")
446
+ current = out.get("confidence", "Insufficient")
447
+ if CONFIDENCE_RANK.get(current, 0) > CONFIDENCE_RANK.get(cap, 0):
448
+ out["confidence"] = cap
449
+ if not gate.get("passed", False) and out.get("recommended_action") == "adopt":
450
+ out["recommended_action"] = "pilot"
451
+ extensions = out.setdefault("extensions", {})
452
+ if not isinstance(extensions, dict):
453
+ extensions = {}
454
+ out["extensions"] = extensions
455
+ enforcement = extensions.setdefault("gate_enforcement", {})
456
+ enforcement.update({
457
+ "gate_version": gate.get("gate_version"),
458
+ "checked_at": gate.get("checked_at"),
459
+ "passed": gate.get("passed"),
460
+ "critical_failures": gate.get("critical_failures"),
461
+ "max_confidence": cap,
462
+ "confidence_before": current,
463
+ "action_before": verdict.get("recommended_action"),
464
+ })
465
+ return out
466
+
467
+
468
+ # ---------------------------------------------------------------------- CLI
469
+
470
+
471
+ def main(argv: list[str] | None = None) -> int:
472
+ parser = argparse.ArgumentParser(description="EduEvidence Pre-Verdict Gate (11-item checklist)")
473
+ parser.add_argument("--workspace", required=True, help="run workspace directory (runs/<run_id>)")
474
+ parser.add_argument("--require-final", action="store_true",
475
+ help="item 11 fails when final_verdict.json is missing (default: warn)")
476
+ parser.add_argument("--apply-verdict", metavar="PATH",
477
+ help="cap the given verdict JSON per the gate result and write it back")
478
+ parser.add_argument("--out", metavar="PATH", help="write the gate report JSON to PATH")
479
+ parser.add_argument("--json", action="store_true", help="print the report as JSON")
480
+ args = parser.parse_args(argv)
481
+
482
+ report = evaluate_workspace(Path(args.workspace), require_final=args.require_final)
483
+
484
+ if args.apply_verdict:
485
+ verdict_path = Path(args.apply_verdict)
486
+ verdict = _load_ws_json(verdict_path.parent, verdict_path.name)
487
+ if not verdict:
488
+ print(f"ERROR: verdict file {verdict_path} missing or empty", file=sys.stderr)
489
+ return 2
490
+ capped = apply_enforcement(verdict, report)
491
+ verdict_path.write_text(json.dumps(capped, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
492
+ report["verdict_applied"] = str(verdict_path)
493
+ report["verdict_confidence"] = capped.get("confidence")
494
+ report["verdict_action"] = capped.get("recommended_action")
495
+
496
+ if args.out:
497
+ Path(args.out).write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
498
+
499
+ if args.json:
500
+ print(json.dumps(report, ensure_ascii=False, indent=2))
501
+ else:
502
+ for iid, item in report["items"].items():
503
+ print(f"[{item['status'].upper():4}] {iid}: {item['detail']}")
504
+ print(f"passed={report['passed']} high_confidence_allowed="
505
+ f"{report['high_confidence_allowed']} max_confidence={report['max_confidence']}")
506
+ if report["critical_failures"]:
507
+ print(f"critical failures: {', '.join(report['critical_failures'])}", file=sys.stderr)
508
+
509
+ return 0 if report["passed"] else 1
510
+
511
+
512
+ if __name__ == "__main__":
513
+ sys.exit(main())
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ """quickstart — 30 分钟上手引导器(plan F1)。
3
+
4
+ 把"跑自己的研究问题"压缩成一条命令:
5
+
6
+ python3 scripts/quickstart.py "大一 C 课程该不该允许学生用 AI 编程助手?"
7
+
8
+ 流程:
9
+ 1. 进程内调用 orchestrator.main(["run", ...]) 创建 run workspace
10
+ (确定性阶段本地执行,LLM 阶段产出任务简报;无子进程、无 shell);
11
+ 2. 读取 state.json,找出待完成的外部 agent 阶段;
12
+ 3. 在 run 目录生成 NEXT_STEPS.md —— 每个待办阶段的精确命令、对应
13
+ sub-skill 简报位置、完成后的一键裁决/渲染/引用核验命令。
14
+
15
+ Stdlib only。
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ ROOT = Path(__file__).resolve().parent.parent
27
+ for _p in (str(ROOT), str(ROOT / "scripts")):
28
+ if _p not in sys.path:
29
+ sys.path.insert(0, _p)
30
+
31
+ import orchestrator as orch # noqa: E402
32
+
33
+ # 外部 agent 阶段 → sub-skill 简报(SKILL.md Canonical Protocol 对应)
34
+ STAGE_BRIEFS = {
35
+ "frame": ("skill/sub-skills/research-planning", "education-frame.schema.json"),
36
+ "retrieve": ("skill/sub-skills/literature-review", "source.schema.json"),
37
+ "extract": ("skill/sub-skills/evidence-extraction", "evidence.schema.json"),
38
+ "challenge": ("skill/sub-skills/contradiction-analysis", "evidence.schema.json"),
39
+ "audit": ("skill/sub-skills/methodology-audit", "methodology.schema.json"),
40
+ }
41
+
42
+
43
+ def newest_run_dir() -> Path:
44
+ runs_dir = ROOT / "runs"
45
+ candidates = sorted((p for p in runs_dir.iterdir() if p.is_dir()),
46
+ key=lambda p: p.stat().st_mtime, reverse=True)
47
+ if not candidates:
48
+ raise SystemExit("no run workspace found after orchestrator run")
49
+ return candidates[0]
50
+
51
+
52
+ def build_next_steps(run_dir: Path, question: str, depth: str) -> str:
53
+ state_path = run_dir / "state.json"
54
+ pending: list[tuple[str, str]] = []
55
+ if state_path.exists():
56
+ state = json.loads(state_path.read_text(encoding="utf-8"))
57
+ for stage, info in (state.get("stages") or {}).items():
58
+ status = info.get("status") if isinstance(info, dict) else info
59
+ if status != "completed":
60
+ brief, schema = STAGE_BRIEFS.get(stage, ("skill/sub-skills/", "schemas/"))
61
+ pending.append((stage, brief + "(产物须过 " + schema + ")"))
62
+
63
+ lines = [
64
+ "# NEXT STEPS — 把这个 run 跑完",
65
+ "",
66
+ "- 研究问题:" + question,
67
+ "- 复杂度:" + depth + " - run 目录:`" + str(run_dir) + "`",
68
+ "- 原则:**LLM 阶段由你的 AI Agent 按 brief 执行并落盘 schema 合法工件;"
69
+ "确定性阶段(audit/adjudicate/present)本地一条命令完成。**",
70
+ ""]
71
+ if pending:
72
+ lines += ["## 待完成的外部 agent 阶段", ""]
73
+ for stage, brief in pending:
74
+ lines += ["### " + stage,
75
+ "- 简报:`" + brief.split("(")[0] + "`",
76
+ "- 完成后重跑:`eduevidence resume --run-id " + run_dir.name + "`",
77
+ ""]
78
+ else:
79
+ lines += ["## 全部阶段已完成 ✓", ""]
80
+ lines += [
81
+ "## 收尾(证据→决策→交付)",
82
+ "",
83
+ "- 确定性裁决:`eduevidence adjudicate --project <run_dir>`",
84
+ "- 五主题报告烘焙:`bash scripts/bake_pack.sh <pack_dir>`",
85
+ "- 引用核验:`python3 scripts/citation_check.py --pack <pack_dir> --write-back`",
86
+ "",
87
+ "## 可信度自检",
88
+ "",
89
+ "- 引用逐条核验报告:`benchmarks/doi-audit/report.md` 与包内 `citation_check.md`",
90
+ "- 报告头徽章标注 data_origin;synthetic 演示不得当作实证引用",
91
+ ""]
92
+ return "\n".join(lines)
93
+
94
+
95
+ def main() -> int:
96
+ parser = argparse.ArgumentParser(description=__doc__)
97
+ parser.add_argument("question", help="你的教育研究问题")
98
+ parser.add_argument("--depth", default="standard",
99
+ choices=["quick", "standard", "deep", "S", "M", "L"])
100
+ parser.add_argument("--dry-run", action="store_true", help="只初始化,不推进阶段")
101
+ args = parser.parse_args()
102
+
103
+ t0 = time.time()
104
+ argv = ["run", "--question", args.question, "--depth", args.depth]
105
+ if args.dry_run:
106
+ argv.append("--dry-run")
107
+ rc = orch.main(argv)
108
+ if rc not in (0, None):
109
+ raise SystemExit("orchestrator run rc=" + str(rc))
110
+ run_dir = newest_run_dir()
111
+ (run_dir / "NEXT_STEPS.md").write_text(
112
+ build_next_steps(run_dir, args.question, args.depth), encoding="utf-8")
113
+
114
+ print("\n✅ run 已创建:" + str(run_dir) + f"({time.time()-t0:.1f}s)")
115
+ print("📋 下一步清单:" + str(run_dir / "NEXT_STEPS.md"))
116
+ print(" 把其中的外部 agent 阶段交给你的 AI Agent;确定性阶段按清单本地执行。")
117
+ return 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ sys.exit(main())