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,268 @@
|
|
|
1
|
+
"""Snapshot-based Shared Research Library.
|
|
2
|
+
|
|
3
|
+
Reusable research facts (Source/Study/Finding/MethodologyAudit) live in an
|
|
4
|
+
immutable library revision model mirroring the Project GraphStore's
|
|
5
|
+
crash-safe pattern. Projects import snapshots: a Project never evaluates
|
|
6
|
+
directly against library JSONL, and a later library change never silently
|
|
7
|
+
changes an existing Project's conclusions — only an explicit import/sync
|
|
8
|
+
advances the Project graph.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from engine.contracts import validate_record
|
|
18
|
+
from engine.graph_store import GraphStore, GraphMutation, GraphRevision, _atomic_write_text
|
|
19
|
+
|
|
20
|
+
LIBRARY_TABLES = ("sources", "studies", "findings", "audits")
|
|
21
|
+
_TABLE_SCHEMA = {
|
|
22
|
+
"sources": "source",
|
|
23
|
+
"studies": "study",
|
|
24
|
+
"findings": "finding",
|
|
25
|
+
"audits": "methodology-audit",
|
|
26
|
+
}
|
|
27
|
+
_TABLE_ID_KEY = {
|
|
28
|
+
"sources": "source_id",
|
|
29
|
+
"studies": "study_id",
|
|
30
|
+
"findings": "finding_id",
|
|
31
|
+
"audits": "audit_id",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now_iso() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).isoformat()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ResearchLibrary:
|
|
40
|
+
"""Shared Research Library under `home/library/`."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, home: Path):
|
|
43
|
+
self.home = Path(home).expanduser().resolve()
|
|
44
|
+
self.library_dir = self.home / "library"
|
|
45
|
+
self.revisions_dir = self.library_dir / "revisions"
|
|
46
|
+
self.head_path = self.library_dir / "HEAD"
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def open(cls, home: Path) -> "ResearchLibrary":
|
|
50
|
+
lib = cls(home)
|
|
51
|
+
lib.revisions_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
if not lib.head_path.is_file():
|
|
53
|
+
_atomic_write_text(lib.head_path, "0")
|
|
54
|
+
return lib
|
|
55
|
+
|
|
56
|
+
def active_revision(self) -> int:
|
|
57
|
+
return int(self.head_path.read_text(encoding="utf-8").strip())
|
|
58
|
+
|
|
59
|
+
def _revision_dir(self, revision: int) -> Path:
|
|
60
|
+
return self.revisions_dir / f"rev-{revision:06d}"
|
|
61
|
+
|
|
62
|
+
def _read_snapshot(self, revision: int) -> dict[str, list[dict]]:
|
|
63
|
+
if revision == 0:
|
|
64
|
+
return {t: [] for t in LIBRARY_TABLES}
|
|
65
|
+
rev_dir = self._revision_dir(revision)
|
|
66
|
+
snapshot: dict[str, list[dict]] = {}
|
|
67
|
+
for table in LIBRARY_TABLES:
|
|
68
|
+
path = rev_dir / f"{table}.jsonl"
|
|
69
|
+
if not path.is_file():
|
|
70
|
+
raise FileNotFoundError(f"library revision {revision} missing {path.name}")
|
|
71
|
+
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
72
|
+
snapshot[table] = rows
|
|
73
|
+
return snapshot
|
|
74
|
+
|
|
75
|
+
def read_table(self, table: str) -> list[dict]:
|
|
76
|
+
if table not in LIBRARY_TABLES:
|
|
77
|
+
raise ValueError(f"unknown library table {table!r}")
|
|
78
|
+
return self._read_snapshot(self.active_revision())[table]
|
|
79
|
+
|
|
80
|
+
# ---- mutation --------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def add_verified_bundle(self, *, sources: list[dict], studies: list[dict],
|
|
83
|
+
findings: list[dict], audits: list[dict]) -> int:
|
|
84
|
+
"""Append verified facts as a new immutable library revision.
|
|
85
|
+
|
|
86
|
+
Returns the new revision number. Existing revisions are never
|
|
87
|
+
rewritten. Upserts are by entity id within each table.
|
|
88
|
+
"""
|
|
89
|
+
before_rev = self.active_revision()
|
|
90
|
+
before = self._read_snapshot(before_rev)
|
|
91
|
+
after = {t: [dict(r) for r in rows] for t, rows in before.items()}
|
|
92
|
+
for table, incoming in (
|
|
93
|
+
("sources", sources), ("studies", studies),
|
|
94
|
+
("findings", findings), ("audits", audits),
|
|
95
|
+
):
|
|
96
|
+
id_key = _TABLE_ID_KEY[table]
|
|
97
|
+
by_id = {r[id_key]: r for r in after[table]}
|
|
98
|
+
for rec in incoming:
|
|
99
|
+
errors = validate_record(_TABLE_SCHEMA[table], rec)
|
|
100
|
+
if errors:
|
|
101
|
+
raise ValueError(f"library {table} {rec.get(id_key, '?')} invalid: {errors}")
|
|
102
|
+
by_id[rec[id_key]] = dict(rec)
|
|
103
|
+
after[table] = list(by_id.values())
|
|
104
|
+
|
|
105
|
+
next_rev = before_rev + 1
|
|
106
|
+
tmp_dir = self.revisions_dir / f".tmp-{next_rev:06d}"
|
|
107
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
try:
|
|
109
|
+
for table in LIBRARY_TABLES:
|
|
110
|
+
lines = "".join(
|
|
111
|
+
json.dumps(r, sort_keys=True, separators=(",", ":")) + "\n"
|
|
112
|
+
for r in after[table]
|
|
113
|
+
)
|
|
114
|
+
(tmp_dir / f"{table}.jsonl").write_text(lines, encoding="utf-8")
|
|
115
|
+
manifest = {
|
|
116
|
+
"revision": next_rev,
|
|
117
|
+
"parent_revision": before_rev,
|
|
118
|
+
"created_at": _now_iso(),
|
|
119
|
+
"extensions": {},
|
|
120
|
+
}
|
|
121
|
+
(tmp_dir / "manifest.json").write_text(
|
|
122
|
+
json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
|
123
|
+
rev_dir = self._revision_dir(next_rev)
|
|
124
|
+
if rev_dir.exists():
|
|
125
|
+
raise FileExistsError(f"refusing to rewrite library revision {rev_dir}")
|
|
126
|
+
os.replace(tmp_dir, rev_dir)
|
|
127
|
+
_atomic_write_text(self.head_path, str(next_rev))
|
|
128
|
+
except Exception:
|
|
129
|
+
if tmp_dir.exists():
|
|
130
|
+
import shutil
|
|
131
|
+
shutil.rmtree(tmp_dir)
|
|
132
|
+
raise
|
|
133
|
+
return next_rev
|
|
134
|
+
|
|
135
|
+
# ---- find ------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def find_source(self, canonical_locator: str) -> dict | None:
|
|
138
|
+
for src in self.read_table("sources"):
|
|
139
|
+
if src["canonical_locator"] == canonical_locator:
|
|
140
|
+
return src
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
# ---- snapshot import -------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def import_snapshot(self, *, project, source_ids: list[str],
|
|
146
|
+
run_id: str) -> GraphRevision:
|
|
147
|
+
"""Import the selected library facts into a Project graph revision.
|
|
148
|
+
|
|
149
|
+
Imported entities get `extensions.origin` metadata recording the
|
|
150
|
+
library revision/entity id/content hash, so the Project never depends
|
|
151
|
+
on the live library.
|
|
152
|
+
"""
|
|
153
|
+
snapshot = self._read_snapshot(self.active_revision())
|
|
154
|
+
requested_ids = set(source_ids)
|
|
155
|
+
sources = [s for s in snapshot["sources"] if s["source_id"] in requested_ids]
|
|
156
|
+
if len(sources) != len(requested_ids):
|
|
157
|
+
missing = requested_ids - {s["source_id"] for s in sources}
|
|
158
|
+
raise ValueError(f"library has no sources for: {sorted(missing)}")
|
|
159
|
+
src_ids = {s["source_id"] for s in sources}
|
|
160
|
+
# a Study citing multiple sources must import them all (transitive
|
|
161
|
+
# closure) so cross-entity validation never sees a dangling reference
|
|
162
|
+
studies = [s for s in snapshot["studies"] if set(s["source_ids"]) & src_ids]
|
|
163
|
+
for s in studies:
|
|
164
|
+
extra = set(s["source_ids"]) - src_ids
|
|
165
|
+
if extra:
|
|
166
|
+
sources.extend(
|
|
167
|
+
src for src in snapshot["sources"] if src["source_id"] in extra)
|
|
168
|
+
src_ids.update(extra)
|
|
169
|
+
study_ids = {s["study_id"] for s in studies}
|
|
170
|
+
findings = [f for f in snapshot["findings"] if f["study_id"] in study_ids]
|
|
171
|
+
audits = [a for a in snapshot["audits"] if a["study_id"] in study_ids]
|
|
172
|
+
|
|
173
|
+
lib_rev = self.active_revision()
|
|
174
|
+
|
|
175
|
+
# origin metadata per table: record library revision + entity id so the
|
|
176
|
+
def stamp(table: str, rec: dict) -> dict:
|
|
177
|
+
out = dict(rec)
|
|
178
|
+
ext = dict(out.get("extensions") or {})
|
|
179
|
+
ext["origin"] = {
|
|
180
|
+
"library_revision": lib_rev,
|
|
181
|
+
"library_entity_id": out[_TABLE_ID_KEY[table]],
|
|
182
|
+
"content_hash": hashlib.sha256(
|
|
183
|
+
json.dumps(out, sort_keys=True, separators=(",", ":"))
|
|
184
|
+
.encode("utf-8")).hexdigest(),
|
|
185
|
+
"imported_at": _now_iso(),
|
|
186
|
+
}
|
|
187
|
+
out["extensions"] = ext
|
|
188
|
+
return out
|
|
189
|
+
store = GraphStore.create(project)
|
|
190
|
+
existing_outcomes = {o["outcome_id"] for o in store.read_table("outcomes")}
|
|
191
|
+
outcomes: list[dict] = []
|
|
192
|
+
for f in findings:
|
|
193
|
+
oid = f["outcome_id"]
|
|
194
|
+
if oid in existing_outcomes:
|
|
195
|
+
continue
|
|
196
|
+
existing_outcomes.add(oid)
|
|
197
|
+
outcomes.append({
|
|
198
|
+
"outcome_id": oid,
|
|
199
|
+
"name": f.get("measure", oid),
|
|
200
|
+
"outcome_type": (f.get("extensions") or {}).get(
|
|
201
|
+
"outcome_type", "learning"),
|
|
202
|
+
"extensions": {
|
|
203
|
+
"auto_created_from_library_import": True,
|
|
204
|
+
"library_revision": lib_rev,
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
mutation = GraphMutation(
|
|
208
|
+
upserts={
|
|
209
|
+
"sources": [stamp("sources", s) for s in sources],
|
|
210
|
+
"studies": [stamp("studies", s) for s in studies],
|
|
211
|
+
"findings": [stamp("findings", f) for f in findings],
|
|
212
|
+
"audits": [stamp("audits", a) for a in audits],
|
|
213
|
+
"outcomes": outcomes,
|
|
214
|
+
},
|
|
215
|
+
retire_ids={},
|
|
216
|
+
)
|
|
217
|
+
return store.commit(run_id=run_id, reason="library snapshot import", mutation=mutation)
|
|
218
|
+
|
|
219
|
+
def diff_project_snapshot(self, *, project, source_ids: list[str]) -> dict:
|
|
220
|
+
"""Diff imported facts vs the current library revision.
|
|
221
|
+
|
|
222
|
+
Compares every library entity reachable from the selected source_ids
|
|
223
|
+
(sources/studies/findings/audits) against the Project graph's copy
|
|
224
|
+
(ignoring import origin metadata). Returns added/changed/removed
|
|
225
|
+
entity ids so the caller can decide whether an explicit sync is
|
|
226
|
+
warranted.
|
|
227
|
+
"""
|
|
228
|
+
snapshot = self._read_snapshot(self.active_revision())
|
|
229
|
+
src_ids = set(source_ids)
|
|
230
|
+
lib_sources = {s["source_id"]: s for s in snapshot["sources"] if s["source_id"] in src_ids}
|
|
231
|
+
lib_studies = {s["study_id"]: s for s in snapshot["studies"]
|
|
232
|
+
if set(s["source_ids"]) & src_ids}
|
|
233
|
+
lib_findings = {f["finding_id"]: f for f in snapshot["findings"]
|
|
234
|
+
if f["study_id"] in lib_studies}
|
|
235
|
+
lib_audits = {a["audit_id"]: a for a in snapshot["audits"]
|
|
236
|
+
if a["study_id"] in lib_studies}
|
|
237
|
+
lib_entities: dict[str, tuple[str, dict]] = {}
|
|
238
|
+
for table, ents in (("sources", lib_sources), ("studies", lib_studies),
|
|
239
|
+
("findings", lib_findings), ("audits", lib_audits)):
|
|
240
|
+
for eid, rec in ents.items():
|
|
241
|
+
lib_entities[eid] = (table, rec)
|
|
242
|
+
|
|
243
|
+
store = GraphStore.create(project)
|
|
244
|
+
proj_by_table = {
|
|
245
|
+
t: {r[_TABLE_ID_KEY[t]]: r for r in store.read_table(t)}
|
|
246
|
+
for t in LIBRARY_TABLES
|
|
247
|
+
}
|
|
248
|
+
diff: dict = {"added": [], "changed": [], "removed": []}
|
|
249
|
+
for eid, (table, lib_rec) in lib_entities.items():
|
|
250
|
+
proj_rec = proj_by_table[table].get(eid)
|
|
251
|
+
if proj_rec is None:
|
|
252
|
+
diff["added"].append(eid)
|
|
253
|
+
continue
|
|
254
|
+
lib_content = {k: v for k, v in lib_rec.items() if k != "extensions"}
|
|
255
|
+
proj_content = {k: v for k, v in proj_rec.items() if k != "extensions"}
|
|
256
|
+
if lib_content != proj_content:
|
|
257
|
+
diff["changed"].append(eid)
|
|
258
|
+
for table in LIBRARY_TABLES:
|
|
259
|
+
for eid, rec in proj_by_table[table].items():
|
|
260
|
+
if eid in lib_entities:
|
|
261
|
+
continue
|
|
262
|
+
# only entities imported FROM this library may be reported
|
|
263
|
+
# removed; project-local entities are never "removed" by a
|
|
264
|
+
# library diff
|
|
265
|
+
origin = ((rec.get("extensions") or {}).get("origin") or {})
|
|
266
|
+
if origin.get("library_revision") is not None:
|
|
267
|
+
diff["removed"].append(eid)
|
|
268
|
+
return diff
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Builtin evidence library + offline preliminary verdict (v4).
|
|
2
|
+
|
|
3
|
+
Two entry points:
|
|
4
|
+
|
|
5
|
+
load_builtin_library() -> read benchmarks/evidence-library.json, validate
|
|
6
|
+
structurally, cache (lru_cache), return a deep
|
|
7
|
+
copy. Raises FileNotFoundError when the library
|
|
8
|
+
has not been built yet and ValueError when the
|
|
9
|
+
shape is invalid.
|
|
10
|
+
|
|
11
|
+
preliminary_verdict(question, *, top_k=10)
|
|
12
|
+
-> deterministic offline screening verdict. Matching
|
|
13
|
+
is Chinese-keyword / outcome-token based using
|
|
14
|
+
CJK bigram overlap (self-implemented; mirrors the
|
|
15
|
+
logic of scripts/benchmark_evaluator.py without
|
|
16
|
+
importing it): the question is tokenized into CJK
|
|
17
|
+
bigrams + English words, each library entry is
|
|
18
|
+
scored by bigram overlap over
|
|
19
|
+
claim_text + effect_summary + title, and the
|
|
20
|
+
top_k entries above MATCH_THRESHOLD with at least
|
|
21
|
+
MIN_SHARED_BIGRAMS shared tokens count as matched.
|
|
22
|
+
|
|
23
|
+
Conservative verdict rules (offline preliminary gate):
|
|
24
|
+
any matched contradict entry -> reject
|
|
25
|
+
else any matched support entry -> pilot
|
|
26
|
+
else -> insufficient_evidence
|
|
27
|
+
adopt is NEVER returned by the preliminary gate.
|
|
28
|
+
|
|
29
|
+
Output:
|
|
30
|
+
{"verdict": ..., "coverage": {"matched_entries": [...],
|
|
31
|
+
"matched_outcome_tokens": [...], "note": "..."},
|
|
32
|
+
"preliminary": True, "library_version": ...}
|
|
33
|
+
|
|
34
|
+
This module is stdlib-only (consistent with engine/ "Native Core" policy).
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import re
|
|
40
|
+
from copy import deepcopy
|
|
41
|
+
from functools import lru_cache
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
46
|
+
def _resolve_library_path() -> Path:
|
|
47
|
+
"""Repository layout first; wheel-installed share/ layout as fallback."""
|
|
48
|
+
repo = ROOT / "benchmarks" / "evidence-library.json"
|
|
49
|
+
if repo.is_file():
|
|
50
|
+
return repo
|
|
51
|
+
import sys
|
|
52
|
+
share = Path(sys.prefix) / "share" / "eduevidence" / "benchmarks" / "evidence-library.json"
|
|
53
|
+
if share.is_file():
|
|
54
|
+
return share
|
|
55
|
+
return repo
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
LIBRARY_PATH = _resolve_library_path()
|
|
59
|
+
|
|
60
|
+
# --- matching knobs (conservative) ---
|
|
61
|
+
MATCH_THRESHOLD = 0.30 # min bigram-overlap ratio (intersection / min sizes)
|
|
62
|
+
MIN_SHARED_BIGRAMS = 2 # min absolute shared tokens (blocks tiny-query over-match)
|
|
63
|
+
|
|
64
|
+
VERDICT_ORDER = ("adopt", "pilot", "reject", "insufficient_evidence")
|
|
65
|
+
DIRECTIONS = ("support", "contradict", "neutral")
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# tokenization (self-implemented, mirrors scripts/benchmark_evaluator.py)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
_ID_RE = re.compile(r"\b([A-Za-z][A-Za-z-]{1,40})\b")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cjk_bigrams(text: str) -> set[str]:
|
|
74
|
+
chars = [c for c in text if "\u4e00" <= c <= "\u9fff"]
|
|
75
|
+
return {chars[i] + chars[i + 1] for i in range(len(chars) - 1)}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _words(text: str) -> set[str]:
|
|
79
|
+
return {w.lower() for w in _ID_RE.findall(text) if len(w) > 2}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _tokenize(text: str) -> set[str]:
|
|
83
|
+
return _cjk_bigrams(text) | _words(text)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _overlap_tokens(ta: set[str], tb: set[str]) -> float:
|
|
87
|
+
if not ta or not tb:
|
|
88
|
+
return 0.0
|
|
89
|
+
return len(ta & tb) / max(1, min(len(ta), len(tb)))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# outcome-token detection (Chinese terms + direct English taxonomy words)
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
_OUTCOME_TAXONOMY = {
|
|
96
|
+
"accuracy", "assignment_score", "retention", "transfer",
|
|
97
|
+
"independent_problem_solving", "completion_time", "cognitive_load",
|
|
98
|
+
"knowledge_gain", "concept_understanding", "engagement", "motivation",
|
|
99
|
+
"metacognition", "help_seeking", "code_quality", "ai_dependency",
|
|
100
|
+
"over_reliance", "reduced_effort", "reduced_transfer",
|
|
101
|
+
"academic_integrity_risk", "false_confidence",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_CN_OUTCOME_TERMS = {
|
|
105
|
+
"正确率": "accuracy", "准确率": "accuracy", "正确性": "accuracy",
|
|
106
|
+
"期末考试成绩": "assignment_score", "考试成绩": "assignment_score",
|
|
107
|
+
"作业得分": "assignment_score", "作业成绩": "assignment_score",
|
|
108
|
+
"成绩": "assignment_score", "得分": "assignment_score",
|
|
109
|
+
"记忆保持": "retention", "保持率": "retention", "保持": "retention",
|
|
110
|
+
"保留": "retention", "记忆": "retention",
|
|
111
|
+
"迁移能力": "transfer", "迁移": "transfer",
|
|
112
|
+
"独立问题解决": "independent_problem_solving",
|
|
113
|
+
"独立写作": "independent_problem_solving", "独立编程": "independent_problem_solving",
|
|
114
|
+
"独立解题": "independent_problem_solving", "脱离工具": "independent_problem_solving",
|
|
115
|
+
"无AI情境": "independent_problem_solving", "独立": "independent_problem_solving",
|
|
116
|
+
"任务完成时间": "completion_time", "完成时间": "completion_time",
|
|
117
|
+
"速度": "completion_time",
|
|
118
|
+
"认知负荷": "cognitive_load", "负荷": "cognitive_load",
|
|
119
|
+
"知识获得": "knowledge_gain", "知识": "knowledge_gain",
|
|
120
|
+
"概念理解": "concept_understanding", "概念": "concept_understanding",
|
|
121
|
+
"参与度": "engagement", "参与": "engagement", "投入": "engagement",
|
|
122
|
+
"学习动机": "motivation", "动机": "motivation", "兴趣": "motivation",
|
|
123
|
+
"元认知": "metacognition",
|
|
124
|
+
"求助行为": "help_seeking", "求助": "help_seeking",
|
|
125
|
+
"代码质量": "code_quality",
|
|
126
|
+
"过度依赖": "over_reliance", "AI依赖": "ai_dependency",
|
|
127
|
+
"依赖": "ai_dependency",
|
|
128
|
+
"减少努力": "reduced_effort", "努力": "reduced_effort",
|
|
129
|
+
"迁移受损": "reduced_transfer", "迁移下降": "reduced_transfer",
|
|
130
|
+
"学术诚信": "academic_integrity_risk", "诚信": "academic_integrity_risk",
|
|
131
|
+
"作弊": "academic_integrity_risk", "原创性": "academic_integrity_risk",
|
|
132
|
+
"虚假自信": "false_confidence",
|
|
133
|
+
}
|
|
134
|
+
# longest phrase first so "保持率" wins over "保持", "期末考试成绩" over "成绩", ...
|
|
135
|
+
_CN_OUTCOME_ORDERED = sorted(_CN_OUTCOME_TERMS.items(), key=lambda kv: -len(kv[0]))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _detect_outcome_tokens(question: str) -> set[str]:
|
|
139
|
+
tokens: set[str] = set()
|
|
140
|
+
for phrase, outcome in _CN_OUTCOME_ORDERED:
|
|
141
|
+
if phrase in question:
|
|
142
|
+
tokens.add(outcome)
|
|
143
|
+
tokens |= {w for w in _words(question) if w in _OUTCOME_TAXONOMY}
|
|
144
|
+
return tokens
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# library loading: validate + cache
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
_TOP_REQUIRED = {"library_id", "version", "generated_at", "entries", "coverage_note"}
|
|
151
|
+
_ENTRY_REQUIRED = {
|
|
152
|
+
"entry_id", "source_id", "title", "year", "outcome_token", "direction",
|
|
153
|
+
"study_type", "claim_text", "effect_summary", "confidence_markers", "domains",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _validate_library_shape(lib: dict[str, Any]) -> None:
|
|
158
|
+
"""Structural validation of the builtin library (stdlib-only).
|
|
159
|
+
|
|
160
|
+
Keeps the engine dependency-free; the full JSON-Schema check lives in the
|
|
161
|
+
build script (scripts/build_evidence_library.py -> validate_schema.Validator).
|
|
162
|
+
"""
|
|
163
|
+
if not isinstance(lib, dict):
|
|
164
|
+
raise ValueError("builtin library must be a JSON object")
|
|
165
|
+
missing = _TOP_REQUIRED - lib.keys()
|
|
166
|
+
if missing:
|
|
167
|
+
raise ValueError(f"builtin library missing required fields: {sorted(missing)}")
|
|
168
|
+
entries = lib.get("entries")
|
|
169
|
+
if not isinstance(entries, list) or not entries:
|
|
170
|
+
raise ValueError("builtin library entries must be a non-empty list")
|
|
171
|
+
for idx, entry in enumerate(entries):
|
|
172
|
+
if not isinstance(entry, dict):
|
|
173
|
+
raise ValueError(f"entry[{idx}] must be an object")
|
|
174
|
+
miss = _ENTRY_REQUIRED - entry.keys()
|
|
175
|
+
if miss:
|
|
176
|
+
raise ValueError(f"entry[{idx}] missing required fields: {sorted(miss)}")
|
|
177
|
+
if entry["direction"] not in DIRECTIONS:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"entry[{idx}] direction {entry['direction']!r} not in {sorted(DIRECTIONS)}"
|
|
180
|
+
)
|
|
181
|
+
for field in ("entry_id", "source_id", "title", "outcome_token",
|
|
182
|
+
"claim_text", "effect_summary", "study_type"):
|
|
183
|
+
if not isinstance(entry[field], str) or not entry[field].strip():
|
|
184
|
+
raise ValueError(f"entry[{idx}] {field!r} must be a non-empty string")
|
|
185
|
+
if entry["year"] is not None and (
|
|
186
|
+
not isinstance(entry["year"], int) or isinstance(entry["year"], bool)
|
|
187
|
+
):
|
|
188
|
+
raise ValueError(f"entry[{idx}] year must be integer or null")
|
|
189
|
+
if not isinstance(entry["confidence_markers"], list):
|
|
190
|
+
raise ValueError(f"entry[{idx}] confidence_markers must be a list")
|
|
191
|
+
if not isinstance(entry["domains"], list) or not entry["domains"]:
|
|
192
|
+
raise ValueError(f"entry[{idx}] domains must be a non-empty list")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@lru_cache(maxsize=1)
|
|
196
|
+
def _read_library() -> dict[str, Any]:
|
|
197
|
+
if not LIBRARY_PATH.is_file():
|
|
198
|
+
raise FileNotFoundError(
|
|
199
|
+
f"builtin evidence library not found: {LIBRARY_PATH}; "
|
|
200
|
+
"run 'python scripts/build_evidence_library.py' first"
|
|
201
|
+
)
|
|
202
|
+
lib = json.loads(LIBRARY_PATH.read_text(encoding="utf-8"))
|
|
203
|
+
_validate_library_shape(lib)
|
|
204
|
+
return lib
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def load_builtin_library() -> dict[str, Any]:
|
|
208
|
+
"""Load (validated + cached) builtin library; returns a deep copy."""
|
|
209
|
+
return deepcopy(_read_library())
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
# preliminary verdict
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
def _entry_text(entry: dict[str, Any]) -> str:
|
|
216
|
+
return " ".join(
|
|
217
|
+
str(entry.get(k) or "")
|
|
218
|
+
for k in ("claim_text", "effect_summary", "title")
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def preliminary_verdict(question: str, *, top_k: int = 10) -> dict[str, Any]:
|
|
223
|
+
"""Offline conservative preliminary verdict for a (Chinese) education question.
|
|
224
|
+
|
|
225
|
+
Matching: CJK bigram overlap between the question and each entry's
|
|
226
|
+
claim_text + effect_summary + title; the top_k entries are considered and an
|
|
227
|
+
entry counts as matched when overlap >= MATCH_THRESHOLD and it shares at
|
|
228
|
+
least MIN_SHARED_BIGRAMS tokens. Verdict: contradict => reject, else
|
|
229
|
+
support => pilot, else insufficient_evidence. Never adopt. Never crashes on
|
|
230
|
+
empty/blank questions.
|
|
231
|
+
"""
|
|
232
|
+
try:
|
|
233
|
+
top_k = int(top_k)
|
|
234
|
+
except (TypeError, ValueError):
|
|
235
|
+
top_k = 10
|
|
236
|
+
lib = _read_library()
|
|
237
|
+
question = (question or "").strip()
|
|
238
|
+
q_tokens = _tokenize(question)
|
|
239
|
+
q_outcomes = _detect_outcome_tokens(question)
|
|
240
|
+
|
|
241
|
+
scored: list[tuple[float, int, bool, dict[str, Any]]] = []
|
|
242
|
+
for entry in lib["entries"]:
|
|
243
|
+
e_tokens = _tokenize(_entry_text(entry))
|
|
244
|
+
base = _overlap_tokens(q_tokens, e_tokens)
|
|
245
|
+
shared = len(q_tokens & e_tokens)
|
|
246
|
+
# multi-outcome entries match when ANY of their tokens is in the question
|
|
247
|
+
entry_tokens = set(entry.get("outcome_tokens") or [entry.get("outcome_token")])
|
|
248
|
+
has_outcome = bool(entry_tokens & q_outcomes)
|
|
249
|
+
scored.append((base, shared, has_outcome, entry))
|
|
250
|
+
|
|
251
|
+
scored.sort(key=lambda t: (-t[0], -t[1], -int(t[2]), t[3].get("entry_id", "")))
|
|
252
|
+
top = scored[: max(0, int(top_k))]
|
|
253
|
+
|
|
254
|
+
matched = [
|
|
255
|
+
entry
|
|
256
|
+
for base, shared, _has_outcome, entry in top
|
|
257
|
+
if base >= MATCH_THRESHOLD and shared >= MIN_SHARED_BIGRAMS
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
directions = {entry["direction"] for entry in matched}
|
|
261
|
+
if "contradict" in directions:
|
|
262
|
+
verdict = "reject"
|
|
263
|
+
elif "support" in directions:
|
|
264
|
+
verdict = "pilot"
|
|
265
|
+
else:
|
|
266
|
+
verdict = "insufficient_evidence"
|
|
267
|
+
|
|
268
|
+
note = _build_note(matched, directions, q_outcomes, top_k)
|
|
269
|
+
return {
|
|
270
|
+
"verdict": verdict,
|
|
271
|
+
"coverage": {
|
|
272
|
+
"matched_entries": [entry["entry_id"] for entry in matched],
|
|
273
|
+
"matched_outcome_tokens": sorted(q_outcomes),
|
|
274
|
+
"note": note,
|
|
275
|
+
},
|
|
276
|
+
"preliminary": True,
|
|
277
|
+
"library_version": lib.get("version", ""),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _build_note(
|
|
282
|
+
matched: list[dict[str, Any]],
|
|
283
|
+
directions: set[str],
|
|
284
|
+
q_outcomes: set[str],
|
|
285
|
+
top_k: int,
|
|
286
|
+
) -> str:
|
|
287
|
+
outcome_str = "、".join(sorted(q_outcomes)) or "无"
|
|
288
|
+
if not matched:
|
|
289
|
+
return (
|
|
290
|
+
f"离线初步裁决未匹配到内置证据(阈值:bigram overlap≥{MATCH_THRESHOLD} 且"
|
|
291
|
+
f"共享 bigram≥{MIN_SHARED_BIGRAMS});检测到结局词:{outcome_str}。"
|
|
292
|
+
"建议进入在线证据检索流程进一步核实。"
|
|
293
|
+
)
|
|
294
|
+
counts = {d: sum(1 for e in matched if e["direction"] == d) for d in DIRECTIONS}
|
|
295
|
+
return (
|
|
296
|
+
f"离线初步裁决在 top_k={top_k} 内匹配到 {len(matched)} 条内置证据:"
|
|
297
|
+
f"support={counts['support']}、contradict={counts['contradict']}、"
|
|
298
|
+
f"neutral={counts['neutral']};匹配结局词:{outcome_str}。"
|
|
299
|
+
"本裁决为初步(preliminary=true)且保守,从不直接给出 adopt,"
|
|
300
|
+
"建议结合完整证据库与在线检索复核。"
|
|
301
|
+
)
|