eduevidence 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- package/visualization/eduevidence-report/themes/presentation.css +140 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""engine/evidencecore.py - EvidenceCore v4 domain registry (抽象第一步).
|
|
2
|
+
|
|
3
|
+
v4 领域包机制:domains/ 注册表 + 领域契约加载 + frame 校验。
|
|
4
|
+
|
|
5
|
+
- 领域无关常量:DECISION_STATES(四态决策)、PROTOCOL_STEPS(9 步协议)。
|
|
6
|
+
- list_domains():按注册表顺序列出领域。
|
|
7
|
+
- load_domain(domain_id):读取并校验领域条目——所有引用契约必须真实存在。
|
|
8
|
+
- validate_frame(domain_id, frame_dict):用该领域的 frame schema 校验
|
|
9
|
+
frame(education 复用现有 scripts.validate_schema 校验
|
|
10
|
+
schemas/education-frame.schema.json;policy 用 domains/policy/frame.schema.json)。
|
|
11
|
+
|
|
12
|
+
education 域只是"指向现有契约"的注册:不新增任何逻辑路径、不引入新 schema
|
|
13
|
+
或新校验器。领域选择(domain select)由主 agent 接 CLI 完成,引擎层不做选择。
|
|
14
|
+
|
|
15
|
+
路径解析:当前按仓库布局(domains/ 在仓库根目录)解析;wheel 安装场景的
|
|
16
|
+
share/ 回退留给后续步骤(pyproject data-files 未包含 domains/)。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _resolve_domains_dir() -> Path:
|
|
29
|
+
"""Repository layout first; wheel-installed share/ layout as fallback."""
|
|
30
|
+
repo = REPO_ROOT / "domains"
|
|
31
|
+
if repo.is_dir():
|
|
32
|
+
return repo
|
|
33
|
+
import sys
|
|
34
|
+
share = Path(sys.prefix) / "share" / "eduevidence" / "domains"
|
|
35
|
+
if share.is_dir():
|
|
36
|
+
return share
|
|
37
|
+
return repo
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DOMAINS_DIR = _resolve_domains_dir()
|
|
41
|
+
REGISTRY_FILE = DOMAINS_DIR / "manifest.json"
|
|
42
|
+
|
|
43
|
+
#: 领域无关:四态决策(README: ADOPT / PILOT / REJECT / INSUFFICIENT EVIDENCE)。
|
|
44
|
+
DECISION_STATES = ("adopt", "pilot", "reject", "insufficient_evidence")
|
|
45
|
+
|
|
46
|
+
#: 领域无关:9 步 EvidenceFlow 协议(SKILL.md Research Core 6 + Decision Extension 3)。
|
|
47
|
+
PROTOCOL_STEPS = (
|
|
48
|
+
"Frame", "Retrieve", "Extract", "Challenge", "Audit",
|
|
49
|
+
"Adjudicate", "Applicability", "Intervene", "Evaluate",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: 注册表条目必备字段(domains/manifest.json)。
|
|
53
|
+
REQUIRED_REGISTRY_FIELDS = (
|
|
54
|
+
"id", "name", "description", "frame_schema", "outcome_taxonomy",
|
|
55
|
+
"methodology_checklist", "golds_dir", "references_dir",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_cache: dict[str, Any] = {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_registry() -> dict:
|
|
62
|
+
"""读取(缓存)domains/manifest.json 注册表。"""
|
|
63
|
+
if "_registry" not in _cache:
|
|
64
|
+
_cache["_registry"] = json.loads(
|
|
65
|
+
REGISTRY_FILE.read_text(encoding="utf-8"))
|
|
66
|
+
return _cache["_registry"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def list_domains() -> list[dict]:
|
|
70
|
+
"""按注册表顺序返回全部领域条目(id/name/description/契约引用)。"""
|
|
71
|
+
return list(_load_registry()["domains"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _field_target(entry: dict, field: str) -> Path | None:
|
|
75
|
+
"""把注册表路径字段解析为仓库内路径。
|
|
76
|
+
|
|
77
|
+
JSON Pointer 引用("file.json#/pointer")取文件本体路径;
|
|
78
|
+
指针指向的内容由 _check_pointer 另行校验。None 表示该字段
|
|
79
|
+
明确为 null(可选契约,如 policy 的 golds_dir)。
|
|
80
|
+
"""
|
|
81
|
+
raw = entry.get(field)
|
|
82
|
+
if raw is None:
|
|
83
|
+
return None
|
|
84
|
+
return REPO_ROOT / str(raw).partition("#")[0]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _check_pointer(entry: dict, field: str) -> None:
|
|
88
|
+
"""校验 "file.json#/pointer" 引用:文件存在且指针可解析为非空列表。"""
|
|
89
|
+
raw = str(entry[field])
|
|
90
|
+
path_text, _, pointer = raw.partition("#")
|
|
91
|
+
if not pointer:
|
|
92
|
+
return
|
|
93
|
+
target = REPO_ROOT / path_text
|
|
94
|
+
if not target.is_file():
|
|
95
|
+
raise FileNotFoundError(
|
|
96
|
+
f"domain {entry['id']!r}: {field} file missing: {target}")
|
|
97
|
+
node: Any = json.loads(target.read_text(encoding="utf-8"))
|
|
98
|
+
for part in pointer.strip("/").split("/"):
|
|
99
|
+
# RFC 6901 unescaping: ~1 -> "/", ~0 -> "~" (P2-9)
|
|
100
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
101
|
+
if not isinstance(node, dict) or part not in node:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"domain {entry['id']!r}: {field} pointer {pointer!r} "
|
|
104
|
+
f"unresolvable in {target}")
|
|
105
|
+
node = node[part]
|
|
106
|
+
if not isinstance(node, list) or not node:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"domain {entry['id']!r}: {field} pointer {pointer!r} must "
|
|
109
|
+
f"resolve to a non-empty list, got {type(node).__name__}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate_contracts(entry: dict) -> None:
|
|
113
|
+
"""校验领域条目引用的契约真实存在(load_domain 的核心职责)。
|
|
114
|
+
|
|
115
|
+
- frame_schema / outcome_taxonomy / methodology_checklist:文件存在且
|
|
116
|
+
为可解析 JSON(指针引用另校验指针内容);
|
|
117
|
+
- golds_dir / references_dir:目录存在(null 视为"无此契约",跳过)。
|
|
118
|
+
"""
|
|
119
|
+
domain_id = entry["id"]
|
|
120
|
+
|
|
121
|
+
def check_file(field: str) -> None:
|
|
122
|
+
target = _field_target(entry, field)
|
|
123
|
+
if target is None:
|
|
124
|
+
raise FileNotFoundError(
|
|
125
|
+
f"domain {domain_id!r}: {field} must reference a file")
|
|
126
|
+
if not target.is_file():
|
|
127
|
+
raise FileNotFoundError(
|
|
128
|
+
f"domain {domain_id!r}: {field} contract missing: {target}")
|
|
129
|
+
json.loads(target.read_text(encoding="utf-8")) # must parse as JSON
|
|
130
|
+
if "#" in str(entry[field]):
|
|
131
|
+
_check_pointer(entry, field)
|
|
132
|
+
|
|
133
|
+
def check_dir(field: str) -> None:
|
|
134
|
+
raw = entry.get(field)
|
|
135
|
+
if raw is None:
|
|
136
|
+
return # 可选契约(如 policy 的 golds_dir)
|
|
137
|
+
target = REPO_ROOT / str(raw)
|
|
138
|
+
if not target.is_dir():
|
|
139
|
+
raise FileNotFoundError(
|
|
140
|
+
f"domain {domain_id!r}: {field} missing: {target}")
|
|
141
|
+
|
|
142
|
+
check_file("frame_schema")
|
|
143
|
+
check_file("outcome_taxonomy")
|
|
144
|
+
check_file("methodology_checklist")
|
|
145
|
+
check_dir("golds_dir")
|
|
146
|
+
check_dir("references_dir")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_domain(domain_id: str) -> dict:
|
|
150
|
+
"""加载领域条目并校验 manifest/契约存在性。
|
|
151
|
+
|
|
152
|
+
Returns: domains/manifest.json 中该领域的注册条目(路径为仓库相对路径)。
|
|
153
|
+
Raises: KeyError(未知领域);FileNotFoundError / ValueError(契约缺失
|
|
154
|
+
或损坏)。
|
|
155
|
+
"""
|
|
156
|
+
for entry in list_domains():
|
|
157
|
+
if entry["id"] == domain_id:
|
|
158
|
+
_validate_contracts(entry)
|
|
159
|
+
return entry
|
|
160
|
+
known = ", ".join(d["id"] for d in list_domains())
|
|
161
|
+
raise KeyError(f"unknown domain {domain_id!r} (registered: {known})")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def validate_frame(domain_id: str, frame: dict) -> list[str]:
|
|
165
|
+
"""用该领域的 frame schema 校验 frame dict。
|
|
166
|
+
|
|
167
|
+
Returns: 稳定错误字符串列表(空 == 合法),与 engine/contracts.py 的
|
|
168
|
+
validate_record 约定一致;education 复用 scripts.validate_schema 校验
|
|
169
|
+
现有 schemas/education-frame.schema.json,不引入新校验器。
|
|
170
|
+
"""
|
|
171
|
+
from scripts.validate_schema import SchemaError, validate # noqa: PLC0415
|
|
172
|
+
|
|
173
|
+
entry = load_domain(domain_id)
|
|
174
|
+
schema_path = _field_target(entry, "frame_schema")
|
|
175
|
+
assert schema_path is not None # load_domain 已保证存在
|
|
176
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
177
|
+
errors: list[str] = []
|
|
178
|
+
try:
|
|
179
|
+
validate(frame, schema)
|
|
180
|
+
except SchemaError as exc:
|
|
181
|
+
errors.append(str(exc))
|
|
182
|
+
return errors
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""engine/gap_lens.py — Research Gap Discovery & Contradiction Lens.
|
|
2
|
+
|
|
3
|
+
Inspired by BioGapLens & PICO gap taxonomy. Analyzes the EvidenceGraph to identify:
|
|
4
|
+
1. Population Gaps (unexplored cohorts, demographic bias)
|
|
5
|
+
2. Measurement / Retention Gaps (missing delayed post-tests, lack of unassisted transfer)
|
|
6
|
+
3. Methodological Gaps (lack of baseline equivalence, missing active controls)
|
|
7
|
+
4. Contradiction Lenses (identifying moderators explaining divergent study findings)
|
|
8
|
+
|
|
9
|
+
Generates pre-registered 12-week quasi-experimental DID / RCT trial protocols
|
|
10
|
+
specifically grounded on verified gaps (enforcing the rule: 'No study design without evidence grounding').
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
from engine.evidence_graph import EvidenceGraph, GapNode, RiskNode
|
|
16
|
+
from engine.semantics import OutcomeDimension
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GapLensAnalyzer:
|
|
20
|
+
"""Automated research gap discovery and contradiction diagnostic engine."""
|
|
21
|
+
|
|
22
|
+
def analyze_gaps(self, graph: EvidenceGraph) -> List[GapNode]:
|
|
23
|
+
gaps: List[GapNode] = []
|
|
24
|
+
pico = graph.intent.get("pico", {})
|
|
25
|
+
intervention = pico.get("intervention", "Target Intervention")
|
|
26
|
+
population = pico.get("population", "Target Population")
|
|
27
|
+
|
|
28
|
+
ev_list = list(graph.evidence.values())
|
|
29
|
+
if not ev_list:
|
|
30
|
+
return gaps
|
|
31
|
+
|
|
32
|
+
# 1. Measurement & Retention Gap Check
|
|
33
|
+
transfer_nodes = [ev for ev in ev_list if ev.outcome_dimension == OutcomeDimension.INDEPENDENT_TRANSFER]
|
|
34
|
+
procedural_nodes = [ev for ev in ev_list if ev.outcome_dimension == OutcomeDimension.PROCEDURAL_EFFICIENCY]
|
|
35
|
+
|
|
36
|
+
if procedural_nodes and not transfer_nodes:
|
|
37
|
+
gaps.append(GapNode(
|
|
38
|
+
gap_id=f"GAP-RETENTION-001",
|
|
39
|
+
gap_type="Measurement/Retention Gap",
|
|
40
|
+
description=f"Existing studies on {intervention} exclusively evaluate immediate in-session task speed without measuring delayed conceptual retention or unassisted transfer.",
|
|
41
|
+
target_outcome="Delayed Unassisted Transfer",
|
|
42
|
+
existing_evidence_summary=f"{len(procedural_nodes)} studies evaluated active speed; 0 studies measured unassisted delayed retention.",
|
|
43
|
+
recommended_trial_design="12-Week Cluster Randomized Trial with 4-week delayed post-test without AI assistance.",
|
|
44
|
+
))
|
|
45
|
+
elif transfer_nodes and any(ev.effect_size.get("value", 0.0) <= 0.05 for ev in transfer_nodes):
|
|
46
|
+
avg_proc = (
|
|
47
|
+
sum(ev.effect_size.get("value", 0.0) for ev in procedural_nodes) / len(procedural_nodes)
|
|
48
|
+
if procedural_nodes else 0.0
|
|
49
|
+
)
|
|
50
|
+
avg_trans = (
|
|
51
|
+
sum(ev.effect_size.get("value", 0.0) for ev in transfer_nodes) / len(transfer_nodes)
|
|
52
|
+
if transfer_nodes else 0.0
|
|
53
|
+
)
|
|
54
|
+
gaps.append(GapNode(
|
|
55
|
+
gap_id=f"GAP-SCAFFOLD-FADE-001",
|
|
56
|
+
gap_type="Methodological Gap",
|
|
57
|
+
description=f"Delayed transfer tests indicate cognitive offloading/scaffolding dependency under unguarded {intervention}. Lack of trials evaluating structured 'fading' protocols.",
|
|
58
|
+
target_outcome="Independent Problem Solving",
|
|
59
|
+
existing_evidence_summary=f"Synthesized evidence shows {avg_proc:+.2f}g in-task speed ({len(procedural_nodes)} studies) vs {avg_trans:+.2f}g delayed unassisted transfer ({len(transfer_nodes)} studies).",
|
|
60
|
+
recommended_trial_design="2x2 Factorial Trial: Unguarded Access vs Structured Socratic Fading Scaffold vs Control.",
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
# 2. Population & Context Heterogeneity Gap
|
|
64
|
+
cohorts = [ev.sample_description.lower() for ev in ev_list if ev.sample_description]
|
|
65
|
+
has_novice = any("freshman" in c or "novice" in c or "cs1" in c or "intro" in c or "初学" in c for c in cohorts)
|
|
66
|
+
has_advanced = any("senior" in c or "advanced" in c or "professional" in c or "高年级" in c or "专家" in c for c in cohorts)
|
|
67
|
+
|
|
68
|
+
if has_novice and not has_advanced:
|
|
69
|
+
gaps.append(GapNode(
|
|
70
|
+
gap_id=f"GAP-POP-EXPERTISE-001",
|
|
71
|
+
gap_type="Population Gap",
|
|
72
|
+
description=f"Evidence is heavily skewed toward introductory novices; efficacy on advanced algorithmic design and large-scale software engineering remains unexplored.",
|
|
73
|
+
target_outcome="Advanced Software Architecture",
|
|
74
|
+
existing_evidence_summary=f"Evaluations across {len(cohorts)} studies are restricted to introductory novice cohorts ({population}).",
|
|
75
|
+
recommended_trial_design="Comparative Quasi-Experimental Study evaluating CS3/CS4 upper-division capstone cohorts.",
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
# 3. Contradiction Lens (Divergent effect size isolation)
|
|
79
|
+
pos_studies = [ev for ev in ev_list if ev.effect_size.get("value", 0.0) > 0.20]
|
|
80
|
+
neg_studies = [ev for ev in ev_list if ev.effect_size.get("value", 0.0) < -0.10]
|
|
81
|
+
|
|
82
|
+
if pos_studies and neg_studies:
|
|
83
|
+
gaps.append(GapNode(
|
|
84
|
+
gap_id=f"GAP-CONTRADICTION-001",
|
|
85
|
+
gap_type="Contradiction Lens",
|
|
86
|
+
description=f"Contradictory findings detected across {len(pos_studies)} positive vs {len(neg_studies)} negative studies. Moderator hypothesis: Degree of real-time Socratic prompting vs direct code generation.",
|
|
87
|
+
target_outcome="Syntax Speed vs Algorithmic Reasoning",
|
|
88
|
+
existing_evidence_summary=f"Positive studies ({[s.evidence_id for s in pos_studies[:2]]}) measured active code drafting; negative studies ({[s.evidence_id for s in neg_studies[:2]]}) measured unassisted closed-book exams.",
|
|
89
|
+
recommended_trial_design="Dismantling Study isolating the active ingredient of AI prompting modality.",
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
# Register discovered gaps into graph
|
|
93
|
+
for g in gaps:
|
|
94
|
+
graph.add_gap(g)
|
|
95
|
+
|
|
96
|
+
return gaps
|
|
97
|
+
|
|
98
|
+
def generate_pre_registered_protocol(self, gap: GapNode, graph: EvidenceGraph) -> Dict[str, Any]:
|
|
99
|
+
"""Generates a complete 12-week pre-registered quasi-experimental DID trial protocol."""
|
|
100
|
+
pico = graph.intent.get("pico", {})
|
|
101
|
+
intervention = pico.get("intervention", "Target Intervention")
|
|
102
|
+
population = pico.get("population", "Target Population")
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"protocol_id": f"PROTO-{gap.gap_id}",
|
|
106
|
+
"grounded_gap_id": gap.gap_id,
|
|
107
|
+
"title": f"Pre-Registered 12-Week Field Trial: Evaluating {gap.target_outcome} under {intervention}",
|
|
108
|
+
"design_type": "Difference-in-Differences (DID) with Baseline Equivalence",
|
|
109
|
+
"duration_weeks": 12,
|
|
110
|
+
"target_population": population,
|
|
111
|
+
"sample_size_target": 240,
|
|
112
|
+
"arms": [
|
|
113
|
+
{"name": "Treatment Arm (Scaffolded Intervention)", "n": 120, "description": f"{intervention} with mandatory reflection and phased scaffolding fade"},
|
|
114
|
+
{"name": "Active Control Arm (Standard Curriculum)", "n": 120, "description": "Traditional IDE with standard pedagogical TA support"},
|
|
115
|
+
],
|
|
116
|
+
"timeline": [
|
|
117
|
+
{"week": "Week 1", "phase": "Pre-Test Baseline", "measurement": "Baseline Equivalence & Prior Achievement Test (WWC 5.0 compliant)"},
|
|
118
|
+
{"week": "Weeks 2-5", "phase": "Phase 1: Assisted Foundation", "measurement": "In-task procedural completion time and cognitive load"},
|
|
119
|
+
{"week": "Weeks 6-9", "phase": "Phase 2: Scaffolding Fade", "measurement": "Socratic prompt fidelity and conceptual mental model check"},
|
|
120
|
+
{"week": "Week 10", "phase": "Phase 3: Unassisted Transfer", "measurement": "Solo unassisted closed-book problem solving exam"},
|
|
121
|
+
{"week": "Week 12", "phase": "Phase 4: Delayed Retention Wave", "measurement": "4-week delayed retention test + DID regression data export"},
|
|
122
|
+
],
|
|
123
|
+
"statistical_model": "DID Regression: Y_it = beta_0 + beta_1*Treat_i + beta_2*Post_t + delta*(Treat_i * Post_t) + gamma*X_it + epsilon_it",
|
|
124
|
+
"causal_estimand": "delta (Average Treatment Effect on the Treated / ATT)",
|
|
125
|
+
"stopping_rules": [
|
|
126
|
+
"Severe drop in solo mid-term exam performance (> 1.0 SD deficit vs control)",
|
|
127
|
+
"Academic integrity breach rate exceeding 15% in treatment cohort",
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
gap_lens = GapLensAnalyzer()
|
package/engine/gaps.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Structured Knowledge Gap derivation.
|
|
2
|
+
|
|
3
|
+
A KnowledgeGap is not free-form "future work": it is derived from coverage —
|
|
4
|
+
the research frame's requested outcomes vs what the graph's Findings
|
|
5
|
+
actually measure. A task-performance Finding never covers a retention or
|
|
6
|
+
transfer gap.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from engine.contracts import validate_record
|
|
15
|
+
from engine.graph_store import GraphStore
|
|
16
|
+
from engine.ids import new_local_id
|
|
17
|
+
from engine.synthesis import ClaimSynthesis
|
|
18
|
+
|
|
19
|
+
# outcome_type names for timepoint-like gaps (frame.requested_outcomes entries
|
|
20
|
+
# may carry outcome_type or be plain strings; we match on outcome_type)
|
|
21
|
+
_RETENTION_TYPES = {"retention", "long_term", "learning_retention"}
|
|
22
|
+
_TRANSFER_TYPES = {"transfer", "transfer_learning", "far_transfer"}
|
|
23
|
+
_TASK_PERFORMANCE = {"task_performance", "assignment_score", "task_completion"}
|
|
24
|
+
_LEARNING = {"learning"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def derive_gaps(*, store: GraphStore,
|
|
28
|
+
syntheses: tuple[ClaimSynthesis, ...] | None = None,
|
|
29
|
+
frame: dict | None = None) -> list[dict]:
|
|
30
|
+
"""Derive structured gaps from graph coverage vs the research frame.
|
|
31
|
+
|
|
32
|
+
`frame` carries `requested_outcomes` (list of outcome names/types) and
|
|
33
|
+
optionally `target_population`. Findings' outcome types come from the
|
|
34
|
+
graph's outcomes table.
|
|
35
|
+
"""
|
|
36
|
+
frame = frame or {}
|
|
37
|
+
requested = frame.get("requested_outcomes") or []
|
|
38
|
+
if not requested and frame.get("target_outcomes"):
|
|
39
|
+
requested = frame["target_outcomes"]
|
|
40
|
+
|
|
41
|
+
findings = store.read_table("findings")
|
|
42
|
+
outcomes = {o["outcome_id"]: o for o in store.read_table("outcomes")}
|
|
43
|
+
covered_types: set[str] = set()
|
|
44
|
+
for f in findings:
|
|
45
|
+
o = outcomes.get(f["outcome_id"])
|
|
46
|
+
if o:
|
|
47
|
+
covered_types.add(o.get("outcome_type", ""))
|
|
48
|
+
|
|
49
|
+
claims = store.read_table("claims")
|
|
50
|
+
claim_ids = [c["claim_id"] for c in claims]
|
|
51
|
+
|
|
52
|
+
gaps: list[dict] = []
|
|
53
|
+
rev = store.active_revision()
|
|
54
|
+
|
|
55
|
+
def add(gap_type: str, priority: str, reasoning: str,
|
|
56
|
+
related_claims: list[str] | None = None,
|
|
57
|
+
related_outcomes: list[str] | None = None):
|
|
58
|
+
gaps.append({
|
|
59
|
+
"gap_id": new_local_id("GAP", {g["gap_id"] for g in gaps}),
|
|
60
|
+
"gap_type": gap_type,
|
|
61
|
+
"related_claim_ids": related_claims or [],
|
|
62
|
+
"related_outcome_ids": related_outcomes or [],
|
|
63
|
+
"priority": priority,
|
|
64
|
+
"reasoning": reasoning,
|
|
65
|
+
"status": "open",
|
|
66
|
+
"derived_from_graph_revision": rev,
|
|
67
|
+
"extensions": {},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
def _req_kind(req) -> tuple[str, str]:
|
|
71
|
+
"""Classify a requested outcome: retention | transfer |
|
|
72
|
+
task_performance | learning | other. Type-aware: names are matched
|
|
73
|
+
only within the outcome's declared type, never type-blind."""
|
|
74
|
+
if isinstance(req, dict):
|
|
75
|
+
req_name = str(req.get("name", "")).lower()
|
|
76
|
+
req_type = str(req.get("outcome_type", "")).lower()
|
|
77
|
+
else:
|
|
78
|
+
req_name, req_type = str(req).lower(), ""
|
|
79
|
+
if req_type in _RETENTION_TYPES or req_name in _RETENTION_TYPES:
|
|
80
|
+
return "retention", req.get("name", "") if isinstance(req, dict) else str(req)
|
|
81
|
+
if req_type in _TRANSFER_TYPES or req_name in _TRANSFER_TYPES:
|
|
82
|
+
return "transfer", req.get("name", "") if isinstance(req, dict) else str(req)
|
|
83
|
+
if req_type in _TASK_PERFORMANCE or req_name in _TASK_PERFORMANCE:
|
|
84
|
+
return "task_performance", req.get("name", "") if isinstance(req, dict) else str(req)
|
|
85
|
+
if req_type in _LEARNING or req_name in _LEARNING:
|
|
86
|
+
return "learning", req.get("name", "") if isinstance(req, dict) else str(req)
|
|
87
|
+
return "other", req.get("name", "") if isinstance(req, dict) else str(req)
|
|
88
|
+
|
|
89
|
+
_RETENTION_COVER = _RETENTION_TYPES
|
|
90
|
+
_TRANSFER_COVER = _TRANSFER_TYPES
|
|
91
|
+
def covered_for_kind(kind: str) -> bool:
|
|
92
|
+
if kind == "retention":
|
|
93
|
+
return bool(covered_types & _RETENTION_COVER)
|
|
94
|
+
if kind == "transfer":
|
|
95
|
+
return bool(covered_types & _TRANSFER_COVER)
|
|
96
|
+
if kind == "task_performance":
|
|
97
|
+
return bool(covered_types & _TASK_PERFORMANCE)
|
|
98
|
+
if kind == "learning":
|
|
99
|
+
return bool(covered_types & _LEARNING)
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# one pass per requested outcome; each gap emitted exactly once
|
|
103
|
+
seen: set[tuple[str, str]] = set()
|
|
104
|
+
for req in requested:
|
|
105
|
+
kind, label = _req_kind(req)
|
|
106
|
+
if not label:
|
|
107
|
+
continue
|
|
108
|
+
key = (kind, label)
|
|
109
|
+
if key in seen:
|
|
110
|
+
continue
|
|
111
|
+
seen.add(key)
|
|
112
|
+
if covered_for_kind(kind):
|
|
113
|
+
continue
|
|
114
|
+
if kind == "retention":
|
|
115
|
+
add("missing_retention", "high",
|
|
116
|
+
f"frame requests retention outcome {label!r} but the graph has "
|
|
117
|
+
f"no retention-type measurement; task-performance coverage does "
|
|
118
|
+
f"not count (RULE 3)")
|
|
119
|
+
elif kind == "transfer":
|
|
120
|
+
add("missing_transfer", "high",
|
|
121
|
+
f"frame requests transfer outcome {label!r} but the graph has "
|
|
122
|
+
f"no transfer-type measurement; AI-assisted task performance "
|
|
123
|
+
f"does not count (RULE 3)")
|
|
124
|
+
elif kind == "task_performance":
|
|
125
|
+
add("missing_outcome", "medium",
|
|
126
|
+
f"frame requests task-performance outcome {label!r} with no "
|
|
127
|
+
f"covering finding")
|
|
128
|
+
elif kind == "learning":
|
|
129
|
+
add("missing_outcome", "medium",
|
|
130
|
+
f"frame requests learning outcome {label!r} with no covering "
|
|
131
|
+
f"learning finding; task performance is not learning (RULE 3)")
|
|
132
|
+
else:
|
|
133
|
+
add("missing_outcome", "medium",
|
|
134
|
+
f"frame requests outcome {label!r} with no covering finding")
|
|
135
|
+
claim_outcomes = {c["claim_id"]: c.get("primary_outcome_ids", [])
|
|
136
|
+
for c in claims}
|
|
137
|
+
|
|
138
|
+
# contradiction gaps
|
|
139
|
+
for syn in syntheses or ():
|
|
140
|
+
if syn.status == "contested":
|
|
141
|
+
add("unresolved_conflict", "high",
|
|
142
|
+
f"claim {syn.claim_id} has independent contradictory studies "
|
|
143
|
+
f"({', '.join(syn.study_ids)})", [syn.claim_id],
|
|
144
|
+
claim_outcomes.get(syn.claim_id, []))
|
|
145
|
+
|
|
146
|
+
# methodology weakness / insufficient independence
|
|
147
|
+
if syntheses:
|
|
148
|
+
for syn in syntheses:
|
|
149
|
+
if syn.status == "insufficient" and len(syn.study_ids) < 2:
|
|
150
|
+
add("insufficient_sample_independence", "medium",
|
|
151
|
+
f"claim {syn.claim_id} rests on fewer than two independent "
|
|
152
|
+
f"studies", [syn.claim_id],
|
|
153
|
+
claim_outcomes.get(syn.claim_id, []))
|
|
154
|
+
|
|
155
|
+
# validate each gap
|
|
156
|
+
for g in gaps:
|
|
157
|
+
errors = validate_record("knowledge-gap", g)
|
|
158
|
+
if errors:
|
|
159
|
+
raise ValueError(f"invalid gap: {errors}")
|
|
160
|
+
return gaps
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def save_gaps(project, *, graph_revision: int, gaps: list[dict]) -> Path:
|
|
164
|
+
"""Persist gaps under gaps/ (one JSONL file per revision)."""
|
|
165
|
+
path = project.path / "gaps" / f"gaps-rev-{graph_revision:06d}.jsonl"
|
|
166
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
lines = "".join(json.dumps(g, ensure_ascii=False) + "\n" for g in gaps)
|
|
168
|
+
path.write_text(lines, encoding="utf-8")
|
|
169
|
+
return path
|