eduevidence 6.0.0 → 6.3.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 (267) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/CONTRIBUTING.md +105 -0
  3. package/README.md +113 -49
  4. package/README.zh-CN.md +39 -12
  5. package/SKILL.md +15 -5
  6. package/assets/readme/landing-tour.gif +0 -0
  7. package/assets/readme/studio-tour.gif +0 -0
  8. package/benchmarks/evidence-library.json +277 -1
  9. package/bin/eduevidence.js +2 -1
  10. package/docs/architecture.md +325 -46
  11. package/docs/demo-workplace-ai.md +1 -1
  12. package/docs/install-guide.md +1 -1
  13. package/docs/j-ev-experimental.md +250 -0
  14. package/docs/orchestration-role-model.md +1 -1
  15. package/docs/release-closeout/README.md +1 -1
  16. package/docs/reproducibility.md +138 -0
  17. package/docs/sciverse-api.md +125 -0
  18. package/domains/_neutral/copy/few_shots.json +21 -0
  19. package/domains/_neutral/copy/framing_lexicon.json +19 -0
  20. package/domains/_neutral/copy/module_labels.json +5 -0
  21. package/domains/_neutral/copy/module_labels_footer.json +102 -0
  22. package/domains/_neutral/copy/module_labels_modules.json +204 -0
  23. package/domains/_neutral/copy/module_labels_nav.json +126 -0
  24. package/domains/_neutral/copy/module_labels_summary.json +98 -0
  25. package/domains/_neutral/copy/module_labels_tables.json +164 -0
  26. package/domains/_neutral/copy/module_labels_v2.json +90 -0
  27. package/domains/_neutral/copy/risk_constructs.json +20 -0
  28. package/domains/_neutral/copy/section_titles.json +66 -0
  29. package/domains/_neutral/copy/terminology.json +11 -0
  30. package/domains/check_copy_packs.py +103 -0
  31. package/domains/education/copy/few_shots.json +22 -0
  32. package/domains/education/copy/framing_enums.json +167 -0
  33. package/domains/education/copy/framing_lexicon.json +166 -0
  34. package/domains/education/copy/module_labels.json +169 -0
  35. package/domains/education/copy/risk_constructs.json +48 -0
  36. package/domains/education/copy/section_titles.json +186 -0
  37. package/domains/education/copy/terminology.json +70 -0
  38. package/domains/education/manifest.json +1 -1
  39. package/domains/education/outcome_taxonomy.json +2 -2
  40. package/domains/manifest.json +1 -1
  41. package/domains/policy/copy/few_shots.json +22 -0
  42. package/domains/policy/copy/framing_enums.json +94 -0
  43. package/domains/policy/copy/framing_lexicon.json +174 -0
  44. package/domains/policy/copy/module_labels.json +168 -0
  45. package/domains/policy/copy/risk_constructs.json +33 -0
  46. package/domains/policy/copy/section_titles.json +186 -0
  47. package/domains/policy/copy/terminology.json +64 -0
  48. package/eduevidence_cli.py +10 -0
  49. package/engine/capabilities.py +57 -5
  50. package/engine/decision_policy.py +167 -0
  51. package/engine/evidence_graph.py +14 -10
  52. package/engine/gaps.py +42 -22
  53. package/engine/ids.py +2 -0
  54. package/engine/library.py +6 -2
  55. package/engine/library_builtin.py +7 -4
  56. package/engine/living.py +34 -4
  57. package/engine/migration.py +88 -3
  58. package/engine/orchestration.py +5 -5
  59. package/engine/paths.py +2 -0
  60. package/engine/pilot.py +34 -32
  61. package/engine/taxonomy.py +211 -0
  62. package/engine/tribunal.py +49 -43
  63. package/engine/versions.py +1 -1
  64. package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +1361 -147
  65. package/examples/ai-coding-assistant-evidence/artifact_manifest.json +3 -3
  66. package/examples/ai-coding-assistant-evidence/citation_check.json +1 -1
  67. package/examples/ai-coding-assistant-evidence/final_verdict.json +107 -0
  68. package/examples/ai-coding-assistant-evidence/gate_report.json +101 -0
  69. package/examples/ai-coding-assistant-evidence/report_spec.json +23 -12
  70. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +448 -128
  71. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +448 -128
  72. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +448 -128
  73. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +448 -128
  74. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +448 -128
  75. package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +1360 -146
  76. package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +1360 -146
  77. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +1360 -146
  78. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +1360 -146
  79. package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +1360 -146
  80. package/examples/ai-coding-assistant-evidence/result.json +13 -9
  81. package/examples/ai-coding-assistant-evidence/result.zh.json +45 -41
  82. package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
  83. package/examples/ai-coding-assistant-evidence/verdict.json +6 -2
  84. package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
  85. package/examples/spaced-retrieval-practice/applicability.json +14 -0
  86. package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
  87. package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
  88. package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
  89. package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
  90. package/examples/spaced-retrieval-practice/frame.json +58 -0
  91. package/examples/spaced-retrieval-practice/gate_report.json +101 -0
  92. package/examples/spaced-retrieval-practice/methodology.json +78 -0
  93. package/examples/spaced-retrieval-practice/report.html +2522 -0
  94. package/examples/spaced-retrieval-practice/report_spec.json +212 -0
  95. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
  96. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
  97. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
  98. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
  99. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
  100. package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
  101. package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
  102. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
  103. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
  104. package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
  105. package/examples/spaced-retrieval-practice/result.json +942 -0
  106. package/examples/spaced-retrieval-practice/result.zh.json +942 -0
  107. package/examples/spaced-retrieval-practice/skeptic.json +70 -0
  108. package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
  109. package/examples/spaced-retrieval-practice/verdict.json +93 -0
  110. package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
  111. package/examples/workplace-ai-assistant/artifact_manifest.json +15 -0
  112. package/examples/workplace-ai-assistant/claims.jsonl +4 -4
  113. package/examples/workplace-ai-assistant/evidence.jsonl +4 -4
  114. package/examples/workplace-ai-assistant/evidence_graph.json +15 -15
  115. package/examples/workplace-ai-assistant/final_verdict.json +78 -0
  116. package/examples/workplace-ai-assistant/gate_report.json +101 -0
  117. package/examples/workplace-ai-assistant/report_spec.json +209 -40
  118. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +449 -119
  119. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +449 -119
  120. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +449 -119
  121. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +449 -119
  122. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +449 -119
  123. package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
  124. package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
  125. package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
  126. package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
  127. package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
  128. package/examples/workplace-ai-assistant/result.json +82 -20
  129. package/examples/workplace-ai-assistant/result.zh.json +82 -20
  130. package/examples/workplace-ai-assistant/skeptic.json +72 -0
  131. package/examples/workplace-ai-assistant/verdict.json +36 -10
  132. package/integrations/agent_mcp.py +2 -2
  133. package/integrations/jev/__init__.py +115 -0
  134. package/integrations/jev/approval.py +212 -0
  135. package/integrations/jev/cli.py +84 -0
  136. package/integrations/jev/config.py +112 -0
  137. package/integrations/jev/gateway.py +128 -0
  138. package/integrations/jev/modes.py +38 -0
  139. package/integrations/jev/tools_classify.py +88 -0
  140. package/integrations/jev/tools_extract.py +111 -0
  141. package/integrations/jev/tools_rerank.py +71 -0
  142. package/integrations/jev/tools_screen.py +87 -0
  143. package/integrations/jev/tools_verify.py +95 -0
  144. package/integrations/jev_mcp.py +22 -0
  145. package/integrations/semantic_decide.py +286 -0
  146. package/integrations/semdecide_cli.py +55 -0
  147. package/package.json +19 -2
  148. package/pyproject.toml +4 -3
  149. package/references/report-copy-style.md +107 -0
  150. package/references/retrieval-compliance.md +75 -0
  151. package/references/retrieval-protocol.md +20 -0
  152. package/retrieval/audit.py +27 -3
  153. package/retrieval/fetch.py +96 -0
  154. package/retrieval/sciverse.py +398 -0
  155. package/retrieval/search.py +47 -7
  156. package/schemas/applicability.schema.json +94 -0
  157. package/schemas/chart-spec.schema.json +10 -3
  158. package/schemas/evidence.schema.json +316 -43
  159. package/schemas/fetch-result.schema.json +2 -1
  160. package/schemas/report-result.schema.json +3 -3
  161. package/schemas/report-spec.schema.json +98 -100
  162. package/schemas/skeptic.schema.json +86 -0
  163. package/schemas/source.schema.json +21 -2
  164. package/schemas/v2/decision-snapshot.schema.json +20 -9
  165. package/schemas/v2/finding.schema.json +5 -1
  166. package/schemas/v2/intake.schema.json +191 -0
  167. package/schemas/v2/methodology-audit.schema.json +5 -1
  168. package/schemas/v2/outcome.schema.json +28 -5
  169. package/schemas/v2/study.schema.json +5 -1
  170. package/schemas/vNext/autoevolve-session.schema.json +34 -1
  171. package/schemas/vNext/eval-snapshot.schema.json +77 -1
  172. package/schemas/vNext/execution-plan.schema.json +50 -1
  173. package/schemas/vNext/gap-priority.schema.json +54 -1
  174. package/schemas/vNext/negative-search-record.schema.json +68 -1
  175. package/schemas/vNext/research-iteration.schema.json +87 -1
  176. package/schemas/vNext/research-strategy.schema.json +62 -1
  177. package/schemas/vNext/skill-experiment.schema.json +90 -1
  178. package/schemas/vNext/task-spec.schema.json +156 -1
  179. package/schemas/vNext/worker-result.schema.json +60 -1
  180. package/schemas/verdict.schema.json +164 -28
  181. package/scripts/build_evidence_library.py +15 -5
  182. package/scripts/build_report_variants.py +18 -2
  183. package/scripts/build_result.py +74 -9
  184. package/scripts/check_package_parity.py +85 -0
  185. package/scripts/check_protocol_alignment.py +375 -0
  186. package/scripts/check_versioned_schemas.py +254 -0
  187. package/scripts/claim_audit.py +13 -8
  188. package/scripts/compute_confidence.py +10 -0
  189. package/scripts/dashboard_server.py +13 -2
  190. package/scripts/did_regression.py +12 -2
  191. package/scripts/evidence_score.py +5 -2
  192. package/scripts/intake/__init__.py +31 -0
  193. package/scripts/intake/__main__.py +18 -0
  194. package/scripts/intake/background.py +78 -0
  195. package/scripts/intake/browser.py +79 -0
  196. package/scripts/intake/cli.py +57 -0
  197. package/scripts/intake/constants.py +57 -0
  198. package/scripts/intake/depth.py +53 -0
  199. package/scripts/intake/enhancements.py +106 -0
  200. package/scripts/intake/hooks.py +90 -0
  201. package/scripts/intake/prefs.py +76 -0
  202. package/scripts/intake/prompts.py +85 -0
  203. package/scripts/intake/session.py +152 -0
  204. package/scripts/lint_file_layers.py +126 -0
  205. package/scripts/orchestrator.py +187 -40
  206. package/scripts/pre_verdict_gate.py +241 -29
  207. package/scripts/quickstart.py +18 -2
  208. package/scripts/run_workspace.py +7 -1
  209. package/scripts/skill_lint.py +11 -1
  210. package/scripts/skill_payload.py +6 -3
  211. package/scripts/test_adversarial_empirical.py +96 -25
  212. package/scripts/validate_schema.py +31 -1
  213. package/skill/agents/evaluation-designer.md +20 -4
  214. package/skill/agents/evidence-analyst.md +19 -3
  215. package/skill/agents/evidence-judge.md +98 -8
  216. package/skill/agents/evidence-retriever.md +20 -3
  217. package/skill/agents/intervention-designer.md +20 -4
  218. package/skill/agents/method-reviewer.md +18 -2
  219. package/skill/agents/{education-planner.md → research-planner.md} +19 -3
  220. package/skill/agents/skeptic.md +18 -2
  221. package/skill/roles/registry.yaml +11 -11
  222. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
  223. package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
  224. package/skill/sub-skills/data-analysis/SKILL.md +34 -15
  225. package/skill/sub-skills/ethics-review/SKILL.md +33 -10
  226. package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
  227. package/skill/sub-skills/evidence-review/SKILL.md +31 -12
  228. package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
  229. package/skill/sub-skills/literature-review/SKILL.md +35 -14
  230. package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
  231. package/skill/sub-skills/report-generation/SKILL.md +28 -0
  232. package/skill/sub-skills/research-planning/SKILL.md +41 -14
  233. package/skill/sub-skills/study-design/SKILL.md +30 -9
  234. package/skill/task-briefs/adjudicate.md +32 -7
  235. package/skill/task-briefs/applicability.md +37 -2
  236. package/skill/task-briefs/audit.md +32 -7
  237. package/skill/task-briefs/challenge.md +34 -5
  238. package/skill/task-briefs/evaluate.md +30 -5
  239. package/skill/task-briefs/extract.md +31 -8
  240. package/skill/task-briefs/frame.md +39 -10
  241. package/skill/task-briefs/intervene.md +32 -6
  242. package/skill/task-briefs/present.md +32 -8
  243. package/skill/task-briefs/projection.md +36 -2
  244. package/skill/task-briefs/retrieve.md +36 -6
  245. package/skill/workflows/decision-and-pilot.md +76 -1
  246. package/skill/workflows/evaluate-and-update.md +83 -0
  247. package/skill/workflows/evidence-review.md +104 -0
  248. package/skill/workflows/experimental-jev.md +170 -0
  249. package/skill/workflows/intake.md +120 -0
  250. package/visualization/eduevidence-report/scripts/build_figures.py +25 -3
  251. package/visualization/eduevidence-report/scripts/build_infographics.py +37 -15
  252. package/visualization/eduevidence-report/scripts/build_report.py +435 -575
  253. package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
  254. package/visualization/eduevidence-report/scripts/lieflat_engine.py +349 -38
  255. package/visualization/eduevidence-report/scripts/report_copy_pack.py +296 -0
  256. package/visualization/eduevidence-report/scripts/report_copy_policy_guard.py +47 -0
  257. package/visualization/eduevidence-report/scripts/zh_labels.py +141 -1
  258. package/web/architecture.html +14885 -0
  259. package/web/studio/assets/index-B8tkF44Q.css +1 -0
  260. package/web/studio/index.html +2 -2
  261. package/scripts/build_esl_artifacts.py +0 -1921
  262. package/scripts/build_killer_demo.py +0 -295
  263. package/scripts/enrich_projects_human_and_lieflat.py +0 -315
  264. package/scripts/generate_new_projects.py +0 -686
  265. package/scripts/sync_killer_demo_report.py +0 -270
  266. package/web/studio/assets/index-CzXocaGv.css +0 -1
  267. /package/web/studio/assets/{index-pa7jD7n4.js → index-CQ6Keoyc.js} +0 -0
@@ -19,20 +19,32 @@ class CapabilitySpec:
19
19
  deterministic_local: bool
20
20
  scientific_gate: str | None
21
21
  workflow_ids: tuple[str, ...] = ()
22
+ experimental: bool = False
22
23
 
23
24
 
24
25
  _REGISTRY: dict[str, CapabilitySpec] = {}
25
26
 
27
+ #: Jev / SemDecide Tier-0 acceleration IDs (opt-in via --experimental).
28
+ #: Not role-owned scientific work: omitted from capability_registry() by
29
+ #: default so the protocol role-ownership gate stays unchanged. Pass
30
+ #: include_experimental=True when wiring skill/workflows/experimental-jev.md.
31
+ EXPERIMENTAL_CAPABILITY_IDS: frozenset[str] = frozenset({
32
+ "content_screen", "semantic_rerank", "field_extract",
33
+ "classify_check", "claim_verify",
34
+ })
35
+
26
36
 
27
37
  def _register(capability_id: str, *, input_contracts: tuple[str, ...],
28
38
  output_contracts: tuple[str, ...], deterministic_local: bool,
29
- scientific_gate: str | None = None) -> None:
39
+ scientific_gate: str | None = None,
40
+ experimental: bool = False) -> None:
30
41
  _REGISTRY[capability_id] = CapabilitySpec(
31
42
  capability_id=capability_id,
32
43
  input_contracts=input_contracts,
33
44
  output_contracts=output_contracts,
34
45
  deterministic_local=deterministic_local,
35
46
  scientific_gate=scientific_gate,
47
+ experimental=experimental,
36
48
  )
37
49
 
38
50
 
@@ -90,10 +102,50 @@ _register("intervention_design", input_contracts=("decision-snapshot",),
90
102
  _register("evaluation_design", input_contracts=("decision-snapshot",),
91
103
  output_contracts=("evaluation",), deterministic_local=False)
92
104
 
93
-
94
- def capability_registry() -> dict[str, CapabilitySpec]:
95
- """Return the frozen capability registry."""
96
- return dict(_REGISTRY)
105
+ # Jev experimental Tier-0 (opt-in --experimental; mode 1/2/3).
106
+ # Acceleration only — never replaces Skeptic, pre_verdict_gate, methodology
107
+ # audit, or tribunal. Uncertain / invalid answers fail closed to the large model.
108
+ _register("content_screen", input_contracts=("fetch-result",),
109
+ output_contracts=("screen-verdict",), deterministic_local=False,
110
+ scientific_gate="experimental Tier-0; advisory screen before context; "
111
+ "RULE 2 snippet != evidence",
112
+ experimental=True)
113
+ _register("semantic_rerank", input_contracts=("source",),
114
+ output_contracts=("ranked-candidates",), deterministic_local=False,
115
+ scientific_gate="experimental Tier-0; ranking is discovery order only",
116
+ experimental=True)
117
+ _register("field_extract", input_contracts=("source",),
118
+ output_contracts=("field-values",), deterministic_local=False,
119
+ scientific_gate="experimental Tier-0; verbatim substring values only, "
120
+ "never model-written fields",
121
+ experimental=True)
122
+ _register("classify_check", input_contracts=("source",),
123
+ output_contracts=("classification",), deterministic_local=False,
124
+ scientific_gate="experimental Tier-0; labels never replace "
125
+ "methodology_appraisal or outcome separation",
126
+ experimental=True)
127
+ _register("claim_verify", input_contracts=("claim", "source"),
128
+ output_contracts=("claim-verdict",), deterministic_local=False,
129
+ scientific_gate="experimental Tier-0; does NOT replace Skeptic nine "
130
+ "checks or Pre-Verdict Gate",
131
+ experimental=True)
132
+
133
+
134
+ def capability_registry(*, include_experimental: bool = False) -> dict[str, CapabilitySpec]:
135
+ """Return the capability registry.
136
+
137
+ Experimental Jev Tier-0 IDs are omitted by default (they are acceleration
138
+ helpers, not role-owned scientific stages). Opt in with
139
+ ``include_experimental=True`` when resolving --experimental modes 1/2/3.
140
+ """
141
+ if include_experimental:
142
+ return dict(_REGISTRY)
143
+ return {cid: spec for cid, spec in _REGISTRY.items() if not spec.experimental}
144
+
145
+
146
+ def experimental_capability_registry() -> dict[str, CapabilitySpec]:
147
+ """Return only the Jev/SemDecide Tier-0 experimental capabilities."""
148
+ return {cid: spec for cid, spec in _REGISTRY.items() if spec.experimental}
97
149
 
98
150
 
99
151
  def capability(capability_id: str) -> CapabilitySpec | None:
@@ -0,0 +1,167 @@
1
+ """Single source of truth for the four-state decision gate.
2
+
3
+ The four-state decision (ADOPT / PILOT / REJECT / INSUFFICIENT EVIDENCE) is
4
+ computed by two layers that must never drift apart:
5
+
6
+ * engine/tribunal.py - V2 Evidence Graph adjudication
7
+ * scripts/pre_verdict_gate.py - V1 run/example-pack gate enforcement
8
+
9
+ Before this module existed the rule lived only inside the tribunal, so the V1
10
+ gate could not enforce it and a hand-written verdict could carry any action it
11
+ liked. Both layers now resolve the 'which outcome categories count for this
12
+ domain' question here, and the V1 gate additionally re-derives direct-evidence
13
+ presence from the pack's own evidence records.
14
+
15
+ Decision matrix (gate-enforced):
16
+
17
+ conflict/mixed/support+oppose -> INSUFFICIENT_EVIDENCE (unresolved_conflict)
18
+ oppose_adoption (oppose-only) -> REJECT
19
+ High + support + direct_primary -> ADOPT
20
+ High + support + missing direct -> PILOT (downgrade_reason=missing_direct_primary)
21
+ Moderate+ support (+/- direct) -> PILOT (confidence_band | missing_direct_primary)
22
+ Low / 无支持 / 间接无 Moderate -> INSUFFICIENT_EVIDENCE
23
+
24
+ Stdlib only, consistent with the "Native Core" policy of engine/.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ #: Category buckets that count as a domain's PRIMARY effect for the ADOPT gate.
29
+ #: The gate asks 'is there direct evidence on the outcome this decision is
30
+ #: actually about?' - for education that is a learning outcome (task
31
+ #: performance and process measures never qualify); for policy it is the
32
+ #: policy-effectiveness / cost class. Every entry must name a category the
33
+ #: domain registry declares, which check_protocol_alignment.py enforces.
34
+ PRIMARY_EFFECT_CATEGORIES: dict[str, tuple[str, ...]] = {
35
+ "education": ("learning",),
36
+ "policy": ("effectiveness", "cost"),
37
+ }
38
+
39
+ #: Directness (0-2) at which an evidence link may carry an ADOPT claim.
40
+ ADOPT_DIRECTNESS = 2
41
+
42
+ #: Confidence band required before ADOPT is possible at all.
43
+ ADOPT_REQUIRED_LABEL = "High"
44
+
45
+ #: Confidence band that can still reach PILOT given a verifiable primary path.
46
+ PILOT_REQUIRED_LABEL = "Moderate"
47
+
48
+ #: Auditable reasons a decision was stepped down from a higher action.
49
+ DOWNGRADE_MISSING_DIRECT_PRIMARY = "missing_direct_primary"
50
+ DOWNGRADE_CONFIDENCE_BAND = "confidence_band"
51
+ DOWNGRADE_GATE_CRITICAL = "gate_critical"
52
+ DOWNGRADE_UNRESOLVED_CONFLICT = "unresolved_conflict"
53
+
54
+ #: Relation values that mark unresolved conflict rather than a clean vote.
55
+ CONFLICT_RELATIONS = frozenset({"conflict", "mixed"})
56
+
57
+
58
+ def primary_effect_categories(domain: str) -> tuple[str, ...]:
59
+ """Categories that satisfy the ADOPT direct-evidence gate for a domain."""
60
+ from engine.taxonomy import categories as taxonomy_categories
61
+
62
+ declared = PRIMARY_EFFECT_CATEGORIES.get(domain)
63
+ if declared:
64
+ known = taxonomy_categories(domain)
65
+ missing = [c for c in declared if c not in known]
66
+ if missing:
67
+ raise ValueError(
68
+ 'domain ' + repr(domain) + ' ADOPT gate references undeclared '
69
+ 'categories ' + repr(missing) + '; declared: ' + repr(sorted(known)))
70
+ return declared
71
+ known = taxonomy_categories(domain)
72
+ if not known:
73
+ raise ValueError('domain ' + repr(domain) + ' declares no outcome categories')
74
+ return (next(iter(known)),)
75
+
76
+
77
+ def outcome_category(domain: str, value: str, primary: tuple[str, ...]) -> str | None:
78
+ """Resolve an outcome value to its category, or None when unresolvable.
79
+
80
+ The outcomes table stores CATEGORY buckets (learning / task_performance /
81
+ process / risk, plus each domain's own buckets), while V1 packs store raw
82
+ taxonomy tokens. Accept a category directly and, for a token, resolve it
83
+ through the registry. An unknown value returns None so callers fail
84
+ closed instead of silently treating it as decision-grade evidence.
85
+ """
86
+ from engine.taxonomy import TaxonomyError, category_of
87
+
88
+ if value in primary:
89
+ return value
90
+ try:
91
+ return category_of(domain, value)
92
+ except (TaxonomyError, ValueError):
93
+ return None
94
+
95
+
96
+ def decision_outcome(*, confidence_label: str, decisive_relations: dict[str, str],
97
+ has_direct_primary_evidence: bool) -> dict[str, str | None]:
98
+ """Gate-enforced decision action plus auditable downgrade_reason.
99
+
100
+ Returns ``{"action": <uppercase four-state>, "downgrade_reason": <str|None>}``.
101
+
102
+ REJECT requires usable direct opposition evidence and no decisive support
103
+ (oppose-only). Mixed support+oppose or any conflict/conditional/mixed
104
+ relation is an unresolved conflict → INSUFFICIENT_EVIDENCE (never ADOPT,
105
+ never a bare REJECT). ADOPT requires High confidence, decisive support,
106
+ AND direct evidence on the domain's PRIMARY outcome category. PILOT is
107
+ the bounded-trial exit: High + support missing that direct primary
108
+ evidence (``missing_direct_primary``), or Moderate + support on a
109
+ verifiable primary-outcome path (``confidence_band``).
110
+ """
111
+ has_oppose = any(r == "oppose_adoption" for r in decisive_relations.values())
112
+ has_support = any(r == "support_adoption" for r in decisive_relations.values())
113
+ has_conflict = any(r in CONFLICT_RELATIONS for r in decisive_relations.values())
114
+
115
+ # Unresolved conflict (mixed votes or explicit conflict tags) fails closed
116
+ # before any ADOPT/PILOT/REJECT claim.
117
+ if has_conflict or (has_oppose and has_support):
118
+ return {"action": "INSUFFICIENT_EVIDENCE",
119
+ "downgrade_reason": DOWNGRADE_UNRESOLVED_CONFLICT}
120
+
121
+ if has_oppose:
122
+ return {"action": "REJECT", "downgrade_reason": None}
123
+
124
+ if (confidence_label == ADOPT_REQUIRED_LABEL and has_support
125
+ and has_direct_primary_evidence):
126
+ return {"action": "ADOPT", "downgrade_reason": None}
127
+
128
+ if confidence_label == ADOPT_REQUIRED_LABEL and has_support:
129
+ return {"action": "PILOT", "downgrade_reason": DOWNGRADE_MISSING_DIRECT_PRIMARY}
130
+
131
+ # Bounded-trial exit: Moderate + support reaches PILOT even without a
132
+ # verified primary path (Gate C allows PILOT / INSUFFICIENT / REJECT).
133
+ if confidence_label == PILOT_REQUIRED_LABEL and has_support:
134
+ reason = (None if has_direct_primary_evidence
135
+ else DOWNGRADE_MISSING_DIRECT_PRIMARY)
136
+ if has_direct_primary_evidence:
137
+ reason = DOWNGRADE_CONFIDENCE_BAND
138
+ return {"action": "PILOT", "downgrade_reason": reason}
139
+
140
+ # Low / 间接 (Moderate without a verifiable primary path) -> INSUFFICIENT.
141
+ if confidence_label in ("Low", "Insufficient"):
142
+ return {"action": "INSUFFICIENT_EVIDENCE",
143
+ "downgrade_reason": DOWNGRADE_CONFIDENCE_BAND}
144
+ if confidence_label == PILOT_REQUIRED_LABEL and has_support:
145
+ return {"action": "INSUFFICIENT_EVIDENCE",
146
+ "downgrade_reason": DOWNGRADE_MISSING_DIRECT_PRIMARY}
147
+ return {"action": "INSUFFICIENT_EVIDENCE", "downgrade_reason": None}
148
+
149
+
150
+ def decision_action(*, confidence_label: str, decisive_relations: dict[str, str],
151
+ has_direct_primary_evidence: bool) -> str:
152
+ """Gate-enforced decision action (uppercase four-state)."""
153
+ return decision_outcome(
154
+ confidence_label=confidence_label,
155
+ decisive_relations=decisive_relations,
156
+ has_direct_primary_evidence=has_direct_primary_evidence,
157
+ )["action"]
158
+
159
+
160
+ def downgrade_reason(*, confidence_label: str, decisive_relations: dict[str, str],
161
+ has_direct_primary_evidence: bool) -> str | None:
162
+ """Auditable reason the action was stepped down (or None when not downgraded)."""
163
+ return decision_outcome(
164
+ confidence_label=confidence_label,
165
+ decisive_relations=decisive_relations,
166
+ has_direct_primary_evidence=has_direct_primary_evidence,
167
+ )["downgrade_reason"]
@@ -54,7 +54,11 @@ class EvidenceNode:
54
54
  outcome_dimension: str = OutcomeDimension.GENERAL_MEASURE
55
55
  claim_id: Optional[str] = None
56
56
  outcome_id: Optional[str] = None
57
- effect_size: Dict[str, Any] = field(default_factory=lambda: {"metric": "Hedges g", "value": 0.0, "ci_lower": 0.0, "ci_upper": 0.0, "p_value": 0.05})
57
+ # A missing effect must stay missing: the old default fabricated a
58
+ # g = 0.0 with a p = 0.05, which would serialise as a real (and
59
+ # false) "no effect" result. Callers that need a number must supply
60
+ # one; the extractors already suppress charts when it is absent.
61
+ effect_size: Optional[Dict[str, Any]] = None
58
62
  sample_size: int = 0
59
63
  sample_description: str = ""
60
64
  study_design: str = "Quasi-Experimental" # RCT, Quasi-Experimental DID, Meta-Analysis, Observational
@@ -292,9 +296,9 @@ class EvidenceGraph:
292
296
  for ev in self.evidence.values():
293
297
  paper = self.papers.get(ev.paper_id)
294
298
  study_label = f"{paper.authors[0] if paper and paper.authors else ev.paper_id} ({paper.year if paper else ''})"
295
- effect_val = ev.effect_size.get("value", 0.0)
296
- ci_l = ev.effect_size.get("ci_lower")
297
- ci_u = ev.effect_size.get("ci_upper")
299
+ effect_val = (ev.effect_size or {}).get("value") or 0.0
300
+ ci_l = (ev.effect_size or {}).get("ci_lower")
301
+ ci_u = (ev.effect_size or {}).get("ci_upper")
298
302
  has_ci = ci_l is not None and ci_u is not None and float(ci_u) >= float(ci_l)
299
303
  points.append({
300
304
  "evidence_id": ev.evidence_id,
@@ -327,13 +331,13 @@ class EvidenceGraph:
327
331
  precision_counts = {"reported_ci": 0, "derived_from_sample_size": 0}
328
332
  excluded_no_precision = 0
329
333
  for n in nodes:
330
- eff = n.effect_size.get("value")
334
+ eff = (n.effect_size or {}).get("value")
331
335
  if eff is None or math.isnan(float(eff)) or math.isinf(float(eff)):
332
336
  continue
333
337
 
334
338
  # Statistical variance derivation (Borenstein et al. 2009)
335
- ci_l = n.effect_size.get("ci_lower")
336
- ci_u = n.effect_size.get("ci_upper")
339
+ ci_l = (n.effect_size or {}).get("ci_lower")
340
+ ci_u = (n.effect_size or {}).get("ci_upper")
337
341
  if ci_l is not None and ci_u is not None and float(ci_u) > float(ci_l):
338
342
  se = (float(ci_u) - float(ci_l)) / (2.0 * 1.95996)
339
343
  precision_counts["reported_ci"] += 1
@@ -422,7 +426,7 @@ class EvidenceGraph:
422
426
  "quote": p.summary,
423
427
  })
424
428
  for ev in self.evidence.values():
425
- effect_val = ev.effect_size.get("value", 0.0)
429
+ effect_val = (ev.effect_size or {}).get("value") or 0.0
426
430
  symbol_size = max(18, min(45, int(18 + abs(effect_val) * 20)))
427
431
  nodes.append({
428
432
  "id": ev.evidence_id,
@@ -433,8 +437,8 @@ class EvidenceGraph:
433
437
  "dimension": ev.outcome_dimension,
434
438
  "direction": ev.direction,
435
439
  "effect_size": effect_val,
436
- "ci_lower": ev.effect_size.get("ci_lower", "N/A"),
437
- "ci_upper": ev.effect_size.get("ci_upper", "N/A"),
440
+ "ci_lower": (ev.effect_size or {}).get("ci_lower", "N/A"),
441
+ "ci_upper": (ev.effect_size or {}).get("ci_upper", "N/A"),
438
442
  "sample_size": ev.sample_size,
439
443
  "wwc_rating": ev.wwc_rating,
440
444
  "quote": ev.key_quote,
package/engine/gaps.py CHANGED
@@ -21,10 +21,32 @@ from engine.graph_store import GraphStore
21
21
  from engine.ids import new_local_id
22
22
  from engine.synthesis import ClaimSynthesis
23
23
 
24
- _RETENTION_TYPES = {"retention", "long_term", "learning_retention"}
25
- _TRANSFER_TYPES = {"transfer", "transfer_learning", "far_transfer"}
26
- _TASK_PERFORMANCE = {"task_performance", "assignment_score", "task_completion"}
27
- _LEARNING = {"learning"}
24
+ #: Gap kind -> the outcome categories that count as covering it. Categories are
25
+ #: read from the domain registry, so a policy run classifies its outcomes with
26
+ #: the policy buckets (effectiveness / cost / equity / feasibility / risk)
27
+ #: instead of being measured against education vocabulary.
28
+ GAP_KIND_CATEGORIES: dict[str, tuple[str, ...]] = {
29
+ "learning": ("learning",),
30
+ "retention": ("learning",),
31
+ "transfer": ("learning",),
32
+ "task_performance": ("task_performance",),
33
+ "process": ("process",),
34
+ "risk": ("risk",),
35
+ "effectiveness": ("effectiveness",),
36
+ "cost": ("cost",),
37
+ "equity": ("equity",),
38
+ "feasibility": ("feasibility",),
39
+ }
40
+
41
+ #: Legacy aliases accepted when a frame names its requested outcomes with older
42
+ #: vocabulary; they resolve to a gap kind above.
43
+ GAP_KIND_ALIASES: dict[str, str] = {
44
+ "long_term": "retention", "learning_retention": "retention",
45
+ "transfer_learning": "transfer", "far_transfer": "transfer",
46
+ "assignment_score": "task_performance", "task_completion": "task_performance",
47
+ "policy_effectiveness": "effectiveness", "cost_effectiveness": "cost",
48
+ "implementation_risk": "risk",
49
+ }
28
50
 
29
51
 
30
52
  def _autoresearch_key(
@@ -97,26 +119,24 @@ def derive_gaps(*, store: GraphStore,
97
119
  req_type = str(req.get("outcome_type", "")).lower()
98
120
  else:
99
121
  req_name, req_type = str(req).lower(), ""
100
- if req_type in _RETENTION_TYPES or req_name in _RETENTION_TYPES:
101
- return "retention", req.get("name", "") if isinstance(req, dict) else str(req)
102
- if req_type in _TRANSFER_TYPES or req_name in _TRANSFER_TYPES:
103
- return "transfer", req.get("name", "") if isinstance(req, dict) else str(req)
104
- if req_type in _TASK_PERFORMANCE or req_name in _TASK_PERFORMANCE:
105
- return "task_performance", req.get("name", "") if isinstance(req, dict) else str(req)
106
- if req_type in _LEARNING or req_name in _LEARNING:
107
- return "learning", req.get("name", "") if isinstance(req, dict) else str(req)
108
- return "other", req.get("name", "") if isinstance(req, dict) else str(req)
122
+ label = req.get("name", "") if isinstance(req, dict) else str(req)
123
+ kind = GAP_KIND_ALIASES.get(req_type) or GAP_KIND_ALIASES.get(req_name)
124
+ if kind is None:
125
+ kind = req_type if req_type in GAP_KIND_CATEGORIES else req_name
126
+ if kind in GAP_KIND_CATEGORIES:
127
+ return kind, label
128
+ return "other", label
109
129
 
110
130
  def covered_for_kind(kind: str) -> bool:
111
- if kind == "retention":
112
- return bool(covered_types & _RETENTION_TYPES)
113
- if kind == "transfer":
114
- return bool(covered_types & _TRANSFER_TYPES)
115
- if kind == "task_performance":
116
- return bool(covered_types & _TASK_PERFORMANCE)
117
- if kind == "learning":
118
- return bool(covered_types & _LEARNING)
119
- return False
131
+ """True when the graph already carries an outcome for this gap kind.
132
+
133
+ ``covered_types`` holds the category buckets stored on outcomes, which
134
+ is why this compares categories rather than V1 tokens.
135
+ """
136
+ expected = GAP_KIND_CATEGORIES.get(kind)
137
+ if not expected:
138
+ return False
139
+ return bool(covered_types & set(expected))
120
140
 
121
141
  seen: set[tuple[str, str]] = set()
122
142
  for req in requested:
package/engine/ids.py CHANGED
@@ -17,6 +17,8 @@ keep stable Project-local IDs across edits; version/revision metadata records
17
17
  change instead of silently re-identifying.
18
18
  """
19
19
 
20
+ from __future__ import annotations
21
+
20
22
  from datetime import datetime, timezone
21
23
  import hashlib
22
24
  import secrets
package/engine/library.py CHANGED
@@ -8,6 +8,8 @@ changes an existing Project's conclusions — only an explicit import/sync
8
8
  advances the Project graph.
9
9
  """
10
10
 
11
+ from __future__ import annotations
12
+
11
13
  import hashlib
12
14
  import json
13
15
  import os
@@ -197,8 +199,10 @@ class ResearchLibrary:
197
199
  outcomes.append({
198
200
  "outcome_id": oid,
199
201
  "name": f.get("measure", oid),
200
- "outcome_type": (f.get("extensions") or {}).get(
201
- "outcome_type", "learning"),
202
+ # No silent default: an unlabelled finding must not be filed as a
203
+ # learning outcome; that is how task-performance evidence used
204
+ # to reach the ADOPT gate on the V2 path.
205
+ "outcome_type": (f.get("extensions") or {}).get("outcome_type", ""),
202
206
  "extensions": {
203
207
  "auto_created_from_library_import": True,
204
208
  "library_revision": lib_rev,
@@ -24,7 +24,8 @@ Conservative verdict rules (offline preliminary gate):
24
24
  any matched contradict entry -> reject
25
25
  else any matched support entry -> pilot
26
26
  else -> insufficient_evidence
27
- adopt is NEVER returned by the preliminary gate.
27
+ The preliminary gate is a screening bound only; a full ADOPT still requires
28
+ the deterministic decision_action gate (High + support + direct primary).
28
29
 
29
30
  Output:
30
31
  {"verdict": ..., "coverage": {"matched_entries": [...],
@@ -228,8 +229,9 @@ def preliminary_verdict(question: str, *, top_k: int = 10) -> dict[str, Any]:
228
229
  claim_text + effect_summary + title; the top_k entries are considered and an
229
230
  entry counts as matched when overlap >= MATCH_THRESHOLD and it shares at
230
231
  least MIN_SHARED_BIGRAMS tokens. Verdict: contradict => reject, else
231
- support => pilot, else insufficient_evidence. Never adopt. Never crashes on
232
- empty/blank questions.
232
+ support => pilot, else insufficient_evidence. This is a screening bound
233
+ only; full ADOPT is decided by engine.decision_policy.decision_action.
234
+ Never crashes on empty/blank questions.
233
235
  """
234
236
  try:
235
237
  top_k = int(top_k)
@@ -298,6 +300,7 @@ def _build_note(
298
300
  f"离线初步裁决在 top_k={top_k} 内匹配到 {len(matched)} 条内置证据:"
299
301
  f"support={counts['support']}、contradict={counts['contradict']}、"
300
302
  f"neutral={counts['neutral']};匹配结局词:{outcome_str}。"
301
- "本裁决为初步(preliminary=true)且保守,从不直接给出 adopt,"
303
+ "本裁决为初步(preliminary=true)筛查上界;完整四态裁决仍须经 "
304
+ "decision_action 闸门(High + support + 主结果直接证据才可 adopt)。"
302
305
  "建议结合完整证据库与在线检索复核。"
303
306
  )
package/engine/living.py CHANGED
@@ -227,6 +227,26 @@ def set_subscription_status(project: ProjectWorkspace, subscription_id: str,
227
227
  return subscription
228
228
 
229
229
 
230
+ def _validated_outcome_type(value, domain: str) -> str:
231
+ """Outcome token from a refresh payload, validated against the registry.
232
+
233
+ A missing or unknown token raises: the living path used to default to
234
+ a learning outcome, which silently promoted task-performance evidence.
235
+ """
236
+ from engine.taxonomy import category_of, tokens as taxonomy_tokens
237
+
238
+ token = str(value or "").strip()
239
+ if not token:
240
+ raise ValueError(
241
+ "living evidence record must declare outcome_type; the engine "
242
+ "will not guess a category")
243
+ if token not in taxonomy_tokens(domain):
244
+ raise ValueError(
245
+ f"outcome_type {token!r} is not registered for domain {domain!r}")
246
+ category_of(domain, token) # fail closed on a malformed taxonomy
247
+ return token
248
+
249
+
230
250
  def refresh(project: ProjectWorkspace, subscription_id: str, *,
231
251
  new_evidence: list[dict] | None = None,
232
252
  retriever: Callable[[dict], list[dict]] | None = None) -> dict:
@@ -299,8 +319,10 @@ def refresh(project: ProjectWorkspace, subscription_id: str, *,
299
319
 
300
320
  # ---- normalize + validate fresh evidence ---------------------------
301
321
  next_revision = store.active_revision() + 1
322
+ domain = str((project.manifest() or {}).get("domain") or "education")
302
323
  mutation, new_hashes, finding_ids, summary_parts = _build_mutation(
303
- project, store, subscription, fresh_packets, claims, next_revision)
324
+ project, store, subscription, fresh_packets, claims, next_revision,
325
+ domain=domain)
304
326
 
305
327
  revision = store.commit(
306
328
  run_id=new_run_id(),
@@ -370,7 +392,7 @@ def _existing_drift_ids(project: ProjectWorkspace) -> set[str]:
370
392
 
371
393
 
372
394
  def _build_mutation(project, store, subscription, fresh_packets, claims,
373
- next_revision) -> tuple[GraphMutation, set[str], set[str], list[str]]:
395
+ next_revision, domain: str = "education") -> tuple[GraphMutation, set[str], set[str], list[str]]:
374
396
  """Normalize + validate each fresh record and assemble one GraphMutation."""
375
397
  existing = {t: {row[_ID_KEY[t]] for row in store.read_table(t)}
376
398
  for t in _GRAPH_TABLES}
@@ -418,6 +440,8 @@ def _build_mutation(project, store, subscription, fresh_packets, claims,
418
440
  source_upserted = True
419
441
  upserts["sources"].append(src)
420
442
 
443
+ # Outcome tokens are validated against the project domain registry
444
+ # rather than defaulted; see _validated_outcome_type().
421
445
  # --- outcome (optional; reused when the id already exists) -------
422
446
  outcome_upserted = False
423
447
  outcome = None
@@ -439,7 +463,7 @@ def _build_mutation(project, store, subscription, fresh_packets, claims,
439
463
  outcome = {
440
464
  "outcome_id": out_id,
441
465
  "name": outcome_pkt.get("name") or out_id[len("OUT-"):],
442
- "outcome_type": outcome_pkt.get("outcome_type", "learning"),
466
+ "outcome_type": _validated_outcome_type(outcome_pkt.get("outcome_type"), domain),
443
467
  "extensions": outcome_pkt.get("extensions") or {},
444
468
  }
445
469
  existing["outcomes"].add(out_id)
@@ -503,7 +527,13 @@ def _build_mutation(project, store, subscription, fresh_packets, claims,
503
527
  f"{label}: relation_to_claim must be one of "
504
528
  f"{sorted(_RELATION_TO_IMPLICATION)}, got {relation!r}")
505
529
  link.setdefault("decision_implication", _RELATION_TO_IMPLICATION[relation])
506
- link.setdefault("directness", 2)
530
+ # directness decides whether this link can carry an ADOPT claim.
531
+ # Defaulting it to 2 (direct) would let an unclassified refresh
532
+ # assert direct evidence it never established, so it is required.
533
+ if "directness" not in link:
534
+ raise ValueError(
535
+ f"{label}: directness is required for a living-evidence link "
536
+ "(2 = direct evidence for the claim; supply it explicitly)")
507
537
  link.setdefault("applicability", {"scope_match": "direct"})
508
538
  link.setdefault("reasoning_note",
509
539
  f"living evidence refresh: {subscription['subscription_id']}")