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,311 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""build_result.py — Assemble result.json from an example research pack.
|
|
3
|
+
|
|
4
|
+
result.json is the validated research-core output (总体实施计划 §27) consumed by
|
|
5
|
+
the visualization layer. It aggregates the pack files:
|
|
6
|
+
|
|
7
|
+
frame.json + evidence.jsonl + methodology.json + verdict.json
|
|
8
|
+
+ intervention.json + evaluation.json (+ optional sources.jsonl)
|
|
9
|
+
|
|
10
|
+
and computes the outcome-level aggregation and claim rows (Claim-Evidence
|
|
11
|
+
Contract). The report layer consumes ONLY this file and never edits it.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 scripts/build_result.py --pack examples/ai-coding-assistant \
|
|
15
|
+
--out examples/ai-coding-assistant/result.json
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
# Repo-root bootstrap so `python3 scripts/build_result.py` works without venv
|
|
27
|
+
# install (removed by the src-layout migration, plan item E1).
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
29
|
+
|
|
30
|
+
from evidence_semantics import effect_direction
|
|
31
|
+
from engine.versions import ENGINE_VERSION
|
|
32
|
+
|
|
33
|
+
OUTCOME_ORDER = [
|
|
34
|
+
"knowledge_gain", "concept_understanding", "retention", "transfer",
|
|
35
|
+
"independent_problem_solving", "completion_time", "accuracy",
|
|
36
|
+
"code_quality", "assignment_score", "engagement", "motivation",
|
|
37
|
+
"cognitive_load", "help_seeking", "metacognition", "ai_dependency",
|
|
38
|
+
"over_reliance", "reduced_effort", "reduced_transfer",
|
|
39
|
+
"academic_integrity_risk", "false_confidence",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_json(path: Path) -> dict[str, Any] | None:
|
|
44
|
+
if path.exists():
|
|
45
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return []
|
|
52
|
+
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _derive_source_from_evidence(ev: dict[str, Any]) -> dict[str, Any] | None:
|
|
56
|
+
"""Honest source fallback (review P1-2): a real DOI in the location
|
|
57
|
+
yields tier1; otherwise the lowest authority is assumed and the record
|
|
58
|
+
is flagged source_metadata_incomplete. Never fabricates a canonical URL."""
|
|
59
|
+
from retrieval.source import parse_doi_from_url # noqa: PLC0415
|
|
60
|
+
|
|
61
|
+
loc = (ev.get("source_location") or "").strip()
|
|
62
|
+
if not loc:
|
|
63
|
+
return None
|
|
64
|
+
doi = parse_doi_from_url(loc)
|
|
65
|
+
authority = "tier1_paper_doi" if doi else "tier5_general_web"
|
|
66
|
+
return {
|
|
67
|
+
"source_id": ev.get("source_id", ""),
|
|
68
|
+
"title": ev.get("title", ""),
|
|
69
|
+
"year": ev.get("year"),
|
|
70
|
+
"canonical_url": loc,
|
|
71
|
+
"authority_level": authority,
|
|
72
|
+
"source_location": loc,
|
|
73
|
+
"extensions": {"source_metadata_incomplete": not doi},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
NOT_CAPTURED_USAGE: dict[str, Any] = {
|
|
78
|
+
"measurement_status": "NOT_CAPTURED",
|
|
79
|
+
"input_tokens": None,
|
|
80
|
+
"output_tokens": None,
|
|
81
|
+
"cost_usd": None,
|
|
82
|
+
"latency_s": None,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def derive_provenance(sources: list[dict[str, Any]]) -> dict[str, Any]:
|
|
87
|
+
"""Aggregate provenance from real Source.fetch records (P1-3).
|
|
88
|
+
|
|
89
|
+
fetched_at only appears when an actual fetch recorded it (sources[].fetch.
|
|
90
|
+
fetched_at); the result-assembly time is reported as meta.generated_at and
|
|
91
|
+
must never be passed off as a fetch time. Unknown values stay unknown.
|
|
92
|
+
"""
|
|
93
|
+
fetched = [
|
|
94
|
+
s["fetch"]["fetched_at"] for s in sources
|
|
95
|
+
if isinstance(s.get("fetch"), dict) and s["fetch"].get("fetched_at")
|
|
96
|
+
]
|
|
97
|
+
providers = sorted({
|
|
98
|
+
s["fetch"]["fetch_provider"] for s in sources
|
|
99
|
+
if isinstance(s.get("fetch"), dict) and s["fetch"].get("fetch_provider")
|
|
100
|
+
})
|
|
101
|
+
provenance: dict[str, Any] = {"search_provider": ", ".join(providers) if providers else "n/a"}
|
|
102
|
+
if fetched:
|
|
103
|
+
provenance["fetched_at"] = min(fetched)
|
|
104
|
+
return provenance
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def aggregate_outcomes(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
108
|
+
"""Outcome-level aggregation keyed by effect_direction (C-1).
|
|
109
|
+
|
|
110
|
+
Outcome rows carry positive_count / negative_count / null_count (based on
|
|
111
|
+
effect_direction) — not support/contradict counts, which are claim-level
|
|
112
|
+
semantics and only belong in the claim trace (relation_to_claim).
|
|
113
|
+
"""
|
|
114
|
+
by_outcome: dict[str, dict[str, Any]] = {}
|
|
115
|
+
for ev in evidence:
|
|
116
|
+
outcome = ev.get("outcome_type", "unknown")
|
|
117
|
+
row = by_outcome.setdefault(outcome, {
|
|
118
|
+
"outcome_type": outcome, "positive_count": 0,
|
|
119
|
+
"negative_count": 0, "null_count": 0, "evidence_ids": [],
|
|
120
|
+
})
|
|
121
|
+
effect = effect_direction(ev)
|
|
122
|
+
if effect == "positive":
|
|
123
|
+
row["positive_count"] += 1
|
|
124
|
+
elif effect == "negative":
|
|
125
|
+
row["negative_count"] += 1
|
|
126
|
+
else:
|
|
127
|
+
row["null_count"] += 1
|
|
128
|
+
row["evidence_ids"].append(ev.get("evidence_id", ""))
|
|
129
|
+
return [by_outcome[o] for o in OUTCOME_ORDER if o in by_outcome]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_outcome_mapping(evidence: list[dict[str, Any]],
|
|
133
|
+
frame: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
134
|
+
"""Frame-declared vs evidence-covered outcome map (OPEN-2).
|
|
135
|
+
|
|
136
|
+
Every outcome type seen in the frame or in evidence gets exactly one
|
|
137
|
+
entry with directional counts (support / contradict / neutral -- never
|
|
138
|
+
mixed, consistent with the three-column Evidence Matrix) and an explicit
|
|
139
|
+
status:
|
|
140
|
+
|
|
141
|
+
supported / contested / contradicted / null_evidence_only / no_evidence
|
|
142
|
+
|
|
143
|
+
Frame-declared outcomes with no covering evidence are surfaced in
|
|
144
|
+
declared_without_evidence so a decision must disclose its gaps.
|
|
145
|
+
"""
|
|
146
|
+
declared: set[str] = set()
|
|
147
|
+
for group in ("primary", "secondary", "risk"):
|
|
148
|
+
declared.update((frame or {}).get("outcomes", {}).get(group, []) or [])
|
|
149
|
+
declared.discard("")
|
|
150
|
+
|
|
151
|
+
by_outcome: dict[str, dict[str, Any]] = {}
|
|
152
|
+
for ev in evidence:
|
|
153
|
+
outcome = ev.get("outcome_type") or "unknown"
|
|
154
|
+
row = by_outcome.setdefault(outcome, {
|
|
155
|
+
"outcome_type": outcome,
|
|
156
|
+
"support_count": 0, "contradict_count": 0, "neutral_count": 0,
|
|
157
|
+
"evidence_ids": [],
|
|
158
|
+
})
|
|
159
|
+
direction = ev.get("direction", "neutral")
|
|
160
|
+
if direction == "support":
|
|
161
|
+
row["support_count"] += 1
|
|
162
|
+
elif direction == "contradict":
|
|
163
|
+
row["contradict_count"] += 1
|
|
164
|
+
else:
|
|
165
|
+
row["neutral_count"] += 1
|
|
166
|
+
if ev.get("evidence_id"):
|
|
167
|
+
row["evidence_ids"].append(ev["evidence_id"])
|
|
168
|
+
|
|
169
|
+
entries: list[dict[str, Any]] = []
|
|
170
|
+
for outcome in sorted(set(declared) | set(by_outcome)):
|
|
171
|
+
row = by_outcome.get(outcome) or {
|
|
172
|
+
"outcome_type": outcome, "support_count": 0,
|
|
173
|
+
"contradict_count": 0, "neutral_count": 0, "evidence_ids": []}
|
|
174
|
+
s, c, n = row["support_count"], row["contradict_count"], row["neutral_count"]
|
|
175
|
+
if s > 0 and c > 0:
|
|
176
|
+
status = "contested"
|
|
177
|
+
elif s > 0:
|
|
178
|
+
status = "supported"
|
|
179
|
+
elif c > 0:
|
|
180
|
+
status = "contradicted"
|
|
181
|
+
elif n > 0:
|
|
182
|
+
status = "null_evidence_only"
|
|
183
|
+
else:
|
|
184
|
+
status = "no_evidence"
|
|
185
|
+
entries.append({
|
|
186
|
+
"outcome_type": outcome,
|
|
187
|
+
"declared_in_frame": outcome in declared,
|
|
188
|
+
"status": status,
|
|
189
|
+
"support_count": s,
|
|
190
|
+
"contradict_count": c,
|
|
191
|
+
"neutral_count": n,
|
|
192
|
+
"evidence_ids": list(row["evidence_ids"]),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
declared_without_evidence = sorted(d for d in declared if d not in by_outcome)
|
|
196
|
+
return {"entries": entries, "declared_without_evidence": declared_without_evidence}
|
|
197
|
+
|
|
198
|
+
def build_claims(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
199
|
+
"""Claim-level rows bound to evidence (Claim-Evidence Contract, A-3).
|
|
200
|
+
|
|
201
|
+
claim_id comes from the evidence objects themselves (first-class field of
|
|
202
|
+
the Evidence Contract), so Claim -> Evidence -> Source never depends on
|
|
203
|
+
render-time renumbering. Legacy evidence without claim_id falls back to a
|
|
204
|
+
deterministic C-{idx:03d} sequence.
|
|
205
|
+
"""
|
|
206
|
+
claims: dict[str, dict[str, Any]] = {}
|
|
207
|
+
for idx, ev in enumerate(evidence, 1):
|
|
208
|
+
claim = ev.get("claim", "")
|
|
209
|
+
outcome = ev.get("outcome_type", "")
|
|
210
|
+
key = (claim, outcome)
|
|
211
|
+
row = claims.setdefault(key, {
|
|
212
|
+
"claim": claim, "outcome_type": outcome,
|
|
213
|
+
"claim_id": ev.get("claim_id") or f"C-{idx:03d}",
|
|
214
|
+
"evidence_ids": [], "status": "SUPPORTED",
|
|
215
|
+
})
|
|
216
|
+
row["evidence_ids"].append(ev.get("evidence_id", ""))
|
|
217
|
+
status = ev.get("status")
|
|
218
|
+
rank = {"CONTRADICT": 3, "UNSUPPORTED": 2, "DOWNGRADE_CONFIDENCE": 1}.get(status, 0)
|
|
219
|
+
if rank > {"CONTRADICT": 3, "UNSUPPORTED": 2, "DOWNGRADE_CONFIDENCE": 1}.get(row["status"], 0):
|
|
220
|
+
row["status"] = status
|
|
221
|
+
return list(claims.values())
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_result(pack_dir: Path, *, mode: str = "platform_native") -> dict[str, Any]:
|
|
225
|
+
frame = _load_json(pack_dir / "frame.json") or {}
|
|
226
|
+
evidence = _load_jsonl(pack_dir / "evidence.jsonl")
|
|
227
|
+
# methodology.json is a single MethodologyAudit object (or a JSONL list)
|
|
228
|
+
methodology_single = _load_json(pack_dir / "methodology.json")
|
|
229
|
+
methodology = [methodology_single] if methodology_single else _load_jsonl(pack_dir / "methodology.jsonl")
|
|
230
|
+
verdict = _load_json(pack_dir / "verdict.json") or {}
|
|
231
|
+
intervention = _load_json(pack_dir / "intervention.json") or {}
|
|
232
|
+
evaluation = _load_json(pack_dir / "evaluation.json") or {}
|
|
233
|
+
sources = _load_jsonl(pack_dir / "sources.jsonl")
|
|
234
|
+
|
|
235
|
+
if not sources:
|
|
236
|
+
seen: dict[str, dict[str, Any]] = {}
|
|
237
|
+
for ev in evidence:
|
|
238
|
+
sid = ev.get("source_id", "")
|
|
239
|
+
if sid and sid not in seen:
|
|
240
|
+
derived = _derive_source_from_evidence(ev)
|
|
241
|
+
if derived is not None:
|
|
242
|
+
seen[sid] = derived
|
|
243
|
+
sources = list(seen.values())
|
|
244
|
+
|
|
245
|
+
decision = verdict
|
|
246
|
+
# execution summary derived from pack (complexity via frame target/depth is optional)
|
|
247
|
+
execution = {
|
|
248
|
+
"complexity": frame.get("complexity", "M"),
|
|
249
|
+
"mode": mode,
|
|
250
|
+
"agents": [],
|
|
251
|
+
"usage": dict(NOT_CAPTURED_USAGE),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
result = {
|
|
255
|
+
"meta": {
|
|
256
|
+
"skill": "eduevidence",
|
|
257
|
+
"version": ENGINE_VERSION,
|
|
258
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
259
|
+
"mode": mode,
|
|
260
|
+
"question": frame.get("question", ""),
|
|
261
|
+
},
|
|
262
|
+
"execution": execution,
|
|
263
|
+
"research_frame": frame,
|
|
264
|
+
"decision": decision,
|
|
265
|
+
"outcomes": aggregate_outcomes(evidence),
|
|
266
|
+
"outcome_mapping": build_outcome_mapping(evidence, frame),
|
|
267
|
+
"claims": build_claims(evidence),
|
|
268
|
+
"sources": sources,
|
|
269
|
+
"evidence": evidence,
|
|
270
|
+
"methodology_reviews": methodology,
|
|
271
|
+
"conflicts": [{"reason_for_disagreement": verdict.get("reason_for_disagreement", "")}]
|
|
272
|
+
if verdict.get("reason_for_disagreement") else [],
|
|
273
|
+
"applicability": verdict.get("applicability", {}),
|
|
274
|
+
"intervention": intervention,
|
|
275
|
+
"evaluation": evaluation,
|
|
276
|
+
"benchmark": {},
|
|
277
|
+
"provenance": derive_provenance(sources),
|
|
278
|
+
}
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main() -> int:
|
|
283
|
+
parser = argparse.ArgumentParser(description="Assemble result.json from an example pack")
|
|
284
|
+
parser.add_argument("--pack", required=True, help="example pack directory")
|
|
285
|
+
parser.add_argument("--out", required=True, help="output result.json path")
|
|
286
|
+
parser.add_argument("--mode", choices=["platform_native", "agent_mcp_enhanced"],
|
|
287
|
+
default="platform_native")
|
|
288
|
+
parser.add_argument("--data-origin", choices=["real", "synthetic", "hybrid", "manual_curated"],
|
|
289
|
+
default=None,
|
|
290
|
+
help="provenance of the pack data (plan R4); rendered as a badge in reports")
|
|
291
|
+
args = parser.parse_args()
|
|
292
|
+
|
|
293
|
+
result = build_result(Path(args.pack), mode=args.mode)
|
|
294
|
+
if args.data_origin:
|
|
295
|
+
result["meta"]["data_origin"] = args.data_origin
|
|
296
|
+
# keep the bilingual projection in lockstep with the English source
|
|
297
|
+
out = Path(args.out)
|
|
298
|
+
zh_path = out.parent / "result.zh.json"
|
|
299
|
+
if zh_path.exists():
|
|
300
|
+
zh_result = json.loads(zh_path.read_text(encoding="utf-8"))
|
|
301
|
+
zh_result.setdefault("meta", {})["data_origin"] = args.data_origin
|
|
302
|
+
zh_path.write_text(json.dumps(zh_result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
303
|
+
out = Path(args.out)
|
|
304
|
+
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
305
|
+
print(f"wrote {args.out} (sources={len(result['sources'])}, evidence={len(result['evidence'])}, "
|
|
306
|
+
f"claims={len(result['claims'])}, outcomes={len(result['outcomes'])})")
|
|
307
|
+
return 0
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
sys.exit(main())
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Version consistency gate: engine/versions.py is the single version authority.
|
|
3
|
+
|
|
4
|
+
Fails (exit 1) when the declared versions drift apart:
|
|
5
|
+
|
|
6
|
+
- engine/versions.py ENGINE_VERSION <- authority
|
|
7
|
+
- pyproject.toml [project] version
|
|
8
|
+
- package.json version
|
|
9
|
+
- CHANGELOG.md first `## [x.y.z]` heading
|
|
10
|
+
- SKILL.md `# EduEvidence X.Y` title (major.minor only)
|
|
11
|
+
|
|
12
|
+
Stdlib only; run in CI before tests.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
22
|
+
|
|
23
|
+
VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
24
|
+
VERSIONS_PY_RE = re.compile(r'^ENGINE_VERSION\s*=\s*"(\d+\.\d+\.\d+)"', re.M)
|
|
25
|
+
PYPROJECT_RE = re.compile(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', re.M)
|
|
26
|
+
PACKAGE_JSON_RE = re.compile(r'"version"\s*:\s*"(\d+\.\d+\.\d+)"')
|
|
27
|
+
CHANGELOG_RE = re.compile(r"^##\s*\[(\d+\.\d+\.\d+)\]", re.M)
|
|
28
|
+
SKILL_TITLE_RE = re.compile(r"^#\s*EduEvidence\s+(\d+\.\d+)", re.M)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read(rel: str) -> str:
|
|
32
|
+
path = REPO_ROOT / rel
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return ""
|
|
35
|
+
return path.read_text(encoding="utf-8")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> int:
|
|
39
|
+
errors: list[str] = []
|
|
40
|
+
|
|
41
|
+
m = VERSIONS_PY_RE.search(_read("engine/versions.py"))
|
|
42
|
+
if not m:
|
|
43
|
+
print("FAIL: cannot parse ENGINE_VERSION from engine/versions.py")
|
|
44
|
+
return 1
|
|
45
|
+
authoritative = m.group(1)
|
|
46
|
+
if not VERSION_RE.match(authoritative):
|
|
47
|
+
errors.append(f"engine/versions.py ENGINE_VERSION not semver: {authoritative!r}")
|
|
48
|
+
|
|
49
|
+
def check_full(label: str, found: str) -> None:
|
|
50
|
+
if found != authoritative:
|
|
51
|
+
errors.append(f"{label} = {found} != engine/versions.py {authoritative}")
|
|
52
|
+
|
|
53
|
+
def check_minor(label: str, found: str) -> None:
|
|
54
|
+
prefix = authoritative.rsplit(".", 1)[0]
|
|
55
|
+
if found != prefix:
|
|
56
|
+
errors.append(f"{label} = {found} != {prefix} (from {authoritative})")
|
|
57
|
+
|
|
58
|
+
# -- pyproject.toml ------------------------------------------------------
|
|
59
|
+
pp = PYPROJECT_RE.search(_read("pyproject.toml"))
|
|
60
|
+
if pp:
|
|
61
|
+
check_full("pyproject.toml", pp.group(1))
|
|
62
|
+
else:
|
|
63
|
+
errors.append("cannot parse version from pyproject.toml")
|
|
64
|
+
|
|
65
|
+
# -- package.json --------------------------------------------------------
|
|
66
|
+
pj = PACKAGE_JSON_RE.search(_read("package.json"))
|
|
67
|
+
if pj:
|
|
68
|
+
check_full("package.json", pj.group(1))
|
|
69
|
+
else:
|
|
70
|
+
errors.append("cannot parse version from package.json")
|
|
71
|
+
|
|
72
|
+
# -- CHANGELOG.md ---------------------------------------------------------
|
|
73
|
+
cl = CHANGELOG_RE.search(_read("CHANGELOG.md"))
|
|
74
|
+
if cl:
|
|
75
|
+
check_full("CHANGELOG.md head entry", cl.group(1))
|
|
76
|
+
else:
|
|
77
|
+
errors.append("cannot parse leading '## [x.y.z]' from CHANGELOG.md")
|
|
78
|
+
|
|
79
|
+
# -- SKILL.md (major.minor only) ------------------------------------------
|
|
80
|
+
sk = SKILL_TITLE_RE.search(_read("SKILL.md"))
|
|
81
|
+
if sk:
|
|
82
|
+
check_minor("SKILL.md title", sk.group(1))
|
|
83
|
+
else:
|
|
84
|
+
errors.append("cannot parse '# EduEvidence X.Y' title from SKILL.md")
|
|
85
|
+
|
|
86
|
+
if errors:
|
|
87
|
+
for e in errors:
|
|
88
|
+
print(f"FAIL: {e}")
|
|
89
|
+
print(f"Version authority: engine/versions.py = {authoritative}")
|
|
90
|
+
return 1
|
|
91
|
+
print(f"version consistency OK ({authoritative})")
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
sys.exit(main())
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI: verify every DOI in a pack's sources/evidence against registries (E6).
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 scripts/citation_check.py --pack examples/ai-coding-assistant-evidence
|
|
6
|
+
python3 scripts/citation_check.py --sources examples/X/sources.jsonl --out /tmp/cite.json
|
|
7
|
+
|
|
8
|
+
Writes citation_check.json (+ .md) into the pack dir by default and exits 1
|
|
9
|
+
when any not_found/mismatch/retracted is found unless --no-fail.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
21
|
+
|
|
22
|
+
from engine.citation_check import RegistryClient, extract_dois # noqa: E402
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_records(path: Path) -> list[dict]:
|
|
26
|
+
text = path.read_text(encoding="utf-8")
|
|
27
|
+
if path.suffix == ".jsonl":
|
|
28
|
+
return [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
29
|
+
return [json.loads(text)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> int:
|
|
33
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
34
|
+
parser.add_argument("--pack", help="example pack directory")
|
|
35
|
+
parser.add_argument("--sources", help="single sources.jsonl / evidence.jsonl")
|
|
36
|
+
parser.add_argument("--out", help="output JSON path (default <pack>/citation_check.json)")
|
|
37
|
+
parser.add_argument("--no-fail", action="store_true")
|
|
38
|
+
parser.add_argument("--write-back", action="store_true",
|
|
39
|
+
help="inject doi_verified/retracted into the pack's sources.jsonl")
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
|
|
42
|
+
targets: list[Path] = []
|
|
43
|
+
out_path: Path | None = None
|
|
44
|
+
if args.pack:
|
|
45
|
+
pack = Path(args.pack)
|
|
46
|
+
for name in ("sources.jsonl", "evidence.jsonl"):
|
|
47
|
+
f = pack / name
|
|
48
|
+
if f.exists():
|
|
49
|
+
targets.append(f)
|
|
50
|
+
out_path = pack / "citation_check.json"
|
|
51
|
+
elif args.sources:
|
|
52
|
+
targets.append(Path(args.sources))
|
|
53
|
+
out_path = Path(args.out) if args.out else Path("citation_check.json")
|
|
54
|
+
else:
|
|
55
|
+
parser.error("pass --pack or --sources")
|
|
56
|
+
if not targets:
|
|
57
|
+
print("no sources/evidence files found; nothing to check")
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
records: list[tuple[str, dict]] = []
|
|
61
|
+
for t in targets:
|
|
62
|
+
records.extend((t.name, rec) for rec in load_records(t))
|
|
63
|
+
|
|
64
|
+
client = RegistryClient()
|
|
65
|
+
seen: set[str] = set()
|
|
66
|
+
rows = []
|
|
67
|
+
for fname, rec in records:
|
|
68
|
+
for doi, title in extract_dois(rec):
|
|
69
|
+
if doi in seen:
|
|
70
|
+
continue
|
|
71
|
+
seen.add(doi)
|
|
72
|
+
verdict = client.check(doi, title)
|
|
73
|
+
rows.append({"file": fname, **verdict})
|
|
74
|
+
print(f"{verdict['status']:9s} retracted={verdict['retracted']!s:5s} {doi}")
|
|
75
|
+
|
|
76
|
+
bad = [r for r in rows if r["status"] != "ok" or r["retracted"]]
|
|
77
|
+
report = {
|
|
78
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
79
|
+
"checked": len(rows),
|
|
80
|
+
"bad": len(bad),
|
|
81
|
+
"results": rows,
|
|
82
|
+
}
|
|
83
|
+
assert out_path is not None
|
|
84
|
+
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
85
|
+
md = out_path.with_suffix(".md")
|
|
86
|
+
lines = ["# Citation Check", "",
|
|
87
|
+
f"生成时间:{report['generated_at']};核验 DOI:{len(rows)};异常:{len(bad)}", ""]
|
|
88
|
+
if bad:
|
|
89
|
+
lines += ["| DOI | 状态 | 撤稿 | 注册表标题 |", "|---|---|---|---|"]
|
|
90
|
+
for r in bad:
|
|
91
|
+
t = (r.get("registry_title") or "")[:70].replace("|", "\\|")
|
|
92
|
+
lines.append(f"| `{r['doi']}` | {r['status']} | {'⚠️ 是' if r['retracted'] else '否'} | {t} |")
|
|
93
|
+
md.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
print(f"\ncitation check -> {out_path} (bad={len(bad)})")
|
|
96
|
+
|
|
97
|
+
if args.write_back and args.pack:
|
|
98
|
+
by_doi = {r["doi"]: r for r in rows}
|
|
99
|
+
src_file = Path(args.pack) / "sources.jsonl"
|
|
100
|
+
if src_file.exists():
|
|
101
|
+
lines = []
|
|
102
|
+
changed = 0
|
|
103
|
+
for line in src_file.read_text(encoding="utf-8").splitlines():
|
|
104
|
+
if not line.strip():
|
|
105
|
+
continue
|
|
106
|
+
rec = json.loads(line)
|
|
107
|
+
for doi, v in by_doi.items():
|
|
108
|
+
rec_doi = (rec.get("doi") or "").strip()
|
|
109
|
+
if rec_doi == doi or doi in json.dumps(rec):
|
|
110
|
+
new_flags = {"doi_verified": v["doi_verified"], "retracted": v["retracted"]}
|
|
111
|
+
if any(rec.get(k) != val for k, val in new_flags.items()):
|
|
112
|
+
rec.update(new_flags)
|
|
113
|
+
changed += 1
|
|
114
|
+
break
|
|
115
|
+
lines.append(json.dumps(rec, ensure_ascii=False))
|
|
116
|
+
src_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
117
|
+
print(f"write-back: {changed} source rows updated in {src_file}")
|
|
118
|
+
|
|
119
|
+
return (1 if bad else 0) if not args.no_fail else 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main())
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""claim_audit.py — Citation Audit (plan section 11).
|
|
3
|
+
|
|
4
|
+
Runs before final report generation. For each claim-evidence pair:
|
|
5
|
+
|
|
6
|
+
Claim exists? -> Evidence exists? -> Source exists? -> Source supports claim?
|
|
7
|
+
-> Outcome matches? -> Scope exceeds source?
|
|
8
|
+
|
|
9
|
+
Failures are marked:
|
|
10
|
+
UNSUPPORTED evidence cannot be bound to a verifiable source
|
|
11
|
+
CONTRADICT bound evidence contradicts the claim (relation_to_claim)
|
|
12
|
+
DOWNGRADE_CONFIDENCE claim overstates the source (scope/outcome mismatch)
|
|
13
|
+
|
|
14
|
+
Citation relation is judged ONLY by relation_to_claim (via
|
|
15
|
+
evidence_semantics.claim_relation) — never by effect_direction or the legacy
|
|
16
|
+
direction field: a negative effect can support a negative claim.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python scripts/claim_audit.py --claims claims.jsonl --evidence evidence.jsonl
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from evidence_semantics import claim_relation
|
|
29
|
+
|
|
30
|
+
SUPPORTED_OUTCOMES = {
|
|
31
|
+
"knowledge_gain", "concept_understanding", "retention", "transfer",
|
|
32
|
+
"independent_problem_solving", "completion_time", "accuracy",
|
|
33
|
+
"code_quality", "assignment_score", "engagement", "motivation",
|
|
34
|
+
"cognitive_load", "help_seeking", "metacognition", "ai_dependency",
|
|
35
|
+
"over_reliance", "reduced_effort", "reduced_transfer",
|
|
36
|
+
"academic_integrity_risk", "false_confidence",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_records(path: Path) -> list[dict]:
|
|
41
|
+
records = []
|
|
42
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
43
|
+
line = line.strip()
|
|
44
|
+
if line:
|
|
45
|
+
records.append(json.loads(line))
|
|
46
|
+
return records
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def audit_claim(claim: dict, evidence_by_id: dict[str, dict]) -> dict:
|
|
50
|
+
"""Audit a single claim. Returns a status record."""
|
|
51
|
+
result = {
|
|
52
|
+
"claim": claim.get("claim", ""),
|
|
53
|
+
"status": "SUPPORTED",
|
|
54
|
+
"issues": [],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# 1. Claim exists?
|
|
58
|
+
if not claim.get("claim"):
|
|
59
|
+
result["status"] = "UNSUPPORTED"
|
|
60
|
+
result["issues"].append("claim is empty")
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
# 2. Evidence exists?
|
|
64
|
+
evidence_ids = claim.get("evidence_ids", [])
|
|
65
|
+
if not evidence_ids:
|
|
66
|
+
result["status"] = "UNSUPPORTED"
|
|
67
|
+
result["issues"].append("no evidence_ids bound to claim")
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
bound = []
|
|
71
|
+
for eid in evidence_ids:
|
|
72
|
+
ev = evidence_by_id.get(eid)
|
|
73
|
+
if ev is None:
|
|
74
|
+
result["issues"].append(f"evidence {eid} not found")
|
|
75
|
+
continue
|
|
76
|
+
bound.append(ev)
|
|
77
|
+
|
|
78
|
+
if not bound:
|
|
79
|
+
result["status"] = "UNSUPPORTED"
|
|
80
|
+
result["issues"].append("no bound evidence found in corpus")
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
# 3. Source exists (source_id + source_location)
|
|
84
|
+
for ev in bound:
|
|
85
|
+
if not ev.get("source_id"):
|
|
86
|
+
result["issues"].append(f"{ev.get('evidence_id')}: missing source_id")
|
|
87
|
+
if not ev.get("source_location"):
|
|
88
|
+
result["issues"].append(f"{ev.get('evidence_id')}: missing source_location")
|
|
89
|
+
|
|
90
|
+
# 4. Source supports claim (judged ONLY by relation_to_claim; a negative
|
|
91
|
+
# effect can support a negative claim — never use effect_direction or
|
|
92
|
+
# the legacy direction field as a citation relation)
|
|
93
|
+
for ev in bound:
|
|
94
|
+
if claim_relation(ev) == "contradict":
|
|
95
|
+
result["issues"].append(
|
|
96
|
+
f"{ev.get('evidence_id')}: evidence contradicts claim (relation_to_claim=contradict)")
|
|
97
|
+
|
|
98
|
+
# 5. Outcome matches
|
|
99
|
+
for ev in bound:
|
|
100
|
+
outcome = ev.get("outcome_type", "")
|
|
101
|
+
if outcome not in SUPPORTED_OUTCOMES:
|
|
102
|
+
result["issues"].append(
|
|
103
|
+
f"{ev.get('evidence_id')}: unknown outcome_type {outcome!r}")
|
|
104
|
+
claimed_outcome = claim.get("outcome_type")
|
|
105
|
+
if claimed_outcome and outcome and claimed_outcome != outcome:
|
|
106
|
+
result["issues"].append(
|
|
107
|
+
f"{ev.get('evidence_id')}: outcome mismatch (claim={claimed_outcome}, evidence={outcome})")
|
|
108
|
+
|
|
109
|
+
# 6. Scope exceeds source?
|
|
110
|
+
claim_scope = claim.get("scope")
|
|
111
|
+
if claim_scope:
|
|
112
|
+
for ev in bound:
|
|
113
|
+
ev_scope = ev.get("applicability", {}).get("scope", "")
|
|
114
|
+
if ev_scope and claim_scope not in ev_scope:
|
|
115
|
+
result["issues"].append(
|
|
116
|
+
f"{ev.get('evidence_id')}: claim scope {claim_scope!r} exceeds source scope {ev_scope!r}")
|
|
117
|
+
|
|
118
|
+
if result["issues"]:
|
|
119
|
+
if any("contradicts claim" in i for i in result["issues"]):
|
|
120
|
+
# Contradiction is the dominant signal: the claim is contradicted
|
|
121
|
+
# by its own evidence, distinct from being unverifiable.
|
|
122
|
+
result["status"] = "CONTRADICT"
|
|
123
|
+
else:
|
|
124
|
+
severe = any("missing source" in i or "not found" in i
|
|
125
|
+
for i in result["issues"])
|
|
126
|
+
result["status"] = "UNSUPPORTED" if severe else "DOWNGRADE_CONFIDENCE"
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def audit_claims(claims: list[dict], evidence_list: list[dict]) -> list[dict]:
|
|
131
|
+
evidence_by_id = {ev.get("evidence_id"): ev for ev in evidence_list}
|
|
132
|
+
return [audit_claim(c, evidence_by_id) for c in claims]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> int:
|
|
136
|
+
parser = argparse.ArgumentParser(description="Run the Citation Audit over claims and evidence")
|
|
137
|
+
parser.add_argument("--claims", required=True, help="claims.jsonl (each line: claim, evidence_ids, outcome_type, scope)")
|
|
138
|
+
parser.add_argument("--evidence", required=True, help="evidence.jsonl")
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
|
|
141
|
+
claims = load_records(Path(args.claims))
|
|
142
|
+
evidence = load_records(Path(args.evidence))
|
|
143
|
+
results = audit_claims(claims, evidence)
|
|
144
|
+
|
|
145
|
+
summary = {"SUPPORTED": 0, "UNSUPPORTED": 0, "DOWNGRADE_CONFIDENCE": 0, "CONTRADICT": 0}
|
|
146
|
+
for r in results:
|
|
147
|
+
summary[r["status"]] += 1
|
|
148
|
+
if r["issues"]:
|
|
149
|
+
print(f"[{r['status']}] {r['claim']}")
|
|
150
|
+
for issue in r["issues"]:
|
|
151
|
+
print(f" - {issue}")
|
|
152
|
+
print(json.dumps(summary, ensure_ascii=False))
|
|
153
|
+
return 0 if summary["UNSUPPORTED"] == 0 else 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
sys.exit(main())
|