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,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""render_report_html.py — Static-first HTML Evidence Report renderer (v5 Iteration 1).
|
|
3
|
+
|
|
4
|
+
Turns a validated result.json into a semantic, static, print-friendly HTML
|
|
5
|
+
report. Iteration 1 acceptance: **even with JavaScript fully disabled, the
|
|
6
|
+
report already has value** — Decision, Outcome summary, Evidence Matrix,
|
|
7
|
+
Tribunal, Intervention, Evaluation and Sources are all real HTML.
|
|
8
|
+
|
|
9
|
+
Theme system (Iteration 1-2) hooks in via `data-theme` + CSS variables; this
|
|
10
|
+
renderer emits the default `claude` theme tokens inline so the file works
|
|
11
|
+
standalone with zero JS and zero CDN.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 scripts/render_report_html.py \
|
|
15
|
+
--result examples/ai-coding-assistant/result.json \
|
|
16
|
+
--out examples/ai-coding-assistant/report.html
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import html
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
THEMES_DIR = Path(__file__).resolve().parent.parent / "visualization" / "eduevidence-report" / "themes"
|
|
28
|
+
THEME_NAMES = ("claude", "academic", "datalab", "presentation") # W5: ghost removed
|
|
29
|
+
|
|
30
|
+
DIRECTION_LABELS = {"support": "支持", "contradict": "反驳", "neutral": "中性"}
|
|
31
|
+
DIRECTION_CLASS = {"support": "pos", "contradict": "neg", "neutral": "neu"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def esc(text: Any) -> str:
|
|
35
|
+
return html.escape(str(text if text is not None else ""))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def section(title: str, content: str, sid: str) -> str:
|
|
39
|
+
return f'<section id="{sid}" class="report-section">\n<h2>{esc(title)}</h2>\n{content}\n</section>\n'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_decision(decision: dict) -> str:
|
|
43
|
+
action = decision.get("recommended_action", "insufficient_evidence")
|
|
44
|
+
confidence = decision.get("confidence", "")
|
|
45
|
+
cls = {"adopt": "adopt", "pilot": "pilot", "reject": "reject",
|
|
46
|
+
"insufficient_evidence": "insufficient"}.get(action, "insufficient")
|
|
47
|
+
rows = [
|
|
48
|
+
f'<div class="decision-card {cls}">',
|
|
49
|
+
f'<span class="decision-label">决策</span>',
|
|
50
|
+
f'<span class="decision-value">{esc(action.upper())}</span>',
|
|
51
|
+
f'<span class="confidence-badge">{esc(confidence)}</span>',
|
|
52
|
+
"</div>",
|
|
53
|
+
]
|
|
54
|
+
if decision.get("decision_rationale"):
|
|
55
|
+
rows.append(f'<p class="rationale">{esc(decision["decision_rationale"])}</p>')
|
|
56
|
+
|
|
57
|
+
def bullet_list(key: str, label: str) -> str:
|
|
58
|
+
items = decision.get(key) or []
|
|
59
|
+
if not items:
|
|
60
|
+
return ""
|
|
61
|
+
lis = "".join(f"<li>{esc(i)}</li>" for i in items)
|
|
62
|
+
return f"<h3>{esc(label)}</h3><ul>{lis}</ul>"
|
|
63
|
+
|
|
64
|
+
rows.append(bullet_list("what_can_be_claimed", "可以主张 (Can Claim)"))
|
|
65
|
+
rows.append(bullet_list("what_cannot_be_claimed", "不能主张 (Cannot Claim)"))
|
|
66
|
+
rows.append(bullet_list("missing_evidence", "缺失证据"))
|
|
67
|
+
rows.append(bullet_list("exceeds_evidence_boundary", "超出证据边界"))
|
|
68
|
+
return "\n".join(rows)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_outcomes(outcomes: list[dict]) -> str:
|
|
72
|
+
if not outcomes:
|
|
73
|
+
return "<p>无 Outcome 数据。</p>"
|
|
74
|
+
rows = ["<table class='data-table'><thead><tr><th>Outcome</th><th>正向效应</th>"
|
|
75
|
+
"<th>负向效应</th><th>零效应</th></tr></thead><tbody>"]
|
|
76
|
+
for o in outcomes:
|
|
77
|
+
rows.append(
|
|
78
|
+
f"<tr><td>{esc(o.get('outcome_type'))}</td>"
|
|
79
|
+
f"<td class='num'>{o.get('positive_count', 0)}</td>"
|
|
80
|
+
f"<td class='num'>{o.get('negative_count', 0)}</td>"
|
|
81
|
+
f"<td class='num'>{o.get('null_count', 0)}</td></tr>")
|
|
82
|
+
rows.append("</tbody></table>")
|
|
83
|
+
return "\n".join(rows)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render_matrix(evidence: list[dict]) -> str:
|
|
87
|
+
if not evidence:
|
|
88
|
+
return "<p>无证据数据。</p>"
|
|
89
|
+
rows = ["<table class='data-table matrix'><thead><tr>"
|
|
90
|
+
"<th>ID</th><th>Outcome</th><th>方向</th><th>Claim</th>"
|
|
91
|
+
"<th>来源</th><th>质量</th></tr></thead><tbody>"]
|
|
92
|
+
for ev in evidence:
|
|
93
|
+
direction = ev.get("direction", "neutral")
|
|
94
|
+
rows.append(
|
|
95
|
+
f"<tr><td><code>{esc(ev.get('evidence_id'))}</code></td>"
|
|
96
|
+
f"<td>{esc(ev.get('outcome_type'))}</td>"
|
|
97
|
+
f"<td><span class='dir {DIRECTION_CLASS.get(direction, 'neu')}'>{esc(DIRECTION_LABELS.get(direction, direction))}</span></td>"
|
|
98
|
+
f"<td>{esc(ev.get('claim'))}</td>"
|
|
99
|
+
f"<td><code>{esc(ev.get('source_id'))}</code></td>"
|
|
100
|
+
f"<td class='num'>{esc(ev.get('quality_score'))}</td></tr>")
|
|
101
|
+
rows.append("</tbody></table>")
|
|
102
|
+
return "\n".join(rows)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def render_tribunal(verdict: dict) -> str:
|
|
106
|
+
lines = [f"<p><strong>决策:</strong>{esc(verdict.get('recommended_action', '').upper())} "
|
|
107
|
+
f"· <strong>置信度:</strong>{esc(verdict.get('confidence', ''))}</p>"]
|
|
108
|
+
|
|
109
|
+
def group(key: str, label: str) -> str:
|
|
110
|
+
items = verdict.get(key) or []
|
|
111
|
+
if not items:
|
|
112
|
+
return ""
|
|
113
|
+
lis = "".join(f"<li>{esc(i)}</li>" for i in items)
|
|
114
|
+
return f"<h3>{esc(label)}</h3><ul>{lis}</ul>"
|
|
115
|
+
|
|
116
|
+
lines.append(group("supported_claims", "支持的结论"))
|
|
117
|
+
lines.append(group("uncertain_claims", "不确定的结论"))
|
|
118
|
+
lines.append(group("contradicted_claims", "被反驳的结论"))
|
|
119
|
+
if verdict.get("reason_for_disagreement"):
|
|
120
|
+
lines.append(f"<p><strong>冲突来源:</strong>{esc(verdict['reason_for_disagreement'])}</p>")
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def render_methodology(reviews: list[dict]) -> str:
|
|
125
|
+
if not reviews:
|
|
126
|
+
return "<p>无方法学审查数据。</p>"
|
|
127
|
+
lines = []
|
|
128
|
+
for r in reviews:
|
|
129
|
+
verdict = r.get("verdict", "")
|
|
130
|
+
lines.append(f"<h3>审查目标:{esc(r.get('target'))} "
|
|
131
|
+
f"<span class='method-verdict'>{esc(verdict)}</span></h3>")
|
|
132
|
+
audit = r.get("audit_items", {})
|
|
133
|
+
if audit:
|
|
134
|
+
rows = ["<table class='data-table'><thead><tr><th>检查项</th><th>状态</th>"
|
|
135
|
+
"<th>说明</th></tr></thead><tbody>"]
|
|
136
|
+
for item, info in audit.items():
|
|
137
|
+
if isinstance(info, dict):
|
|
138
|
+
rows.append(f"<tr><td>{esc(item)}</td><td>{esc(info.get('status'))}</td>"
|
|
139
|
+
f"<td>{esc(info.get('note'))}</td></tr>")
|
|
140
|
+
rows.append("</tbody></table>")
|
|
141
|
+
lines.append("\n".join(rows))
|
|
142
|
+
guard = r.get("task_vs_learning_guard", {})
|
|
143
|
+
if guard:
|
|
144
|
+
lines.append(f"<p><strong>任务 vs 学习护栏:</strong>{esc(guard.get('note'))}</p>")
|
|
145
|
+
return "\n".join(lines)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def render_intervention(intervention: dict) -> str:
|
|
149
|
+
if not intervention:
|
|
150
|
+
return "<p>无干预方案数据。</p>"
|
|
151
|
+
lines = [f"<p><strong>目标学习者:</strong>{esc(intervention.get('target_learners'))} · "
|
|
152
|
+
f"<strong>试点时长:</strong>{esc(intervention.get('pilot_duration'))}</p>"]
|
|
153
|
+
if intervention.get("ai_usage_policy"):
|
|
154
|
+
lines.append(f"<p><strong>AI 使用规则:</strong>{esc(intervention['ai_usage_policy'])}</p>")
|
|
155
|
+
for phase in ("phase_1", "phase_2", "phase_3", "phase_4"):
|
|
156
|
+
p = intervention.get(phase)
|
|
157
|
+
if isinstance(p, dict):
|
|
158
|
+
name = p.get("name", phase)
|
|
159
|
+
rule = p.get("ai_usage_rule", "")
|
|
160
|
+
lines.append(f"<div class='phase'><h3>{esc(name)}</h3>"
|
|
161
|
+
f"<p><strong>AI 规则:</strong>{esc(rule)}</p></div>")
|
|
162
|
+
if intervention.get("stop_conditions"):
|
|
163
|
+
lis = "".join(f"<li>{esc(s)}</li>" for s in intervention["stop_conditions"])
|
|
164
|
+
lines.append(f"<h3>停止条件</h3><ul>{lis}</ul>")
|
|
165
|
+
return "\n".join(lines)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_evaluation(evaluation: dict) -> str:
|
|
169
|
+
if not evaluation:
|
|
170
|
+
return "<p>无评价方案数据。</p>"
|
|
171
|
+
lines = [f"<p><strong>研究问题:</strong>{esc(evaluation.get('research_question'))}</p>"]
|
|
172
|
+
for key, label in (("baseline", "基线"), ("post_test", "后测"),
|
|
173
|
+
("retention_test", "保持测试"), ("transfer_test", "迁移测试")):
|
|
174
|
+
if evaluation.get(key):
|
|
175
|
+
lines.append(f"<p><strong>{esc(label)}:</strong>{esc(evaluation[key])}</p>")
|
|
176
|
+
for key, label in (("process_metrics", "过程指标"), ("learning_metrics", "学习指标"),
|
|
177
|
+
("risk_metrics", "风险指标")):
|
|
178
|
+
items = evaluation.get(key) or []
|
|
179
|
+
if items:
|
|
180
|
+
lis = "".join(f"<li>{esc(i)}</li>" for i in items)
|
|
181
|
+
lines.append(f"<h3>{esc(label)}</h3><ul>{lis}</ul>")
|
|
182
|
+
if evaluation.get("success_threshold"):
|
|
183
|
+
lines.append(f"<p><strong>成功阈值:</strong>{esc(evaluation['success_threshold'])}</p>")
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def render_sources(sources: list[dict]) -> str:
|
|
188
|
+
if not sources:
|
|
189
|
+
return "<p>无来源数据。</p>"
|
|
190
|
+
rows = ["<table class='data-table'><thead><tr><th>ID</th><th>标题</th>"
|
|
191
|
+
"<th>年份</th><th>权威级别</th><th>位置</th></tr></thead><tbody>"]
|
|
192
|
+
for s in sources:
|
|
193
|
+
rows.append(f"<tr><td><code>{esc(s.get('source_id'))}</code></td>"
|
|
194
|
+
f"<td>{esc(s.get('title'))}</td><td>{esc(s.get('year'))}</td>"
|
|
195
|
+
f"<td>{esc(s.get('authority_level'))}</td>"
|
|
196
|
+
f"<td><a href='{esc(s.get('canonical_url') or s.get('source_location'))}'>"
|
|
197
|
+
f"{esc(s.get('source_location') or s.get('canonical_url'))}</a></td></tr>")
|
|
198
|
+
rows.append("</tbody></table>")
|
|
199
|
+
return "\n".join(rows)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def render_sources_provenance(sources: list[dict]) -> str:
|
|
203
|
+
"""SWF Iteration E: fetch provenance lives in Sources & Provenance panel
|
|
204
|
+
(v3 方案 §20) — never in the main evidence surface."""
|
|
205
|
+
if not sources:
|
|
206
|
+
return "<p>无 fetch 信息。</p>"
|
|
207
|
+
lines = ["<table class='data-table'><thead><tr><th>来源</th><th>Fetch 方式</th>"
|
|
208
|
+
"<th>状态</th><th>降级</th><th>时间</th></tr></thead><tbody>"]
|
|
209
|
+
for s in sources:
|
|
210
|
+
fetch = s.get("fetch", {})
|
|
211
|
+
lines.append(f"<tr><td><code>{esc(s.get('source_id'))}</code></td>"
|
|
212
|
+
f"<td>{esc(fetch.get('fetch_provider'))}</td>"
|
|
213
|
+
f"<td>{esc(fetch.get('fetch_status'))}</td>"
|
|
214
|
+
f"<td>{esc(fetch.get('fallback_used'))}</td>"
|
|
215
|
+
f"<td>{esc(fetch.get('fetched_at'))}</td></tr>")
|
|
216
|
+
lines.append("</tbody></table>")
|
|
217
|
+
return "\n".join(lines)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _theme_css() -> str:
|
|
221
|
+
"""Load all five theme CSS files (visualization/.../themes/*.css) and the
|
|
222
|
+
base (claude) tokens; the report supports live theme switching via
|
|
223
|
+
data-theme + CSS variables (v5 方案 §3-9)."""
|
|
224
|
+
base = THEMES_DIR / "claude.css"
|
|
225
|
+
blocks = [base.read_text(encoding="utf-8")] if base.exists() else []
|
|
226
|
+
for name in THEME_NAMES:
|
|
227
|
+
if name == "claude":
|
|
228
|
+
continue
|
|
229
|
+
css = THEMES_DIR / f"{name}.css"
|
|
230
|
+
if css.exists():
|
|
231
|
+
blocks.append(css.read_text(encoding="utf-8"))
|
|
232
|
+
return "\n".join(blocks)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def render_html(result: dict) -> str:
|
|
236
|
+
frame = result.get("research_frame", {})
|
|
237
|
+
decision = result.get("decision", {})
|
|
238
|
+
outcomes = result.get("outcomes", [])
|
|
239
|
+
evidence = result.get("evidence", [])
|
|
240
|
+
reviews = result.get("methodology_reviews", [])
|
|
241
|
+
intervention = result.get("intervention", {})
|
|
242
|
+
evaluation = result.get("evaluation", {})
|
|
243
|
+
sources = result.get("sources", [])
|
|
244
|
+
meta = result.get("meta", {})
|
|
245
|
+
|
|
246
|
+
# W5: 主题在生成时烘焙;最终 HTML 不做运行时主题切换(仅中英文切换/language switch)
|
|
247
|
+
|
|
248
|
+
body = "\n".join([
|
|
249
|
+
section("01 Executive Decision", render_decision(decision), "01-executive-decision"),
|
|
250
|
+
section("02 Outcome Evidence Overview", render_outcomes(outcomes), "02-outcome-overview"),
|
|
251
|
+
section("03 Evidence Matrix", render_matrix(evidence), "03-evidence-matrix"),
|
|
252
|
+
section("04 Evidence Tribunal", render_tribunal(decision), "04-evidence-tribunal"),
|
|
253
|
+
section("05 Methodology Audit", render_methodology(reviews), "05-methodology-audit"),
|
|
254
|
+
section("06 Conflict Analysis",
|
|
255
|
+
f"<p>{esc(decision.get('reason_for_disagreement', '无冲突分析数据。'))}</p>",
|
|
256
|
+
"06-conflict-analysis"),
|
|
257
|
+
section("07 Claim-Evidence Trace",
|
|
258
|
+
"<p>交互式 Claim→Evidence→Source 图将在 JS 增强层启用;"
|
|
259
|
+
"静态表格见 Evidence Matrix 与 Sources。</p>",
|
|
260
|
+
"07-claim-trace"),
|
|
261
|
+
section("08 Applicability", f"<pre>{esc(json.dumps(decision.get('applicability', {}), ensure_ascii=False, indent=2))}</pre>",
|
|
262
|
+
"08-applicability"),
|
|
263
|
+
section("09 Teaching Intervention", render_intervention(intervention), "09-intervention"),
|
|
264
|
+
section("10 Evaluation Plan", render_evaluation(evaluation), "10-evaluation"),
|
|
265
|
+
section("11 Benchmark", "<p>Benchmark 数据见独立 Benchmark 报告(benchmarks/results/v2-report.md)。</p>",
|
|
266
|
+
"11-benchmark"),
|
|
267
|
+
section("12 Sources & Provenance",
|
|
268
|
+
render_sources(sources) + "<h3>Fetch Provenance</h3>" + render_sources_provenance(sources),
|
|
269
|
+
"12-sources"),
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
theme_css = _theme_css()
|
|
273
|
+
|
|
274
|
+
return f"""<!DOCTYPE html>
|
|
275
|
+
<html lang="zh-CN" data-theme="claude">
|
|
276
|
+
<head>
|
|
277
|
+
<meta charset="utf-8">
|
|
278
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
279
|
+
<title>{esc(meta.get('question', 'EduEvidence Evidence Report'))}</title>
|
|
280
|
+
<style>
|
|
281
|
+
{theme_css}
|
|
282
|
+
:root {{
|
|
283
|
+
--bg:#F7F4ED; --surface:#FFFFFF; --surface2:#FCFAF6;
|
|
284
|
+
--text:#3A3833; --primary:#B8694A; --support:#5E8A6A;
|
|
285
|
+
--contradict:#A85B53; --uncertain:#C99A4A; --insufficient:#8A867E;
|
|
286
|
+
--border:#E5DFD3; --radius:10px; --shadow:0 1px 3px rgba(60,56,48,.08);
|
|
287
|
+
--font-head:'Georgia','Songti SC',serif; --font-ui:'Helvetica Neue',Arial,sans-serif;
|
|
288
|
+
}}
|
|
289
|
+
body {{ margin:0; background:var(--bg); color:var(--text);
|
|
290
|
+
font-family:var(--font-ui); line-height:1.65; }}
|
|
291
|
+
.report-shell {{ max-width:1200px; margin:0 auto; padding:24px 32px 80px; }}
|
|
292
|
+
.report-header {{ border-bottom:1px solid var(--border); padding-bottom:16px; margin-bottom:24px; }}
|
|
293
|
+
.report-header h1 {{ font-family:var(--font-head); font-size:1.9rem; margin:0 0 8px; color:var(--text); }}
|
|
294
|
+
.report-header .meta {{ color:var(--insufficient); font-size:.85rem; }}
|
|
295
|
+
padding:4px 12px; font-size:.8rem; cursor:pointer; color:var(--text); }}
|
|
296
|
+
.report-section {{ background:var(--surface); border:1px solid var(--border);
|
|
297
|
+
border-radius:var(--radius); box-shadow:var(--shadow);
|
|
298
|
+
padding:20px 24px; margin-bottom:20px; }}
|
|
299
|
+
.report-section h2 {{ font-family:var(--font-head); font-size:1.25rem; margin:0 0 12px;
|
|
300
|
+
border-bottom:1px solid var(--border); padding-bottom:8px; }}
|
|
301
|
+
.report-section h3 {{ font-size:.95rem; margin:14px 0 6px; }}
|
|
302
|
+
.decision-card {{ display:flex; align-items:center; gap:14px; padding:14px 18px; border-radius:8px;
|
|
303
|
+
border-left:6px solid var(--insufficient); background:var(--surface2); }}
|
|
304
|
+
.decision-card.adopt {{ border-left-color:var(--support); }}
|
|
305
|
+
.decision-card.pilot {{ border-left-color:var(--uncertain); }}
|
|
306
|
+
.decision-card.reject {{ border-left-color:var(--contradict); }}
|
|
307
|
+
.decision-value {{ font-family:var(--font-head); font-size:1.5rem; font-weight:700; }}
|
|
308
|
+
.confidence-badge {{ background:var(--uncertain); color:#fff; border-radius:999px; padding:2px 10px; font-size:.8rem; }}
|
|
309
|
+
.rationale {{ color:var(--text); font-size:.92rem; }}
|
|
310
|
+
.data-table {{ width:100%; border-collapse:collapse; font-size:.88rem; }}
|
|
311
|
+
.data-table th, .data-table td {{ border:1px solid var(--border); padding:6px 10px; text-align:left;
|
|
312
|
+
vertical-align:top; }}
|
|
313
|
+
.data-table th {{ background:var(--surface2); }}
|
|
314
|
+
.num {{ text-align:right; font-variant-numeric:tabular-nums; }}
|
|
315
|
+
.dir {{ display:inline-block; border-radius:999px; padding:1px 8px; font-size:.78rem; }}
|
|
316
|
+
.dir.pos {{ background:var(--support); color:#fff; }}
|
|
317
|
+
.dir.neg {{ background:var(--contradict); color:#fff; }}
|
|
318
|
+
.dir.neu {{ background:var(--uncertain); color:#fff; }}
|
|
319
|
+
.method-verdict {{ font-weight:700; }}
|
|
320
|
+
.phase {{ border-left:3px solid var(--primary); padding-left:12px; margin:10px 0; }}
|
|
321
|
+
code {{ font-family:'SF Mono',Menlo,monospace; font-size:.82em; background:var(--surface2);
|
|
322
|
+
padding:1px 4px; border-radius:4px; }}
|
|
323
|
+
a {{ color:var(--primary); word-break:break-all; }}
|
|
324
|
+
@media print {{ body {{ background:#fff; }} .report-section {{ box-shadow:none; border:none; }} }}
|
|
325
|
+
@media (max-width:720px) {{ .report-shell {{ padding:12px; }} .data-table {{ font-size:.78rem; }} }}
|
|
326
|
+
</style>
|
|
327
|
+
</head>
|
|
328
|
+
<body>
|
|
329
|
+
<div class="report-shell">
|
|
330
|
+
<header class="report-header">
|
|
331
|
+
<h1>{esc(meta.get('question', 'EduEvidence Evidence Report'))}</h1>
|
|
332
|
+
<p class="meta">EduEvidence · {esc(meta.get('mode'))} · {esc(meta.get('generated_at'))}</p>
|
|
333
|
+
</header>
|
|
334
|
+
{body}
|
|
335
|
+
<footer class="report-section"><p>EduEvidence Evidence Report · 由 eduevidence-report Skill 确定性渲染 · 数据源:result.json</p></footer>
|
|
336
|
+
</div>
|
|
337
|
+
</body>
|
|
338
|
+
</html>
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def main() -> int:
|
|
343
|
+
parser = argparse.ArgumentParser(description="Render static-first HTML Evidence Report")
|
|
344
|
+
parser.add_argument("--result", required=True, help="result.json path")
|
|
345
|
+
parser.add_argument("--out", required=True, help="output report.html path")
|
|
346
|
+
args = parser.parse_args()
|
|
347
|
+
|
|
348
|
+
result = json.loads(Path(args.result).read_text(encoding="utf-8"))
|
|
349
|
+
out = Path(args.out)
|
|
350
|
+
out.write_text(render_html(result), encoding="utf-8")
|
|
351
|
+
print(f"wrote {args.out} ({out.stat().st_size} bytes)")
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
sys.exit(main())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""retraction_watch.py — 定期重查已引 DOI 的撤稿状态(plan F4)。
|
|
3
|
+
|
|
4
|
+
引用可信不是一次性校验:文献可能在报告产出后被撤稿/更正。本脚本读取
|
|
5
|
+
examples/*/sources.jsonl(或 --state 指定的上次快照),仅查询每个 DOI 的
|
|
6
|
+
registry 记录变化,输出:
|
|
7
|
+
|
|
8
|
+
- benchmarks/retraction-watch/state.json 本次全量快照(doi → status)
|
|
9
|
+
- benchmarks/retraction-watch/report.md 与上次快照的 diff + 当前撤稿清单
|
|
10
|
+
|
|
11
|
+
退出码:发现新增撤稿或核验失败 → 1;否则 0(--no-fail 恒 0)。
|
|
12
|
+
Stdlib only;网络纪律同 engine/citation_check.py(域名白名单 + 严格 DOI 形态)。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
24
|
+
|
|
25
|
+
from engine.citation_check import RegistryClient, clean_doi # noqa: E402
|
|
26
|
+
from engine.log import get_log # noqa: E402
|
|
27
|
+
|
|
28
|
+
log = get_log("retraction-watch")
|
|
29
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
30
|
+
OUT_DIR = ROOT / "benchmarks" / "retraction-watch"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def collect_dois() -> dict[str, list[str]]:
|
|
34
|
+
"""doi -> 出现的示例包列表(读 sources.jsonl 的 doi 字段)。"""
|
|
35
|
+
out: dict[str, list[str]] = {}
|
|
36
|
+
examples = ROOT / "examples"
|
|
37
|
+
for pack in sorted(examples.iterdir()):
|
|
38
|
+
src = pack / "sources.jsonl"
|
|
39
|
+
if not (pack.is_dir() and src.exists()):
|
|
40
|
+
continue
|
|
41
|
+
for line in src.read_text(encoding="utf-8").splitlines():
|
|
42
|
+
if not line.strip():
|
|
43
|
+
continue
|
|
44
|
+
rec = json.loads(line)
|
|
45
|
+
doi = rec.get("doi")
|
|
46
|
+
if doi:
|
|
47
|
+
doi = clean_doi(doi)
|
|
48
|
+
packs = out.setdefault(doi, [])
|
|
49
|
+
if pack.name not in packs:
|
|
50
|
+
packs.append(pack.name)
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> int:
|
|
55
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
56
|
+
parser.add_argument("--sleep", type=float, default=0.3)
|
|
57
|
+
parser.add_argument("--no-fail", action="store_true")
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
|
|
60
|
+
dois = collect_dois()
|
|
61
|
+
log.info("watching %d DOIs from %d packs", len(dois), len({p for v in dois.values() for p in v}))
|
|
62
|
+
client = RegistryClient(sleep_s=args.sleep)
|
|
63
|
+
|
|
64
|
+
snapshot: dict[str, dict] = {}
|
|
65
|
+
for doi in sorted(dois):
|
|
66
|
+
verdict = client.check(doi)
|
|
67
|
+
snapshot[doi] = {
|
|
68
|
+
"status": verdict["status"],
|
|
69
|
+
"retracted": verdict["retracted"],
|
|
70
|
+
"registry_title": verdict.get("registry_title"),
|
|
71
|
+
"packs": dois[doi],
|
|
72
|
+
}
|
|
73
|
+
flag = " ⚠️RETRACTED" if verdict["retracted"] else ""
|
|
74
|
+
print(f"{verdict['status']:9s}{flag} {doi}")
|
|
75
|
+
|
|
76
|
+
state_path = OUT_DIR / "state.json"
|
|
77
|
+
prev = json.loads(state_path.read_text(encoding="utf-8"))["dois"] if state_path.exists() else {}
|
|
78
|
+
newly_retracted = [d for d, v in snapshot.items()
|
|
79
|
+
if v["retracted"] and not prev.get(d, {}).get("retracted")]
|
|
80
|
+
broken = [d for d, v in snapshot.items() if v["status"] in ("not_found", "mismatch", "error")]
|
|
81
|
+
|
|
82
|
+
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
(OUT_DIR / "state.json").write_text(
|
|
84
|
+
json.dumps({"updated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "dois": snapshot},
|
|
85
|
+
ensure_ascii=False, indent=2),
|
|
86
|
+
encoding="utf-8")
|
|
87
|
+
|
|
88
|
+
lines = ["# Retraction Watch", "",
|
|
89
|
+
f"更新时间:{time.strftime('%Y-%m-%d %H:%M %Z')};监控 DOI:{len(snapshot)}",
|
|
90
|
+
f"撤稿:{sum(1 for v in snapshot.values() if v['retracted'])};"
|
|
91
|
+
f"本次新增撤稿:{len(newly_retracted)};核验异常:{len(broken)}", ""]
|
|
92
|
+
if newly_retracted:
|
|
93
|
+
lines += ["## 🆕 本次新增撤稿", ""]
|
|
94
|
+
lines += [f"- `{d}`({', '.join(snapshot[d]['packs'])})" for d in newly_retracted]
|
|
95
|
+
lines.append("")
|
|
96
|
+
retracted_all = [d for d, v in snapshot.items() if v["retracted"]]
|
|
97
|
+
if retracted_all:
|
|
98
|
+
lines += ["## 已知撤稿(持续监控)", ""]
|
|
99
|
+
lines += [f"- `{d}`" for d in retracted_all] + [""]
|
|
100
|
+
if broken:
|
|
101
|
+
lines += ["## 核验异常", ""] + [f"- `{d}` ({snapshot[d]['status']})" for d in broken] + [""]
|
|
102
|
+
(OUT_DIR / "report.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
print(f"\nreport -> {OUT_DIR}/report.md "
|
|
105
|
+
f"(newly_retracted={len(newly_retracted)}, broken={len(broken)})")
|
|
106
|
+
return 1 if ((newly_retracted or broken) and not args.no_fail) else 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|