Files
imbad0202__academic-researc…/scripts/check_reviewer_scoring_honesty.py
T

293 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Guard criterion-bound reviewer judgement and calibration honesty.
This lint protects current runtime/documentation surfaces. Historical design
records and unrelated scoring systems are intentionally out of scope.
Exit 0: contract present; 1: drift; 2: required input missing.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
LIVE_FILES = (
"academic-paper-reviewer/SKILL.md",
"academic-paper-reviewer/agents/eic_agent.md",
"academic-paper-reviewer/agents/methodology_reviewer_agent.md",
"academic-paper-reviewer/agents/domain_reviewer_agent.md",
"academic-paper-reviewer/agents/perspective_reviewer_agent.md",
"academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md",
"academic-paper-reviewer/agents/editorial_synthesizer_agent.md",
"academic-paper-reviewer/references/quality_rubrics.md",
"academic-paper-reviewer/references/review_criteria_framework.md",
"academic-paper-reviewer/references/review_quality_thinking.md",
"academic-paper-reviewer/references/editorial_decision_standards.md",
"academic-paper-reviewer/references/statistical_reporting_standards.md",
"academic-paper-reviewer/references/calibration_mode_protocol.md",
"academic-paper-reviewer/references/sprint_contract_protocol.md",
"academic-paper-reviewer/templates/peer_review_report_template.md",
"academic-paper-reviewer/examples/hei_paper_review_example.md",
"academic-paper-reviewer/examples/interdisciplinary_review_example.md",
"academic-paper-reviewer/examples/subclaim_decomposition_example.md",
"academic-paper/agents/peer_reviewer_agent.md",
"academic-paper/agents/literature_strategist_agent.md",
"academic-paper/SKILL.md",
"academic-paper/references/failure_paths.md",
"academic-paper/references/workflow_phase_details.md",
"academic-pipeline/SKILL.md",
"academic-pipeline/agents/pipeline_orchestrator_agent.md",
"academic-pipeline/references/score_trajectory_protocol.md",
"academic-pipeline/references/integrity_review_protocol.md",
"shared/handoff_schemas.md",
"docs/ARCHITECTURE.md",
"README.md",
)
REQUIRED = {
"academic-paper-reviewer/references/quality_rubrics.md": (
"`NOT_CALIBRATED` — the required default",
"Individual reviewer seats always emit `NOT_CALIBRATED`",
"current Schema 6 package adapter also remains",
"`EXCEEDS` / `MEETS` / `PARTLY_MEETS` / `DOES_NOT_MEET` / `NOT_ASSESSED`",
"no universal minimum source count",
"no universal peer-reviewed-source ratio",
"Do not map any total or count of labels mechanically",
),
"academic-paper-reviewer/templates/peer_review_report_template.md": (
"Calibration status: `NOT_CALIBRATED`",
"self-upgrade from a candidate profile",
"Do not assign points, calculate a weighted total, rank papers",
"Confidence is an uncertainty/scope disclosure only",
),
"academic-paper-reviewer/references/calibration_mode_protocol.md": (
"An ordinary review has `calibration_status: NOT_CALIBRATED`",
"The directional tier always reports `calibration_status: NOT_CALIBRATED`",
"application_status: NOT_WIRED_TO_LIVE_REVIEW",
"future application example only",
"per_dimension_gold_judgements",
"Do not report AUC: there is no continuous rubric score",
"execution_topology_sha256",
),
"academic-pipeline/references/score_trajectory_protocol.md": (
"No current producer emits `criterion_trajectory`",
"design contract, not wired to a current machine producer",
"does not calculate rubric scores, numerical deltas, or a paper-quality ranking",
),
"academic-paper/agents/literature_strategist_agent.md": (
"Do not impose a universal final-source count",
"no universal peer-reviewed ratio applies",
"no universal per-theme source count",
"no universal recent-source ratio",
"do not total points or use journal rank/citation",
),
"shared/handoff_schemas.md": (
"Current Schema 6 producer emits `NOT_CALIBRATED`",
"judgement_scale: sprint_contract",
"Producers never translate between the two scales",
"Historical numeric values cannot feed current synthesis, trajectory, or calibration",
"*(legacy/read-only)* Historical 0-100 editorial-confidence field",
),
"docs/ARCHITECTURE.md": (
"No numerical delta or typed machine trajectory is currently emitted",
"100% of E1 registered claims; semantic extraction completeness unknown",
),
"academic-paper-reviewer/agents/editorial_synthesizer_agent.md": (
"Schema 6 criterion adapter and calibration boundary",
"judgement_scale: sprint_contract",
"Set the package-level `calibration_status` to `NOT_CALIBRATED`",
"Confidence and competence disclosure — never a weighting rule",
),
"academic-paper-reviewer/references/sprint_contract_protocol.md": (
"The Schema 6 adapter preserves the sprint vocabulary exactly",
"Package calibration remains `NOT_CALIBRATED`",
),
"academic-paper-reviewer/examples/hei_paper_review_example.md": (
"R1's confidence remains uncertainty/scope metadata and does not determine the resolution",
),
"academic-paper-reviewer/examples/interdisciplinary_review_example.md": (
"Confidence is retained only as uncertainty/scope metadata and never governs arbitration",
"Confidence neither strengthens nor softens that conclusion",
),
"academic-paper-reviewer/examples/subclaim_decomposition_example.md": (
"Confidence remains uncertainty/scope metadata and does not prioritize the finding",
),
"academic-paper/SKILL.md": (
"five-perspective categorical assessment",
),
"academic-paper/references/failure_paths.md": (
"Criterion-bound trigger: at least one named, decision-critical criterion is",
"`DOES_NOT_MEET`, with the manuscript evidence and applicable standard stated",
"A decision-critical criterion is `NOT_ASSESSED` because required evidence is",
),
"academic-pipeline/SKILL.md": (
"Criterion status: [named criterion + evidence-anchored categorical judgement, or `NOT_COMPARABLE`]",
"Never reduce this to a hidden scalar",
),
"academic-pipeline/agents/pipeline_orchestrator_agent.md": (
"Criterion status: [named criterion + evidence-anchored categorical judgement, or `NOT_COMPARABLE`]",
),
"README.md": (
"Current live reviews remain `NOT_CALIBRATED`",
"the typed trajectory carrier is deferred",
),
}
for _agent in (
"academic-paper-reviewer/agents/eic_agent.md",
"academic-paper-reviewer/agents/methodology_reviewer_agent.md",
"academic-paper-reviewer/agents/domain_reviewer_agent.md",
"academic-paper-reviewer/agents/perspective_reviewer_agent.md",
"academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md",
):
REQUIRED[_agent] = (
"### Calibration Status",
"Seat reports always emit `NOT_CALIBRATED`",
"### Criterion-Bound Judgements",
"Do not total, weight, average, or mechanically map",
"Confidence is an uncertainty/scope disclosure only",
)
# Positive forms only. Prohibitory prose such as "do not calculate a weighted
# total" is allowed and protected by REQUIRED witnesses.
FORBIDDEN = (
("fixed 0-100 decision boundary", re.compile(
r"(?i)(?:>=|≥)\s*80.{0,35}\baccept|"
r"\b65\s*[-]\s*79.{0,35}\bminor|"
r"\b50\s*[-]\s*64.{0,35}\bmajor|"
r"(?:<|below)\s*50.{0,35}\breject"
)),
("numeric reviewer rubric table", re.compile(
r"(?im)^\|\s*(?:90\s*[-]\s*100|9\s*[-]\s*10)\s*\|"
)),
("weighted reviewer result", re.compile(
r"\b(?:final|overall)\s+score\s*=|"
r"^\|\s*\*\*weighted average\*\*\s*\||"
r"\bfour raw scoring-seat weighted averages\b",
re.IGNORECASE | re.MULTILINE,
)),
("fixed peer-reviewed ratio", re.compile(
r"(?i)peer[- ]reviewed\s+(?:ratio\s*)?(?:>=|≥|>|at least)\s*\d+\s*%|"
r"\d+\s*%\+?\s+peer[- ]reviewed"
)),
("cross-domain source quota", re.compile(
r"(?im)^\|\s*(?:IMRaD|Literature Review|Theoretical|Case Study|Policy Brief|Conference)\s*\|\s*\d+\s*\|"
)),
("fixed per-theme literature quota", re.compile(
r"(?i)(?:every|each)\s+theme.{0,25}(?:>=|≥|at least)\s*\d+\s+sources?"
)),
("fixed literature-gap quota", re.compile(
r"(?i)research\s+gaps?.{0,15}(?:>=|≥|at least)\s*\d+"
)),
("fixed recency ratio", re.compile(
r"(?i)currency.{0,35}(?:>=|≥|at least)\s*\d+\s*%.{0,20}(?:last|recent)"
)),
("fixed language ratio", re.compile(
r"(?i)(?:chinese|english)\s+literature.{0,25}(?:>=|≥|at least|comprise)\s*\d+\s*%"
)),
("retired score trajectory", re.compile(
r"(?i)score\s+trajectory(?:\s+tracked|\s+visualization)"
)),
("unbounded all-claim final check", re.compile(
r"(?i)final-check\s+mode:\s*100%\s+of\s+claims\b"
)),
("calibration overclaim", re.compile(
r"(?i)provides calibrated scoring rubrics|"
r"ensures consistent, reproducible scoring|"
r"without calibration.{0,80}ordinally meaningful"
)),
("retired statistical completeness score", re.compile(
r"(?i)statistical reporting completeness score"
)),
("retired score-first reviewer heuristic", re.compile(
r"(?i)write the score first"
)),
("seat-level measured-profile upgrade", re.compile(
r"(?i)(?:replace|upgrade).{0,40}PROFILE_MEASURED"
)),
("scalar checkpoint quality comparison", re.compile(
r"(?i)quality indicators:\s*\[score if available\]|"
r"latest output\s*[≥>]\s*the quality"
)),
("live typed-trajectory overclaim", re.compile(
r"(?i)(?:criterion trajectory tracks|criterion trajectory visualization|"
r"current producers emit `criterion_trajectory`)"
)),
("mechanical confidence weighting", re.compile(
r"(?is)(?:\bScore-?5\b.{0,160}\b(?:outweighs|takes precedence over|overrides)\b.{0,160}\bScore-?2\b|"
r"^\|\s*Score\s*\|[^\n]*Weight in Synthesis|"
r"this reviewer's opinion carries the highest weight)"
)),
("confidence-driven arbitration", re.compile(
r"(?is)(?:"
r"prioriti[sz]ed\s+by[^\n]{0,100}confidence[^\n]{0,40}weight|"
r"per-finding\s+confidence[^\n]{0,180}governs?\s+arbitration|"
r"arbitration\s+was\s+based\s+on\s*:\s*(?:\n[-*][^\n]*){0,5}confidence|"
r"confidence\s+is\s+\d/5[^\n]{0,160}should\s+be\s+respected|"
r"only\s+\d/5\s+confidence[^\n]{0,200}confidence\s+limitation"
r")"
)),
("retired reviewer scoring label", re.compile(
r"(?i)five-dimension scoring"
)),
("fixed dimension-score rejection threshold", re.compile(
r"(?is)(?:two|2)\s+or\s+more.{0,60}dimensions?.{0,40}"
r"(?:scored?\s*)?(?:below|under|<)\s*\d+"
)),
)
def check(root: Path) -> tuple[list[str], bool]:
errors: list[str] = []
missing = False
texts: dict[str, str] = {}
for rel in LIVE_FILES:
path = root / rel
try:
texts[rel] = path.read_text(encoding="utf-8")
except OSError as exc:
errors.append(f"{rel}: required input unavailable: {exc}")
missing = True
for rel, witnesses in REQUIRED.items():
text = texts.get(rel, "")
for witness in witnesses:
if witness not in text:
errors.append(f"{rel}: missing honesty witness {witness!r}")
for rel, text in texts.items():
for label, pattern in FORBIDDEN:
match = pattern.search(text)
if match:
excerpt = " ".join(match.group(0).split())
errors.append(f"{rel}: {label}: {excerpt!r}")
quality = texts.get("academic-paper-reviewer/references/quality_rubrics.md", "")
for legacy in ("## Decision Mapping", "## Aggregation Formula", "Score Range", "Weight:"):
if legacy in quality:
errors.append(f"quality_rubrics.md: retired numeric surface remains: {legacy!r}")
return errors, missing
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=REPO_ROOT)
args = parser.parse_args(argv)
errors, missing = check(args.root)
if errors:
for error in errors:
print(f"FAIL: {error}", file=sys.stderr)
return 2 if missing else 1
print("check_reviewer_scoring_honesty: OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())