eduevidence 6.0.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.
Files changed (189) hide show
  1. package/CONTRIBUTING.md +105 -0
  2. package/README.md +93 -38
  3. package/README.zh-CN.md +26 -6
  4. package/SKILL.md +11 -2
  5. package/assets/readme/landing-tour.gif +0 -0
  6. package/assets/readme/studio-tour.gif +0 -0
  7. package/bin/eduevidence.js +2 -1
  8. package/docs/architecture.md +319 -43
  9. package/docs/demo-workplace-ai.md +1 -1
  10. package/docs/install-guide.md +1 -1
  11. package/docs/orchestration-role-model.md +1 -1
  12. package/docs/release-closeout/README.md +1 -1
  13. package/docs/sciverse-api.md +125 -0
  14. package/eduevidence_cli.py +10 -0
  15. package/engine/decision_policy.py +96 -0
  16. package/engine/evidence_graph.py +14 -10
  17. package/engine/gaps.py +42 -22
  18. package/engine/ids.py +2 -0
  19. package/engine/library.py +6 -2
  20. package/engine/living.py +34 -4
  21. package/engine/migration.py +88 -3
  22. package/engine/orchestration.py +5 -5
  23. package/engine/paths.py +2 -0
  24. package/engine/pilot.py +34 -32
  25. package/engine/taxonomy.py +211 -0
  26. package/engine/tribunal.py +43 -31
  27. package/engine/versions.py +1 -1
  28. package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +1360 -146
  29. package/examples/ai-coding-assistant-evidence/artifact_manifest.json +3 -3
  30. package/examples/ai-coding-assistant-evidence/citation_check.json +1 -1
  31. package/examples/ai-coding-assistant-evidence/final_verdict.json +107 -0
  32. package/examples/ai-coding-assistant-evidence/gate_report.json +101 -0
  33. package/examples/ai-coding-assistant-evidence/report_spec.json +23 -12
  34. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +447 -127
  35. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +447 -127
  36. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +447 -127
  37. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +447 -127
  38. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +447 -127
  39. package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +1360 -146
  40. package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +1360 -146
  41. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +1360 -146
  42. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +1360 -146
  43. package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +1360 -146
  44. package/examples/ai-coding-assistant-evidence/result.json +13 -9
  45. package/examples/ai-coding-assistant-evidence/result.zh.json +45 -41
  46. package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
  47. package/examples/ai-coding-assistant-evidence/verdict.json +6 -2
  48. package/examples/spaced-retrieval-practice/applicability.json +14 -0
  49. package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
  50. package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
  51. package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
  52. package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
  53. package/examples/spaced-retrieval-practice/frame.json +58 -0
  54. package/examples/spaced-retrieval-practice/gate_report.json +101 -0
  55. package/examples/spaced-retrieval-practice/methodology.json +78 -0
  56. package/examples/spaced-retrieval-practice/report_spec.json +212 -0
  57. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
  58. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
  59. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
  60. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
  61. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
  62. package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
  63. package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
  64. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
  65. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
  66. package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
  67. package/examples/spaced-retrieval-practice/result.json +942 -0
  68. package/examples/spaced-retrieval-practice/result.zh.json +942 -0
  69. package/examples/spaced-retrieval-practice/skeptic.json +70 -0
  70. package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
  71. package/examples/spaced-retrieval-practice/verdict.json +93 -0
  72. package/examples/workplace-ai-assistant/artifact_manifest.json +15 -0
  73. package/examples/workplace-ai-assistant/claims.jsonl +4 -4
  74. package/examples/workplace-ai-assistant/evidence.jsonl +4 -4
  75. package/examples/workplace-ai-assistant/evidence_graph.json +15 -15
  76. package/examples/workplace-ai-assistant/final_verdict.json +78 -0
  77. package/examples/workplace-ai-assistant/gate_report.json +101 -0
  78. package/examples/workplace-ai-assistant/report_spec.json +209 -40
  79. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +435 -105
  80. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +435 -105
  81. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +435 -105
  82. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +435 -105
  83. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +435 -105
  84. package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
  85. package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
  86. package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
  87. package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
  88. package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
  89. package/examples/workplace-ai-assistant/result.json +82 -20
  90. package/examples/workplace-ai-assistant/result.zh.json +82 -20
  91. package/examples/workplace-ai-assistant/skeptic.json +72 -0
  92. package/examples/workplace-ai-assistant/verdict.json +36 -10
  93. package/integrations/agent_mcp.py +2 -2
  94. package/package.json +12 -3
  95. package/pyproject.toml +4 -3
  96. package/references/report-copy-style.md +67 -0
  97. package/references/retrieval-compliance.md +75 -0
  98. package/references/retrieval-protocol.md +20 -0
  99. package/retrieval/audit.py +27 -3
  100. package/retrieval/fetch.py +96 -0
  101. package/retrieval/sciverse.py +398 -0
  102. package/retrieval/search.py +47 -7
  103. package/schemas/applicability.schema.json +94 -0
  104. package/schemas/chart-spec.schema.json +10 -3
  105. package/schemas/evidence.schema.json +316 -43
  106. package/schemas/fetch-result.schema.json +2 -1
  107. package/schemas/report-result.schema.json +3 -3
  108. package/schemas/report-spec.schema.json +98 -100
  109. package/schemas/skeptic.schema.json +86 -0
  110. package/schemas/source.schema.json +21 -2
  111. package/schemas/v2/finding.schema.json +5 -1
  112. package/schemas/v2/methodology-audit.schema.json +5 -1
  113. package/schemas/v2/outcome.schema.json +28 -5
  114. package/schemas/v2/study.schema.json +5 -1
  115. package/schemas/vNext/autoevolve-session.schema.json +34 -1
  116. package/schemas/vNext/eval-snapshot.schema.json +77 -1
  117. package/schemas/vNext/execution-plan.schema.json +50 -1
  118. package/schemas/vNext/gap-priority.schema.json +54 -1
  119. package/schemas/vNext/negative-search-record.schema.json +68 -1
  120. package/schemas/vNext/research-iteration.schema.json +87 -1
  121. package/schemas/vNext/research-strategy.schema.json +62 -1
  122. package/schemas/vNext/skill-experiment.schema.json +90 -1
  123. package/schemas/vNext/task-spec.schema.json +156 -1
  124. package/schemas/vNext/worker-result.schema.json +60 -1
  125. package/schemas/verdict.schema.json +164 -28
  126. package/scripts/build_esl_artifacts.py +2 -2
  127. package/scripts/build_report_variants.py +18 -2
  128. package/scripts/build_result.py +74 -9
  129. package/scripts/check_package_parity.py +85 -0
  130. package/scripts/check_protocol_alignment.py +375 -0
  131. package/scripts/check_versioned_schemas.py +254 -0
  132. package/scripts/claim_audit.py +13 -8
  133. package/scripts/compute_confidence.py +10 -0
  134. package/scripts/did_regression.py +12 -2
  135. package/scripts/evidence_score.py +5 -2
  136. package/scripts/generate_new_projects.py +4 -4
  137. package/scripts/orchestrator.py +120 -24
  138. package/scripts/pre_verdict_gate.py +224 -26
  139. package/scripts/quickstart.py +18 -2
  140. package/scripts/run_workspace.py +7 -1
  141. package/scripts/skill_payload.py +4 -1
  142. package/scripts/test_adversarial_empirical.py +26 -19
  143. package/scripts/validate_schema.py +31 -1
  144. package/skill/agents/evaluation-designer.md +20 -4
  145. package/skill/agents/evidence-analyst.md +19 -3
  146. package/skill/agents/evidence-judge.md +50 -2
  147. package/skill/agents/evidence-retriever.md +20 -3
  148. package/skill/agents/intervention-designer.md +20 -4
  149. package/skill/agents/method-reviewer.md +18 -2
  150. package/skill/agents/{education-planner.md → research-planner.md} +19 -3
  151. package/skill/agents/skeptic.md +18 -2
  152. package/skill/roles/registry.yaml +11 -11
  153. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
  154. package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
  155. package/skill/sub-skills/data-analysis/SKILL.md +34 -15
  156. package/skill/sub-skills/ethics-review/SKILL.md +33 -10
  157. package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
  158. package/skill/sub-skills/evidence-review/SKILL.md +31 -12
  159. package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
  160. package/skill/sub-skills/literature-review/SKILL.md +35 -14
  161. package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
  162. package/skill/sub-skills/report-generation/SKILL.md +28 -0
  163. package/skill/sub-skills/research-planning/SKILL.md +41 -14
  164. package/skill/sub-skills/study-design/SKILL.md +30 -9
  165. package/skill/task-briefs/adjudicate.md +32 -7
  166. package/skill/task-briefs/applicability.md +37 -2
  167. package/skill/task-briefs/audit.md +32 -7
  168. package/skill/task-briefs/challenge.md +34 -5
  169. package/skill/task-briefs/evaluate.md +30 -5
  170. package/skill/task-briefs/extract.md +31 -8
  171. package/skill/task-briefs/frame.md +39 -10
  172. package/skill/task-briefs/intervene.md +32 -6
  173. package/skill/task-briefs/present.md +32 -8
  174. package/skill/task-briefs/projection.md +36 -2
  175. package/skill/task-briefs/retrieve.md +36 -6
  176. package/skill/workflows/decision-and-pilot.md +76 -1
  177. package/skill/workflows/evaluate-and-update.md +83 -0
  178. package/skill/workflows/evidence-review.md +104 -0
  179. package/visualization/eduevidence-report/scripts/build_figures.py +25 -3
  180. package/visualization/eduevidence-report/scripts/build_infographics.py +5 -1
  181. package/visualization/eduevidence-report/scripts/build_report.py +512 -65
  182. package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
  183. package/visualization/eduevidence-report/scripts/lieflat_engine.py +349 -38
  184. package/visualization/eduevidence-report/scripts/zh_labels.py +80 -1
  185. package/web/architecture.html +14885 -0
  186. package/web/studio/assets/index-B8tkF44Q.css +1 -0
  187. package/web/studio/index.html +2 -2
  188. package/web/studio/assets/index-CzXocaGv.css +0 -1
  189. /package/web/studio/assets/{index-pa7jD7n4.js → index-CQ6Keoyc.js} +0 -0
@@ -7,7 +7,7 @@ reads ONLY the run workspace — no model call, no network.
7
7
 
8
8
  Checklist:
9
9
 
10
- 1. research_frame_valid frame.json validates against education-frame.schema.json
10
+ 1. research_frame_valid frame.json validates against the run domain frame schema
11
11
  2. sources_valid sources.jsonl non-empty and schema-valid
12
12
  3. evidence_schema_valid evidence.jsonl non-empty and schema-valid
13
13
  4. source_dedupe no duplicate sources remain (dedupe applied)
@@ -58,7 +58,7 @@ for _p in (str(ROOT), str(ROOT / "scripts")):
58
58
 
59
59
  from validate_schema import SchemaError, Validator # noqa: E402
60
60
  from evidence_score import independent_samples, independent_studies # noqa: E402
61
- from evidence_semantics import claim_relation # noqa: E402
61
+ from evidence_semantics import claim_relation, decision_relation # noqa: E402
62
62
  from run_workspace import utc_now # noqa: E402
63
63
 
64
64
  GATE_VERSION = "2026-08-13.v1"
@@ -66,29 +66,52 @@ GATE_VERSION = "2026-08-13.v1"
66
66
  CONFIDENCE_RANK = {"Insufficient": 0, "Low": 1, "Moderate": 2, "High": 3}
67
67
 
68
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
- }
69
+ def _supported_outcomes() -> set[str]:
70
+ """Every registered outcome token, read from the domain registry.
71
+
72
+ This was a hand-copied 20-token education list in three separate files;
73
+ it silently rejected policy tokens such as policy_effectiveness. The
74
+ registry (domains/<id>/outcome_taxonomy.json) is the single authority.
75
+ """
76
+ from engine.taxonomy import all_tokens_ordered
77
+
78
+ return set(all_tokens_ordered())
79
+
80
+
81
+ SUPPORTED_OUTCOMES = _supported_outcomes()
77
82
 
78
83
  _CLAIM_ID_RE = re.compile(r"\b[A-Z]{1,3}-\d{2,4}\b")
79
84
 
80
85
  _SCHEMA_CACHE: dict[str, dict[str, Any]] = {}
81
86
 
82
87
 
88
+ def _schema_path(name: str) -> Path:
89
+ """Resolve a schema name to a file.
90
+
91
+ Accepts a bare name in schemas/ as well as a repository-relative path,
92
+ because a domain may own its frame contract outside schemas/ (policy does:
93
+ domains/policy/frame.schema.json).
94
+ """
95
+ candidate = ROOT / "schemas" / name
96
+ if candidate.is_file():
97
+ return candidate
98
+ owned = ROOT / name
99
+ if owned.is_file():
100
+ return owned
101
+ return candidate # missing: the caller reports it
102
+
103
+
83
104
  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]
105
+ path = _schema_path(name)
106
+ key = str(path)
107
+ if key not in _SCHEMA_CACHE:
108
+ _SCHEMA_CACHE[key] = json.loads(path.read_text(encoding="utf-8"))
109
+ return _SCHEMA_CACHE[key]
87
110
 
88
111
 
89
112
  def _validate_records(records: list[dict[str, Any]], schema_name: str, path: str) -> list[str]:
90
113
  schema = _schema(schema_name)
91
- validator = Validator(schema, base_dir=(ROOT / "schemas"))
114
+ validator = Validator(schema, base_dir=_schema_path(schema_name).parent)
92
115
  errors = []
93
116
  for idx, record in enumerate(records):
94
117
  try:
@@ -133,11 +156,43 @@ def _item_res(status: str, detail: str, *, blocks_high: bool | None = None) -> d
133
156
  return res
134
157
 
135
158
 
159
+ def workspace_domain(ws: Path) -> str:
160
+ """The domain this run/pack registered; defaults to education when absent.
161
+
162
+ A run workspace records it in run_manifest.json, but an example pack has
163
+ no manifest: reading only that file made a policy pack validate against the
164
+ education frame schema and fail item 1. Fall back to the same declared
165
+ places the read model uses, in the same order.
166
+ """
167
+ manifest = _load_ws_json(ws, "run_manifest.json")
168
+ if manifest.get("domain"):
169
+ return str(manifest["domain"])
170
+ frame = _load_ws_json(ws, "frame.json")
171
+ declared = (frame.get("extensions") or {}).get("domain")
172
+ if declared:
173
+ return str(declared)
174
+ result_meta = (_load_ws_json(ws, "result.json").get("meta") or {})
175
+ if result_meta.get("domain"):
176
+ return str(result_meta["domain"])
177
+ return "education"
178
+
179
+
180
+ def frame_schema_name(domain: str) -> str:
181
+ """Registered frame schema for a domain (repository-relative path)."""
182
+ from engine.evidencecore import load_domain
183
+
184
+ return str(load_domain(domain)["frame_schema"])
185
+
186
+
136
187
  def check_research_frame(ws: Path) -> dict[str, str]:
137
188
  frame = _load_ws_json(ws, "frame.json")
138
189
  if not frame:
139
190
  return _item_res("fail", "frame.json missing or empty (research question not framed)")
140
- errors = _validate_records([frame], "education-frame.schema.json", "frame")
191
+ try:
192
+ schema_name = frame_schema_name(workspace_domain(ws))
193
+ except KeyError as exc:
194
+ return _item_res("fail", f"run declares an unknown domain: {exc}")
195
+ errors = _validate_records([frame], schema_name, "frame")
141
196
  if errors:
142
197
  return _item_res("fail", f"frame.json schema invalid: {errors[0]}")
143
198
  return _item_res("pass", f"frame.json valid (question={frame.get('question', '')[:80]})")
@@ -182,11 +237,41 @@ def check_counter_evidence(ws: Path) -> dict[str, str]:
182
237
  return _item_res("fail", "skeptic.json missing or empty (counter-evidence search not performed)")
183
238
  if skeptic.get("search_performed") is not True:
184
239
  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"
240
+
241
+ # The nine fixed checks are the contract (skill/task-briefs/challenge.md).
242
+ # A two-key shell used to pass this gate, which made the counter-evidence
243
+ # check cosmetic on every deterministic path.
244
+ required_checks = (
245
+ "1_null_result", "2_negative_result", "3_contradictory_evidence",
246
+ "4_alternative_explanation", "5_measurement_mismatch", "6_sampling_bias",
247
+ "7_novelty_effect", "8_ai_dependency", "9_scope_overreach",
248
+ )
249
+ findings = skeptic.get("skeptic_findings")
250
+ if not isinstance(findings, list) or not findings:
251
+ return _item_res(
252
+ "fail", "skeptic.json lacks skeptic_findings[] (nine fixed checks not run)")
253
+ present = {str(f.get("check")) for f in findings if isinstance(f, dict)}
254
+ missing = [c for c in required_checks if c not in present]
255
+ if missing:
256
+ return _item_res("fail", f"skeptic findings incomplete; missing {missing}")
257
+ for item in findings:
258
+ if not isinstance(item, dict) or item.get("status") not in ("found", "not_found"):
259
+ return _item_res(
260
+ "fail", f"skeptic finding {item.get('check')!r} lacks a found/not_found status")
261
+
262
+ found = bool(skeptic.get("contradictory_evidence_found"))
263
+ statement = skeptic.get("no_contradictory_evidence_statement")
264
+ if not found and not statement:
265
+ # Absence of counter-evidence must be asserted, not left implicit.
266
+ return _item_res(
267
+ "fail", "no contradictory evidence found but no statement recorded")
268
+ if found and statement:
269
+ return _item_res(
270
+ "fail", "contradictory_evidence_found=true together with a no-evidence statement")
271
+
272
+ found_checks = sum(1 for f in findings if f.get("status") == "found")
273
+ detail = (f"search_performed=true; 9/9 checks run; findings={found_checks}; "
274
+ f"contradictory_evidence_found={found}")
190
275
  return _item_res("pass", detail)
191
276
 
192
277
 
@@ -270,6 +355,96 @@ def check_claim_evidence(ws: Path) -> dict[str, str]:
270
355
  return _item_res("pass", "all verdict claims bind to existing evidence with consistent categories")
271
356
 
272
357
 
358
+ def _primary_evidence_summary(ws: Path) -> dict[str, Any]:
359
+ """Re-derive ADOPT eligibility from the pack's own evidence records.
360
+
361
+ The gate audits an artifact another party wrote, so it cannot trust the
362
+ verdict about itself: primary-result directness is read back from the
363
+ corpus (frame primary outcomes + per-record D5 Directness), using the same
364
+ domain registry and directness threshold the tribunal imports.
365
+ """
366
+ from engine.decision_policy import (
367
+ ADOPT_DIRECTNESS,
368
+ outcome_category,
369
+ primary_effect_categories,
370
+ )
371
+
372
+ domain = workspace_domain(ws)
373
+ try:
374
+ primary = primary_effect_categories(domain)
375
+ except (KeyError, ValueError) as exc:
376
+ return {"domain": domain, "primary": (), "direct": False, "error": str(exc)}
377
+
378
+ frame = _load_ws_json(ws, "frame.json")
379
+ declared_primary = [
380
+ str(token) for token in ((frame.get("outcomes") or {}).get("primary") or [])
381
+ ]
382
+ evidence = _load_ws_jsonl(ws, "evidence.jsonl")
383
+ direct_hits: list[str] = []
384
+ for ev in evidence:
385
+ token = str(ev.get("outcome_type") or "")
386
+ category = outcome_category(domain, token, primary)
387
+ if category is None or category not in primary:
388
+ continue
389
+ dims = ev.get("quality_dimensions") or {}
390
+ d5 = dims.get("D5_directness")
391
+ if isinstance(d5, bool) or not isinstance(d5, int):
392
+ continue
393
+ if d5 >= ADOPT_DIRECTNESS:
394
+ direct_hits.append(str(ev.get("evidence_id") or "?"))
395
+ return {
396
+ "domain": domain,
397
+ "primary": primary,
398
+ "declared_primary": declared_primary,
399
+ "direct": bool(direct_hits),
400
+ "direct_evidence_ids": direct_hits,
401
+ }
402
+
403
+
404
+ def check_decision_action(ws: Path) -> dict[str, str]:
405
+ """The stated action must be the action the evidence supports.
406
+
407
+ A verdict cannot hand itself ADOPT: the gate requires High confidence, a
408
+ supporting decisive relation, and primary-outcome evidence at directness 2.
409
+ Anything less is capped to pilot - the conservative bound - because the
410
+ underlying evidence may still justify a bounded trial.
411
+ """
412
+ verdict = _verdict_for_audit(ws)
413
+ if not verdict:
414
+ return _item_res("warn", "no verdict artifact yet; action not audited")
415
+ action = str(verdict.get("recommended_action") or "").lower()
416
+ if action != "adopt":
417
+ label = action or "unset"
418
+ return _item_res("pass", "action=" + label + " is within the conservative bound")
419
+
420
+ from engine.decision_policy import ADOPT_REQUIRED_LABEL, decision_action
421
+
422
+ evidence = _load_ws_jsonl(ws, "evidence.jsonl")
423
+ relations = [decision_relation(ev) for ev in evidence]
424
+ decisive = {str(index): rel for index, rel in enumerate(relations)
425
+ if rel in ("support_adoption", "oppose_adoption")}
426
+ label = str(verdict.get("confidence") or "")
427
+ summary = _primary_evidence_summary(ws)
428
+ expected = decision_action(
429
+ confidence_label=label,
430
+ decisive_relations=decisive,
431
+ has_direct_primary_evidence=bool(summary.get("direct")),
432
+ )
433
+ if expected == "ADOPT":
434
+ detail = "ADOPT is supported: " + label + " confidence with direct primary-outcome evidence"
435
+ return _item_res("pass", detail)
436
+ reasons: list[str] = []
437
+ if label != ADOPT_REQUIRED_LABEL:
438
+ reasons.append("confidence=" + (label or "unset") + " (needs " + ADOPT_REQUIRED_LABEL + ")")
439
+ if "support_adoption" not in relations:
440
+ reasons.append("no decisive supporting evidence")
441
+ if not summary.get("direct"):
442
+ reasons.append("no primary-outcome evidence at directness 2")
443
+ detail = ("recommended_action=adopt is not supported (" + "; ".join(reasons)
444
+ + "); the evidence bounds this decision to pilot")
445
+ return _item_res("fail", detail)
446
+
447
+
273
448
  def check_outcome_mapping(ws: Path) -> dict[str, str]:
274
449
  verdict = _verdict_for_audit(ws)
275
450
  frame = _load_ws_json(ws, "frame.json")
@@ -282,18 +457,35 @@ def check_outcome_mapping(ws: Path) -> dict[str, str]:
282
457
  issues.append(f"unknown outcome key(s) in verdict: {', '.join(sorted(unknown))}")
283
458
 
284
459
  evidence_outcomes = {e.get("outcome_type") for e in _load_ws_jsonl(ws, "evidence.jsonl")}
460
+ frame_outcomes = frame.get("outcomes", {}) or {}
285
461
  declared = set()
286
462
  for group in ("primary", "secondary", "risk"):
287
- declared.update((frame.get("outcomes", {}) or {}).get(group, []) or [])
463
+ declared.update(frame_outcomes.get(group, []) or [])
288
464
  if declared:
289
465
  missing = sorted(d for d in declared if d and d not in evidence_outcomes)
290
466
  if missing:
291
- notes.append(f"frame-declared outcomes without evidence: {', '.join(missing)}")
467
+ # Missing evidence is not a zero effect, and a secondary or risk
468
+ # outcome the frame named but the corpus never measured does not
469
+ # contaminate the decision. Only a PRIMARY outcome with no
470
+ # evidence at all means the decision rests on the wrong construct,
471
+ # so only that case blocks High confidence.
472
+ primary_missing = sorted(
473
+ d for d in (frame_outcomes.get("primary", []) or [])
474
+ if d and d not in evidence_outcomes)
475
+ note = f"frame-declared outcomes without evidence: {', '.join(missing)}"
476
+ if primary_missing:
477
+ return _item_res(
478
+ "warn",
479
+ note + f"; primary outcomes unmeasured: {', '.join(primary_missing)}",
480
+ blocks_high=True)
481
+ notes.append(note)
482
+ else:
483
+ notes.append("all frame-declared outcomes are covered by evidence")
292
484
 
293
485
  if issues:
294
486
  return _item_res("fail", "; ".join(issues))
295
- if notes:
296
- return _item_res("warn", "outcome keys known; " + notes[0])
487
+ if notes and "without evidence" in notes[0]:
488
+ return _item_res("warn", "outcome keys known; " + notes[0], blocks_high=False)
297
489
  if not declared:
298
490
  return _item_res("warn", "outcome keys known; frame declares no outcomes to map")
299
491
  return _item_res("pass", f"outcome mapping complete ({len(evidence_outcomes)} outcome type(s) covered)")
@@ -375,13 +567,15 @@ GATE_ITEMS: list[dict[str, Any]] = [
375
567
  {"id": "claim_evidence_audit", "title": "Claim-Evidence Audit", "critical": True,
376
568
  "blocks_high": False, "check": check_claim_evidence},
377
569
  {"id": "outcome_mapping", "title": "Outcome mapping", "critical": False,
378
- "blocks_high": False, "check": check_outcome_mapping},
570
+ "blocks_high": True, "check": check_outcome_mapping},
379
571
  {"id": "scope_calibration", "title": "Scope calibration", "critical": False,
380
- "blocks_high": False, "check": check_scope_calibration},
572
+ "blocks_high": True, "check": check_scope_calibration},
381
573
  {"id": "independent_study_count", "title": "Independent study-sample count", "critical": True,
382
574
  "blocks_high": True, "check": check_study_count},
383
575
  {"id": "deterministic_confidence", "title": "Deterministic confidence", "critical": True,
384
576
  "blocks_high": True, "check": None},
577
+ {"id": "decision_action_consistency", "title": "Decision action consistency",
578
+ "critical": True, "blocks_high": False, "check": check_decision_action},
385
579
  ]
386
580
 
387
581
 
@@ -437,6 +631,7 @@ def apply_enforcement(verdict: dict[str, Any], gate: dict[str, Any]) -> dict[str
437
631
 
438
632
  - gate failed -> confidence at most Low; adopt downgraded to pilot
439
633
  - gate passed but High blocked -> confidence at most Moderate
634
+ - stated adopt the evidence does not support -> downgraded to pilot
440
635
  Always records the enforcement inside verdict.extensions.gate_enforcement.
441
636
  """
442
637
  import copy
@@ -448,6 +643,9 @@ def apply_enforcement(verdict: dict[str, Any], gate: dict[str, Any]) -> dict[str
448
643
  out["confidence"] = cap
449
644
  if not gate.get("passed", False) and out.get("recommended_action") == "adopt":
450
645
  out["recommended_action"] = "pilot"
646
+ action_item = (gate.get("items") or {}).get("decision_action_consistency") or {}
647
+ if action_item.get("status") == "fail" and out.get("recommended_action") == "adopt":
648
+ out["recommended_action"] = "pilot"
451
649
  extensions = out.setdefault("extensions", {})
452
650
  if not isinstance(extensions, dict):
453
651
  extensions = {}
@@ -40,8 +40,23 @@ STAGE_BRIEFS = {
40
40
  }
41
41
 
42
42
 
43
+ def runs_root() -> Path:
44
+ """Runs directory, honouring EDUEVIDENCE_RUNS_DIR like the orchestrator does.
45
+
46
+ Without this the quickstart wrote into the repository even when the user had
47
+ pointed the runs directory elsewhere.
48
+ """
49
+ import os
50
+
51
+ return Path(os.environ.get("EDUEVIDENCE_RUNS_DIR") or (ROOT / "runs"))
52
+
53
+
43
54
  def newest_run_dir() -> Path:
44
- runs_dir = ROOT / "runs"
55
+ runs_dir = runs_root()
56
+ if not runs_dir.is_dir():
57
+ raise SystemExit(
58
+ f"no runs directory at {runs_dir}; run `eduevidence run --question ...` first "
59
+ "(or set EDUEVIDENCE_RUNS_DIR)")
45
60
  candidates = sorted((p for p in runs_dir.iterdir() if p.is_dir()),
46
61
  key=lambda p: p.stat().st_mtime, reverse=True)
47
62
  if not candidates:
@@ -86,7 +101,8 @@ def build_next_steps(run_dir: Path, question: str, depth: str) -> str:
86
101
  "",
87
102
  "## 可信度自检",
88
103
  "",
89
- "- 引用逐条核验报告:`benchmarks/doi-audit/report.md` 与包内 `citation_check.md`",
104
+ "- 引用逐条核验报告:包内 `citation_check.md`"
105
+ "(源码仓库另见 `benchmarks/doi-audit/report.md`)",
90
106
  "- 报告头徽章标注 data_origin;synthetic 演示不得当作实证引用",
91
107
  ""]
92
108
  return "\n".join(lines)
@@ -179,14 +179,20 @@ def build_manifest(
179
179
  scp_available: bool | None = None,
180
180
  root: Path | None = None,
181
181
  started_at: str | None = None,
182
+ domain: str = "education",
182
183
  ) -> dict[str, Any]:
183
- """Phase 13 run manifest with every contract field."""
184
+ """Phase 13 run manifest with every contract field.
185
+
186
+ ``domain`` selects which registered frame contract this run must satisfy;
187
+ it defaults to education so existing callers and manifests stay valid.
188
+ """
184
189
  return {
185
190
  "run_id": run_id,
186
191
  "skill_version": SKILL_VERSION,
187
192
  "git_commit": git_commit(root),
188
193
  "started_at": started_at or utc_now(),
189
194
  "question": question,
195
+ "domain": domain,
190
196
  "execution_mode": execution_mode,
191
197
  "scp_available": detect_scp() if scp_available is None else scp_available,
192
198
  "agent_mcp_available": agent_mcp_available,
@@ -13,6 +13,7 @@ TREES = (
13
13
  FILES = (
14
14
  "SKILL.md", "eduevidence_cli.py", "install.sh", "pyproject.toml", "setup.py",
15
15
  "LICENSE", "CHANGELOG.md", "README.md", "README.zh-CN.md", "web/index.html",
16
+ "CONTRIBUTING.md", "web/architecture.html",
16
17
  "benchmarks/evidence-library.json", "benchmarks/partitions.json",
17
18
  "benchmarks/adversarial/cases.jsonl",
18
19
  )
@@ -21,11 +22,13 @@ DOCS = (
21
22
  "reproducibility.md", "release-contract.md", "autoresearch-evolution-plan.md",
22
23
  "orchestration-role-model.md", "autoresearch-implementation-status.md",
23
24
  "research-studio-guide.zh-CN.md", "demo-workplace-ai.md",
25
+ "sciverse-api.md",
24
26
  "release-closeout/README.md", "release-closeout/issues.md",
25
27
  "release-closeout/frontend-acceptance.md", "release-closeout/verification.md",
26
28
  )
27
29
  EXAMPLES = (
28
- "ai-coding-assistant-evidence", "workplace-ai-assistant",
30
+ "ai-coding-assistant-evidence", "spaced-retrieval-practice",
31
+ "workplace-ai-assistant",
29
32
  )
30
33
  EXAMPLE_FILES = (
31
34
  "result.json", "result.zh.json", "evidence_graph.json", "report_spec.json",
@@ -87,9 +87,8 @@ def test_eventbus_concurrency():
87
87
  t.join()
88
88
 
89
89
  print(f"[*] Concurrent subscribe/unsubscribe completed. Race errors caught: {len(race_errors)}")
90
- if race_errors:
91
- for err in race_errors[:5]:
92
- print(f" - [BUG FOUND] {err}")
90
+ assert not race_errors, (
91
+ f"EventBus raised under concurrent subscribe/unsubscribe: {race_errors[:3]}")
93
92
 
94
93
  # 2. Test Subscribe Non-Atomic Check (Duplicate Subscriber Appending)
95
94
  bus._subscribers.clear()
@@ -105,8 +104,9 @@ def test_eventbus_concurrency():
105
104
  t.join()
106
105
 
107
106
  print(f"[*] Subscribed same callback across 10 threads. Total registered subscribers: {len(bus._subscribers)} (Expected: 1)")
108
- if len(bus._subscribers) > 1:
109
- print(f" - [BUG FOUND] Race condition in subscribe: duplicate callbacks registered ({len(bus._subscribers)})")
107
+ assert len(bus._subscribers) == 1, (
108
+ f"subscribe() is not atomic: {len(bus._subscribers)} copies of one callback "
109
+ "registered from 10 threads")
110
110
 
111
111
  # 3. Concurrent Publish & Unbounded Memory Leak
112
112
  bus.clear()
@@ -156,21 +156,23 @@ def test_did_regression_adversarial():
156
156
  results = {}
157
157
 
158
158
  # Case 2.1: Column Name Parsing Collision
159
+ # Enough rows for a DID fit (n - 4 > 0): this case is about COLUMN MAPPING,
160
+ # not about saturation, so a 4-row fixture would test the wrong thing.
159
161
  with tempfile.NamedTemporaryFile("w", suffix=".csv", delete=False) as f:
160
162
  writer = csv.writer(f)
161
163
  writer.writerow(["student_id", "treatment_group", "post_test_score", "time_period"])
162
- writer.writerow([1, 1, 85.0, 1])
163
- writer.writerow([2, 1, 70.0, 0])
164
- writer.writerow([3, 0, 80.0, 1])
165
- writer.writerow([4, 0, 75.0, 0])
164
+ for i in range(1, 13):
165
+ treat = 1 if i % 2 else 0
166
+ post = 1 if i % 3 else 0
167
+ writer.writerow([i, treat, 60.0 + 4 * treat + 3 * post + 2 * treat * post, post])
166
168
  col_test_path = f.name
167
169
 
168
170
  res_col = run_did_analysis(col_test_path)
169
171
  os.remove(col_test_path)
170
172
  print(f"[*] Case 2.1: Column name collision ('treatment_group', 'post_test_score', 'time_period'):")
171
173
  print(f" Result: {res_col}")
172
- if res_col.get("status") == "error":
173
- print(f" - [BUG FOUND] Column mapper failed to parse outcome column due to 'post' keyword priority collision!")
174
+ assert res_col.get("status") != "error", (
175
+ "DID column mapper failed to parse the outcome column (keyword collision)")
174
176
  results["column_mapping_bug"] = res_col
175
177
 
176
178
  # Case 2.2: Perfect Multicollinearity / Singular Design Matrix
@@ -187,8 +189,10 @@ def test_did_regression_adversarial():
187
189
  os.remove(collinear_path)
188
190
  print(f"\n[*] Case 2.2: Singular Matrix / Perfect Multicollinearity:")
189
191
  print(f" Result: {json.dumps(res_coll, indent=2)}")
190
- if res_coll.get("status") == "success" and res_coll.get("standard_error") == 1.0:
191
- print(f" - [BUG FOUND] Matrix inversion failed on singular matrix, but returned fake standard_error=1.0 and fake p_value={res_coll.get('p_value')} instead of reporting collinearity/singular error!")
192
+ assert not (res_coll.get("status") == "success"
193
+ and res_coll.get("standard_error") == 1.0), (
194
+ "singular design matrix produced a fabricated standard error of 1.0 "
195
+ "instead of reporting collinearity")
192
196
  results["singular_matrix_fallback"] = res_coll
193
197
 
194
198
  # Case 2.3: Zero Variance in Outcome
@@ -228,8 +232,9 @@ def test_did_regression_adversarial():
228
232
  # Case 2.5: WWC Baseline Equivalence Rating for QED
229
233
  print(f"\n[*] Case 2.5: WWC 5.0 Baseline Rating Check for QED:")
230
234
  print(f" When baseline_equivalence_g = {res_sat.get('baseline_equivalence_g')}, WWC rating reported is: '{res_sat.get('wwc_baseline_rating')}'")
231
- if res_sat.get("wwc_baseline_rating") == "Meets Standards Without Reservations":
232
- print(f" - [BUG FOUND] Methodological violation: Quasi-Experimental Designs (QED/DID) can NEVER meet WWC standards without reservations; maximum possible rating is 'Meets Standards With Reservations'!")
235
+ assert res_sat.get("wwc_baseline_rating") != "Meets Standards Without Reservations", (
236
+ "methodological violation: a quasi-experimental design was rated as meeting "
237
+ "WWC standards without reservations")
233
238
  results["wwc_rating_bug"] = res_sat.get("wwc_baseline_rating")
234
239
 
235
240
  # Case 2.6: Small Sample Normal Z-Test vs Student t-distribution
@@ -307,8 +312,8 @@ def test_evidence_graph_adversarial():
307
312
  print(f"[*] Meta-synthesis with NaN effect and negative weight:")
308
313
  print(f" - Pooled g: {proc_syn.get('pooled_g')}")
309
314
  print(f" - Q statistic: {proc_syn.get('q_statistic')}")
310
- if math.isnan(proc_syn.get("pooled_g", 0.0)):
311
- print(f" - [BUG FOUND] NaN effect size propagated directly into meta_synthesis without input validation!")
315
+ assert not math.isnan(proc_syn.get("pooled_g", 0.0)), (
316
+ "NaN effect size propagated into meta-synthesis without validation")
312
317
 
313
318
  return {
314
319
  "json_roundtrip": len(reloaded.edges),
@@ -495,8 +500,10 @@ def test_dashboard_server_adversarial():
495
500
  except Exception as e:
496
501
  leakage_results[tf] = False
497
502
 
498
- if any(leakage_results.values()):
499
- print(f" - [SECURITY DEFECT] StudioHandler exposes arbitrary local project source files through unauthenticated HTTP GET via super().do_GET() fallback!")
503
+ leaked = [path for path, ok in leakage_results.items() if ok]
504
+ assert not leaked, (
505
+ "StudioHandler served local source files over unauthenticated GET: "
506
+ f"{leaked}")
500
507
  results["file_leakage"] = leakage_results
501
508
 
502
509
  # 3. Concurrency Stress Test (30 Concurrent HTTP Clients)
@@ -3,7 +3,8 @@
3
3
 
4
4
  Zero-dependency JSON Schema (draft-07 subset) validator covering the constructs
5
5
  used by schemas/*.schema.json: $id, title, description, type, properties,
6
- required, enum, minimum, maximum, minLength, additionalProperties, $ref
6
+ required, enum, minimum, maximum, minLength, additionalProperties, $ref,
7
+ anyOf / oneOf / allOf
7
8
  (local #/definitions and relative-file references), const, format (uri,
8
9
  date-time), pattern.
9
10
 
@@ -138,6 +139,35 @@ class Validator:
138
139
  self.validate(value, self._resolve_ref(ref, path), path)
139
140
  return
140
141
 
142
+ # Combinators (draft-07 subset): anyOf / oneOf / allOf. Without
143
+ # these a schema can express a real alternative shape and be
144
+ # silently ignored - which is how report-spec (two accepted
145
+ # shapes) and visual-layout (oneOf) became dead weight.
146
+ for key in ("anyOf", "oneOf", "allOf"):
147
+ subschemas = schema.get(key)
148
+ if not isinstance(subschemas, list) or not subschemas:
149
+ continue
150
+ matched = 0
151
+ first_error = None
152
+ for sub in subschemas:
153
+ try:
154
+ self.validate(value, sub, path)
155
+ matched += 1
156
+ except SchemaError as exc:
157
+ if first_error is None:
158
+ first_error = exc
159
+ if key == "allOf" and matched != len(subschemas):
160
+ raise first_error or SchemaError(
161
+ f"{path}: allOf not satisfied")
162
+ if key in ("anyOf", "oneOf") and matched == 0:
163
+ detail = f" (first: {first_error})" if first_error else ""
164
+ raise SchemaError(
165
+ f"{path}: matches none of the {key} alternatives" + detail)
166
+ if key == "oneOf" and matched > 1:
167
+ raise SchemaError(
168
+ f"{path}: matches {matched} oneOf alternatives "
169
+ "(exactly one required)")
170
+
141
171
  if "type" in schema:
142
172
  types = schema["type"]
143
173
  if isinstance(types, str):
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: evaluation-designer
3
3
  description: EduEvidence 效果评价设计者。为任何 PILOT/ADOPT 建议附 EvaluationPlan:基线/后测/保持/迁移 + 过程/学习/风险指标 + 成功阈值与停止条件;区分任务表现与学习效果。
4
- default_cli: claude
5
- default_model: claude-sonnet-4-6
4
+ role_id: evaluation-designer
5
+ capabilities: evaluation_design, data_validation, data_analysis
6
+ output_contracts: evaluation.json (schemas/evaluation.schema.json)
7
+ recommended_reasoning: high # capability hint only — no model or CLI name is bound here
6
8
  default_permission: read
7
9
  default_summary_chars: 800
8
10
  default_context_mode: compact
@@ -54,7 +56,7 @@ critical_path: false
54
56
  - `groups` 是对象,必须含 `treatment` 与 `comparison` 两个字段;
55
57
  - `process_metrics` / `learning_metrics` / `risk_metrics` / `stop_conditions` 必须都是**数组**,禁止逗号拼接字符串;
56
58
  - `retention_test` / `transfer_test` 是字符串或 `null`——没有延迟/迁移测试时必须显式写 `null`,禁止缺失字段;
57
- - `risk_metrics` 必须覆盖 AI 教学风险(如 `ai_dependency`、`academic_integrity_risk`、`false_confidence`,红线要求),缺失即不合格;
59
+ - `risk_metrics` 必须覆盖该干预的相关风险(教育场景如 `ai_dependency`、`academic_integrity_risk`、`false_confidence`;其他领域用其自身风险构念),缺失即不合格;
58
60
  - `analysis_plan` 必须写明统计方法(如基线调整 ANCOVA),且任务表现与学习指标分开报告;
59
61
  - `learning_metrics` 不得只含 self-report(红线),至少一项客观/行为指标。
60
62
 
@@ -63,7 +65,7 @@ critical_path: false
63
65
  - 只测任务完成速度的评价方案 → 不合格,必须补学习/保持/迁移指标;
64
66
  - 迁移测试必须是**无 AI 环境**的新任务;
65
67
  - 不许把 self-report 当作唯一的学习指标;
66
- - 风险指标缺失 → 不合格(AI 教学试点必须测 AI 依赖与学术诚信风险)。
68
+ - 风险指标缺失 → 不合格(任何 AI 相关试点都必须测依赖与诚信/合规风险)。
67
69
 
68
70
  ## 输出格式
69
71
 
@@ -72,3 +74,17 @@ critical_path: false
72
74
  ## 卡住升级
73
75
 
74
76
  干预方案缺失回传 `NEEDS_CONTEXT`;课堂现实约束不明回传 `NEEDS_USER_CONTEXT`。
77
+
78
+ ## 独立性与交叉评审
79
+
80
+ - 阈值在数据到达前固定;事后调整属协议偏离,必须记录而非吸收。
81
+ - 与分析阶段的独立性:评价设计者不参与对结果的解释,避免"自己设计自己解释"。
82
+ - 与宿主的模型选择解耦:本文件只声明能力要求(`recommended_reasoning: high`、量化偏好),具体 CLI/模型由用户确认的模型映射决定。
83
+
84
+ ## 失败模式与回退
85
+
86
+ | 失败 | 处理 |
87
+ |---|---|
88
+ | 无法设置对照 | 改为单组前后测并显式标注设计局限。 |
89
+ | 样本量不足 | 报告功效局限;不得把不显著当作"无效果"。 |
90
+ | 阈值事后变更 | 记录为协议偏离并降级结论强度。 |