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.
- package/CHANGELOG.md +395 -0
- package/CONTRIBUTING.md +105 -0
- package/README.md +113 -49
- package/README.zh-CN.md +39 -12
- package/SKILL.md +15 -5
- package/assets/readme/landing-tour.gif +0 -0
- package/assets/readme/studio-tour.gif +0 -0
- package/benchmarks/evidence-library.json +277 -1
- package/bin/eduevidence.js +2 -1
- package/docs/architecture.md +325 -46
- package/docs/demo-workplace-ai.md +1 -1
- package/docs/install-guide.md +1 -1
- package/docs/j-ev-experimental.md +250 -0
- package/docs/orchestration-role-model.md +1 -1
- package/docs/release-closeout/README.md +1 -1
- package/docs/reproducibility.md +138 -0
- package/docs/sciverse-api.md +125 -0
- package/domains/_neutral/copy/few_shots.json +21 -0
- package/domains/_neutral/copy/framing_lexicon.json +19 -0
- package/domains/_neutral/copy/module_labels.json +5 -0
- package/domains/_neutral/copy/module_labels_footer.json +102 -0
- package/domains/_neutral/copy/module_labels_modules.json +204 -0
- package/domains/_neutral/copy/module_labels_nav.json +126 -0
- package/domains/_neutral/copy/module_labels_summary.json +98 -0
- package/domains/_neutral/copy/module_labels_tables.json +164 -0
- package/domains/_neutral/copy/module_labels_v2.json +90 -0
- package/domains/_neutral/copy/risk_constructs.json +20 -0
- package/domains/_neutral/copy/section_titles.json +66 -0
- package/domains/_neutral/copy/terminology.json +11 -0
- package/domains/check_copy_packs.py +103 -0
- package/domains/education/copy/few_shots.json +22 -0
- package/domains/education/copy/framing_enums.json +167 -0
- package/domains/education/copy/framing_lexicon.json +166 -0
- package/domains/education/copy/module_labels.json +169 -0
- package/domains/education/copy/risk_constructs.json +48 -0
- package/domains/education/copy/section_titles.json +186 -0
- package/domains/education/copy/terminology.json +70 -0
- package/domains/education/manifest.json +1 -1
- package/domains/education/outcome_taxonomy.json +2 -2
- package/domains/manifest.json +1 -1
- package/domains/policy/copy/few_shots.json +22 -0
- package/domains/policy/copy/framing_enums.json +94 -0
- package/domains/policy/copy/framing_lexicon.json +174 -0
- package/domains/policy/copy/module_labels.json +168 -0
- package/domains/policy/copy/risk_constructs.json +33 -0
- package/domains/policy/copy/section_titles.json +186 -0
- package/domains/policy/copy/terminology.json +64 -0
- package/eduevidence_cli.py +10 -0
- package/engine/capabilities.py +57 -5
- package/engine/decision_policy.py +167 -0
- package/engine/evidence_graph.py +14 -10
- package/engine/gaps.py +42 -22
- package/engine/ids.py +2 -0
- package/engine/library.py +6 -2
- package/engine/library_builtin.py +7 -4
- package/engine/living.py +34 -4
- package/engine/migration.py +88 -3
- package/engine/orchestration.py +5 -5
- package/engine/paths.py +2 -0
- package/engine/pilot.py +34 -32
- package/engine/taxonomy.py +211 -0
- package/engine/tribunal.py +49 -43
- package/engine/versions.py +1 -1
- package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +1361 -147
- package/examples/ai-coding-assistant-evidence/artifact_manifest.json +3 -3
- package/examples/ai-coding-assistant-evidence/citation_check.json +1 -1
- package/examples/ai-coding-assistant-evidence/final_verdict.json +107 -0
- package/examples/ai-coding-assistant-evidence/gate_report.json +101 -0
- package/examples/ai-coding-assistant-evidence/report_spec.json +23 -12
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +448 -128
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +1360 -146
- package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +1360 -146
- package/examples/ai-coding-assistant-evidence/result.json +13 -9
- package/examples/ai-coding-assistant-evidence/result.zh.json +45 -41
- package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
- package/examples/ai-coding-assistant-evidence/verdict.json +6 -2
- package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
- package/examples/spaced-retrieval-practice/applicability.json +14 -0
- package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
- package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
- package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
- package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
- package/examples/spaced-retrieval-practice/frame.json +58 -0
- package/examples/spaced-retrieval-practice/gate_report.json +101 -0
- package/examples/spaced-retrieval-practice/methodology.json +78 -0
- package/examples/spaced-retrieval-practice/report.html +2522 -0
- package/examples/spaced-retrieval-practice/report_spec.json +212 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
- package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
- package/examples/spaced-retrieval-practice/result.json +942 -0
- package/examples/spaced-retrieval-practice/result.zh.json +942 -0
- package/examples/spaced-retrieval-practice/skeptic.json +70 -0
- package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
- package/examples/spaced-retrieval-practice/verdict.json +93 -0
- package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
- package/examples/workplace-ai-assistant/artifact_manifest.json +15 -0
- package/examples/workplace-ai-assistant/claims.jsonl +4 -4
- package/examples/workplace-ai-assistant/evidence.jsonl +4 -4
- package/examples/workplace-ai-assistant/evidence_graph.json +15 -15
- package/examples/workplace-ai-assistant/final_verdict.json +78 -0
- package/examples/workplace-ai-assistant/gate_report.json +101 -0
- package/examples/workplace-ai-assistant/report_spec.json +209 -40
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +449 -119
- package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
- package/examples/workplace-ai-assistant/result.json +82 -20
- package/examples/workplace-ai-assistant/result.zh.json +82 -20
- package/examples/workplace-ai-assistant/skeptic.json +72 -0
- package/examples/workplace-ai-assistant/verdict.json +36 -10
- package/integrations/agent_mcp.py +2 -2
- package/integrations/jev/__init__.py +115 -0
- package/integrations/jev/approval.py +212 -0
- package/integrations/jev/cli.py +84 -0
- package/integrations/jev/config.py +112 -0
- package/integrations/jev/gateway.py +128 -0
- package/integrations/jev/modes.py +38 -0
- package/integrations/jev/tools_classify.py +88 -0
- package/integrations/jev/tools_extract.py +111 -0
- package/integrations/jev/tools_rerank.py +71 -0
- package/integrations/jev/tools_screen.py +87 -0
- package/integrations/jev/tools_verify.py +95 -0
- package/integrations/jev_mcp.py +22 -0
- package/integrations/semantic_decide.py +286 -0
- package/integrations/semdecide_cli.py +55 -0
- package/package.json +19 -2
- package/pyproject.toml +4 -3
- package/references/report-copy-style.md +107 -0
- package/references/retrieval-compliance.md +75 -0
- package/references/retrieval-protocol.md +20 -0
- package/retrieval/audit.py +27 -3
- package/retrieval/fetch.py +96 -0
- package/retrieval/sciverse.py +398 -0
- package/retrieval/search.py +47 -7
- package/schemas/applicability.schema.json +94 -0
- package/schemas/chart-spec.schema.json +10 -3
- package/schemas/evidence.schema.json +316 -43
- package/schemas/fetch-result.schema.json +2 -1
- package/schemas/report-result.schema.json +3 -3
- package/schemas/report-spec.schema.json +98 -100
- package/schemas/skeptic.schema.json +86 -0
- package/schemas/source.schema.json +21 -2
- package/schemas/v2/decision-snapshot.schema.json +20 -9
- package/schemas/v2/finding.schema.json +5 -1
- package/schemas/v2/intake.schema.json +191 -0
- package/schemas/v2/methodology-audit.schema.json +5 -1
- package/schemas/v2/outcome.schema.json +28 -5
- package/schemas/v2/study.schema.json +5 -1
- package/schemas/vNext/autoevolve-session.schema.json +34 -1
- package/schemas/vNext/eval-snapshot.schema.json +77 -1
- package/schemas/vNext/execution-plan.schema.json +50 -1
- package/schemas/vNext/gap-priority.schema.json +54 -1
- package/schemas/vNext/negative-search-record.schema.json +68 -1
- package/schemas/vNext/research-iteration.schema.json +87 -1
- package/schemas/vNext/research-strategy.schema.json +62 -1
- package/schemas/vNext/skill-experiment.schema.json +90 -1
- package/schemas/vNext/task-spec.schema.json +156 -1
- package/schemas/vNext/worker-result.schema.json +60 -1
- package/schemas/verdict.schema.json +164 -28
- package/scripts/build_evidence_library.py +15 -5
- package/scripts/build_report_variants.py +18 -2
- package/scripts/build_result.py +74 -9
- package/scripts/check_package_parity.py +85 -0
- package/scripts/check_protocol_alignment.py +375 -0
- package/scripts/check_versioned_schemas.py +254 -0
- package/scripts/claim_audit.py +13 -8
- package/scripts/compute_confidence.py +10 -0
- package/scripts/dashboard_server.py +13 -2
- package/scripts/did_regression.py +12 -2
- package/scripts/evidence_score.py +5 -2
- package/scripts/intake/__init__.py +31 -0
- package/scripts/intake/__main__.py +18 -0
- package/scripts/intake/background.py +78 -0
- package/scripts/intake/browser.py +79 -0
- package/scripts/intake/cli.py +57 -0
- package/scripts/intake/constants.py +57 -0
- package/scripts/intake/depth.py +53 -0
- package/scripts/intake/enhancements.py +106 -0
- package/scripts/intake/hooks.py +90 -0
- package/scripts/intake/prefs.py +76 -0
- package/scripts/intake/prompts.py +85 -0
- package/scripts/intake/session.py +152 -0
- package/scripts/lint_file_layers.py +126 -0
- package/scripts/orchestrator.py +187 -40
- package/scripts/pre_verdict_gate.py +241 -29
- package/scripts/quickstart.py +18 -2
- package/scripts/run_workspace.py +7 -1
- package/scripts/skill_lint.py +11 -1
- package/scripts/skill_payload.py +6 -3
- package/scripts/test_adversarial_empirical.py +96 -25
- package/scripts/validate_schema.py +31 -1
- package/skill/agents/evaluation-designer.md +20 -4
- package/skill/agents/evidence-analyst.md +19 -3
- package/skill/agents/evidence-judge.md +98 -8
- package/skill/agents/evidence-retriever.md +20 -3
- package/skill/agents/intervention-designer.md +20 -4
- package/skill/agents/method-reviewer.md +18 -2
- package/skill/agents/{education-planner.md → research-planner.md} +19 -3
- package/skill/agents/skeptic.md +18 -2
- package/skill/roles/registry.yaml +11 -11
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
- package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
- package/skill/sub-skills/data-analysis/SKILL.md +34 -15
- package/skill/sub-skills/ethics-review/SKILL.md +33 -10
- package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
- package/skill/sub-skills/evidence-review/SKILL.md +31 -12
- package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
- package/skill/sub-skills/literature-review/SKILL.md +35 -14
- package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
- package/skill/sub-skills/report-generation/SKILL.md +28 -0
- package/skill/sub-skills/research-planning/SKILL.md +41 -14
- package/skill/sub-skills/study-design/SKILL.md +30 -9
- package/skill/task-briefs/adjudicate.md +32 -7
- package/skill/task-briefs/applicability.md +37 -2
- package/skill/task-briefs/audit.md +32 -7
- package/skill/task-briefs/challenge.md +34 -5
- package/skill/task-briefs/evaluate.md +30 -5
- package/skill/task-briefs/extract.md +31 -8
- package/skill/task-briefs/frame.md +39 -10
- package/skill/task-briefs/intervene.md +32 -6
- package/skill/task-briefs/present.md +32 -8
- package/skill/task-briefs/projection.md +36 -2
- package/skill/task-briefs/retrieve.md +36 -6
- package/skill/workflows/decision-and-pilot.md +76 -1
- package/skill/workflows/evaluate-and-update.md +83 -0
- package/skill/workflows/evidence-review.md +104 -0
- package/skill/workflows/experimental-jev.md +170 -0
- package/skill/workflows/intake.md +120 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +25 -3
- package/visualization/eduevidence-report/scripts/build_infographics.py +37 -15
- package/visualization/eduevidence-report/scripts/build_report.py +435 -575
- package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +349 -38
- package/visualization/eduevidence-report/scripts/report_copy_pack.py +296 -0
- package/visualization/eduevidence-report/scripts/report_copy_policy_guard.py +47 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +141 -1
- package/web/architecture.html +14885 -0
- package/web/studio/assets/index-B8tkF44Q.css +1 -0
- package/web/studio/index.html +2 -2
- package/scripts/build_esl_artifacts.py +0 -1921
- package/scripts/build_killer_demo.py +0 -295
- package/scripts/enrich_projects_human_and_lieflat.py +0 -315
- package/scripts/generate_new_projects.py +0 -686
- package/scripts/sync_killer_demo_report.py +0 -270
- package/web/studio/assets/index-CzXocaGv.css +0 -1
- /package/web/studio/assets/{index-pa7jD7n4.js → index-CQ6Keoyc.js} +0 -0
package/engine/capabilities.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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"]
|
package/engine/evidence_graph.py
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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"
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
return
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
return
|
|
119
|
-
return
|
|
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
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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.
|
|
232
|
-
|
|
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
|
|
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",
|
|
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.
|
|
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']}")
|