Files
imbad0202__academic-researc…/scripts/test_check_sprint_contract.py
T
Edward Cheng-I Wu 6b7ee6dcae fix: Astra request compat, no-delegation citation transport, hedge/quota prompt repairs, audit provenance (#823–#826) (#827)
* fix: Astra request compatibility, no-delegation citation transport, hedge/quota prompt repairs, audit provenance (#823 #824 #825 #826)

#823 — OpenAI request builders (smoke entrypoint + documented example) drop
`temperature`, which GPT-6 Astra rejects; the per-model effort vocabulary
lives in scripts/cross_model_verification/openai_effort_guard.sh, sourced by
both, and an unsupported explicit Astra value fails before curl. Hermetic
fake-curl test runs both surfaces.

#824 — the contained Codex citation transport rejects effort=ultra with
REASONING_EFFORT_REQUIRES_DELEGATION before detection/auth/tempdir/launch on
both entry paths (codex-cli 0.153.4 defines ultra as the multiAgentMode
replacement). Model-independent by design.

#825 — hedging can no longer rescue an unsupported claim (writer recovery
tree, CER fallback row, temporal rule 5 in writer + both compiler mirrors,
writer contract D2); universal prose quotas in the writer, compilers,
writing_quality_check.md, academic-paper/SKILL.md, and contract D6 become
diagnostics subordinate to author/venue requirements. Audit inventory
corrected in place; held-out seed evals/heldout/unsupported_claim_recovery
(NOT_RUN) registered.

#826 — run_codex_audit.sh pins gpt-6-astra/xhigh and records both in a new
sidecar `model` block; claim_audit_pipeline binds an unknown judge identity
to a run-local cache key (no cross-run reuse) instead of defaulting to
gpt-5.5-xhigh.

Review: /simplify (4 angles), codex gpt-5.6-sol xhigh 2 rounds (r1: 1 P1 +
1 P2 + 2 P3 fixed; r2: 0 P1/P2), /security-review 0 findings; all 102
spec-consistency steps + pytest manifest replayed locally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNKiXpdHx1T5F5RbXT2Ueu

* docs(claude): record the #824 ultra reversal in the v3.21.2 key-additions line

The v3.21.2 bullet still said the contained Codex citation transport accepts
ultra; #824 on this branch rejects it as a delegation request. Add the
reversal so the live instruction surface matches the transport.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7emV5r2aqZDJzAyYVuuDo

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 08:08:30 +09:00

459 lines
16 KiB
Python

"""Schema 13.2 validator tests."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from scripts import check_sprint_contract as checker
from tests.test_helpers import run_script
REPO = Path(__file__).resolve().parents[1]
SCRIPT = REPO / "scripts/check_sprint_contract.py"
FULL_PATH = REPO / "shared/contracts/reviewer/full.json"
MF_PATH = REPO / "shared/contracts/reviewer/methodology_focus.json"
WRITER_PATH = REPO / "shared/contracts/writer/full.json"
EVALUATOR_PATH = REPO / "shared/contracts/evaluator/full.json"
def load(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def full() -> dict:
return load(FULL_PATH)
@pytest.mark.parametrize("path", [FULL_PATH, MF_PATH, WRITER_PATH, EVALUATOR_PATH])
def test_shipped_templates_validate(path):
contract = load(path)
assert checker.validate(contract) == []
assert checker.check_structural_invariants(contract) == []
def test_reviewer_re_review_mode_rejected():
"""#576 Spec B §5.4: `reviewer_re_review` left the Schema 13 enum.
Re-review is governed by the dedicated contract family under
shared/contracts/re_review/ (+ scripts/check_re_review_synthesis.py),
not by a Schema 13 sprint contract — a contract claiming the mode
must fail validation.
"""
contract = full()
contract["mode"] = "reviewer_re_review"
errors = checker.validate(contract)
assert any("reviewer_re_review" in e or "mode" in e for e in errors)
def test_branch13_reviewer_without_role_scope_fails():
contract = full()
contract["acceptance_dimensions"][0].pop("eligible_roles")
contract["acceptance_dimensions"][0].pop("owner_role")
assert any("eligible_roles" in error or "owner_role" in error
for error in checker.validate(contract))
def test_owner_must_be_eligible():
contract = full()
contract["acceptance_dimensions"][0]["owner_role"] = "eic"
assert any("owner_role" in error
for error in checker.check_structural_invariants(contract))
def test_roles_must_be_mode_subset():
contract = load(MF_PATH)
contract["acceptance_dimensions"][0]["eligible_roles"].append("domain")
assert any("outside" in error
for error in checker.check_structural_invariants(contract))
def test_every_mode_role_must_have_a_dimension():
contract = full()
for dim in contract["acceptance_dimensions"]:
dim["eligible_roles"] = [
role for role in dim["eligible_roles"] if role != "perspective"
]
if dim["owner_role"] == "perspective":
dim["eligible_roles"] = ["eic"]
dim["owner_role"] = "eic"
assert any("perspective" in error
for error in checker.check_structural_invariants(contract))
@pytest.mark.parametrize("path", [WRITER_PATH, EVALUATOR_PATH])
def test_writer_evaluator_reject_reviewer_fields(path):
contract = load(path)
contract["acceptance_dimensions"][0]["eligible_roles"] = ["eic"]
contract["acceptance_dimensions"][0]["owner_role"] = "eic"
assert any("reviewer-only" in error
for error in checker.check_structural_invariants(contract))
def test_scoring_plan_requires_five_canonical_fields():
contract = full()
required = contract["measurement_procedure"]["scoring_plan_schema"]["required"]
required.remove("what_triggers_fatal")
assert checker.validate(contract)
def test_hybrid_action_rejected_by_branch4():
contract = full()
contract["failure_conditions"][0]["action"] = (
"editorial_decision=reject_or_major_revision"
)
assert checker.validate(contract)
@pytest.mark.parametrize(
"expression",
[
"any high dimension has a fatal block",
"D4 has a fatal block",
"D5 has a fatal block",
],
)
def test_fatal_atom_mandatory_scope_only(expression):
contract = full()
contract["failure_conditions"][0]["expression"] = expression
assert any("fatal atom" in error
for error in checker.check_structural_invariants(contract))
def test_mandatory_fatal_atoms_are_valid():
contract = full()
assert not any("fatal atom" in error
for error in checker.check_structural_invariants(contract))
def test_sc12_single_judge_mandatory_warning():
warnings = checker.warn_suspicious(full(), "v3.20.0")
assert {line.split("dimension ")[1].split()[0] for line in warnings
if "SC-12" in line} == {"D1", "D2", "D6"}
def test_schema_rejects_duplicate_eligible_role():
contract = full()
contract["acceptance_dimensions"][0]["eligible_roles"].append("methodology")
assert checker.validate(contract)
def test_duplicate_dimension_and_condition_ids_fail_invariants():
contract = full()
contract["acceptance_dimensions"][1]["id"] = "D1"
contract["failure_conditions"][1]["condition_id"] = "F1"
errors = checker.check_structural_invariants(contract)
assert any("duplicate acceptance_dimensions id" in error for error in errors)
assert any("duplicate failure_conditions" in error for error in errors)
def test_f0_accept_grade_is_schema_required():
contract = full()
contract["failure_conditions"] = [
condition for condition in contract["failure_conditions"]
if condition["condition_id"] != "F0"
]
assert checker.validate(contract)
def test_full_and_mf_exact_eligibility_maps():
full_map = {
dim["id"]: (dim["eligible_roles"], dim["owner_role"])
for dim in full()["acceptance_dimensions"]
}
assert full_map == {
"D1": (["methodology"], "methodology"),
"D2": (["domain"], "domain"),
"D3": (["da", "methodology"], "da"),
"D4": (["perspective"], "perspective"),
"D5": (["eic"], "eic"),
"D6": (["eic"], "eic"),
}
mf_map = {
dim["id"]: (dim["eligible_roles"], dim["owner_role"])
for dim in load(MF_PATH)["acceptance_dimensions"]
}
assert mf_map == {
"D1": (["methodology"], "methodology"),
"D2": (["eic"], "eic"),
}
def test_writer_evaluator_byte_unchanged_against_hardcoded_baseline():
"""Spec A zero-touch pair, with non-self-referential byte witnesses."""
import hashlib
expected = {
"shared/contracts/writer/full.json":
"ee29da7a0efaf17b7f90059e5a158ba55f4dd3d210062e4d3855da6d03f3f4a3",
"shared/contracts/evaluator/full.json":
"ce3b3e19f1da68985ebeb2dd2d7904d7343724d0847fff4e08704d79f083158a",
}
for rel, digest in expected.items():
assert hashlib.sha256((REPO / rel).read_bytes()).hexdigest() == digest
SCHEMA_MUTATIONS = (
"missing_contract_id",
"missing_mode",
"missing_stage",
"missing_baseline_version",
"missing_dimensions",
"missing_conditions",
"bad_contract_id",
"bad_mode",
"empty_stage",
"zero_panel",
"empty_dimensions",
"bad_dimension_id",
"bad_dimension_name",
"empty_description",
"bad_priority",
"empty_eligible_roles",
"bad_eligible_role",
"bad_owner_role",
"extra_dimension_field",
"short_scoring_schema",
"duplicate_scoring_field",
"typo_scoring_field",
"zero_paraphrase_minimum",
"empty_conditions",
"bad_condition_id",
"negative_severity",
"excessive_severity",
"bad_quantifier",
"empty_expression",
"bad_action",
"missing_quantifier",
"missing_panel",
"short_override_ladder",
"misordered_override_ladder",
)
def apply_schema_mutation(contract: dict, case: str) -> None:
dim = contract["acceptance_dimensions"][0]
condition = contract["failure_conditions"][0]
if case.startswith("missing_") and case.removeprefix("missing_") in {
"contract_id", "mode", "stage", "baseline_version",
}:
contract.pop(case.removeprefix("missing_"))
elif case == "missing_dimensions":
contract.pop("acceptance_dimensions")
elif case == "missing_conditions":
contract.pop("failure_conditions")
elif case == "bad_contract_id":
contract["contract_id"] = "BAD"
elif case == "bad_mode":
contract["mode"] = "reviewer_quick"
elif case == "empty_stage":
contract["stage"] = ""
elif case == "zero_panel":
contract["panel_size"] = 0
elif case == "empty_dimensions":
contract["acceptance_dimensions"] = []
elif case == "bad_dimension_id":
dim["id"] = "D01"
elif case == "bad_dimension_name":
dim["name"] = "Bad Name"
elif case == "empty_description":
dim["description"] = ""
elif case == "bad_priority":
dim["priority"] = "urgent"
elif case == "empty_eligible_roles":
dim["eligible_roles"] = []
elif case == "bad_eligible_role":
dim["eligible_roles"] = ["copyeditor"]
elif case == "bad_owner_role":
dim["owner_role"] = "copyeditor"
elif case == "extra_dimension_field":
dim["scoring_scale"] = ["pass"]
elif case == "short_scoring_schema":
contract["measurement_procedure"]["scoring_plan_schema"]["required"].pop()
elif case == "duplicate_scoring_field":
required = contract["measurement_procedure"]["scoring_plan_schema"]["required"]
required[-1] = required[0]
elif case == "typo_scoring_field":
contract["measurement_procedure"]["scoring_plan_schema"]["required"][-1] = (
"fatal_trigger"
)
elif case == "zero_paraphrase_minimum":
contract["measurement_procedure"]["paraphrase_minimum_dimensions"] = 0
elif case == "empty_conditions":
contract["failure_conditions"] = []
elif case == "bad_condition_id":
condition["condition_id"] = "F01"
elif case == "negative_severity":
condition["severity"] = -1
elif case == "excessive_severity":
condition["severity"] = 101
elif case == "bad_quantifier":
condition["cross_reviewer_quantifier"] = "plurality"
elif case == "empty_expression":
condition["expression"] = ""
elif case == "bad_action":
condition["action"] = "editorial_decision=revise"
elif case == "missing_quantifier":
condition.pop("cross_reviewer_quantifier")
elif case == "missing_panel":
contract.pop("panel_size")
elif case in {"short_override_ladder", "misordered_override_ladder"}:
contract["override_ladder"] = [
{"round": 1, "trigger": "first", "required": ["a"]},
{"round": 2, "trigger": "second", "required": ["b"]},
{"round": 3, "trigger": "third", "required": ["c"]},
]
if case == "short_override_ladder":
contract["override_ladder"].pop()
else:
contract["override_ladder"][0]["round"] = 2
else: # pragma: no cover - guards the test table itself
raise AssertionError(case)
@pytest.mark.parametrize("case", SCHEMA_MUTATIONS)
def test_legacy_schema_mutations_still_fail(case):
contract = full()
apply_schema_mutation(contract, case)
assert checker.validate(contract), case
WARNING_CASES = (
("sc1_lag", "SC-1", True),
("sc1_exact_two", "SC-1", False),
("sc1_no_current", "SC-1", False),
("sc2_single", "SC-2", True),
("sc3_no_mandatory", "SC-3", True),
("sc4_orphan", "SC-4", True),
("sc5_missing_output", "SC-5", True),
("sc7_conflict", "SC-7", True),
("sc9_impossible", "SC-9", True),
("sc10_unreferenced", "SC-10", True),
("sc11_single", "SC-11", True),
("sc11_mode_mismatch", "SC-11", True),
("writer_no_sc5", "SC-5", False),
("writer_no_sc11", "SC-11", False),
)
@pytest.mark.parametrize("case,fragment,present", WARNING_CASES)
def test_legacy_warning_boundaries(case, fragment, present):
contract = full()
current = None
if case == "sc1_lag":
contract["baseline_version"], current = "v3.3.0", "v3.6.2"
elif case == "sc1_exact_two":
contract["baseline_version"], current = "v3.4.0", "v3.6.2"
elif case == "sc1_no_current":
contract["baseline_version"] = "v3.3.0"
elif case == "sc2_single":
contract["acceptance_dimensions"] = contract["acceptance_dimensions"][:1]
elif case == "sc3_no_mandatory":
for dim in contract["acceptance_dimensions"]:
dim["priority"] = "normal"
elif case == "sc4_orphan":
contract["failure_conditions"][0]["expression"] = "D99 scores 'block'"
elif case == "sc5_missing_output":
contract["measurement_procedure"]["reviewer_must_output_before_paper"] = [
"contract_paraphrase"
]
elif case == "sc7_conflict":
contract["failure_conditions"][1]["severity"] = (
contract["failure_conditions"][0]["severity"]
)
elif case == "sc9_impossible":
contract["measurement_procedure"]["paraphrase_minimum_dimensions"] = 99
elif case == "sc10_unreferenced":
contract["failure_conditions"] = [
{
"condition_id": "F0",
"severity": 0,
"cross_reviewer_quantifier": "all",
"expression": "every dimension scores 'pass'",
"action": "editorial_decision=accept",
}
]
elif case == "sc11_single":
contract["panel_size"] = 1
elif case == "sc11_mode_mismatch":
contract["panel_size"] = 4
elif case.startswith("writer_"):
contract = load(WRITER_PATH)
else: # pragma: no cover - guards the test table itself
raise AssertionError(case)
warnings = checker.warn_suspicious(contract, current)
assert any(
warning.startswith(f"{fragment} WARNING") for warning in warnings
) is present
@pytest.mark.parametrize("case,expected", (("valid", 0), ("missing", 1), ("bad_json", 1)))
def test_cli_executes_real_process(tmp_path, case, expected):
if case == "missing":
path = tmp_path / "missing.json"
elif case == "bad_json":
path = tmp_path / "bad.json"
path.write_text("{", encoding="utf-8")
else:
path = tmp_path / "valid.json"
path.write_text(json.dumps(full()), encoding="utf-8")
result = run_script(SCRIPT, str(path))
assert result.returncode == expected, result.stderr
@pytest.mark.parametrize(
"path,required_field",
(
(WRITER_PATH, "pre_commitment_artifacts"),
(EVALUATOR_PATH, "disagreement_handling"),
),
)
def test_generator_mode_specific_artifact_is_required(path, required_field):
contract = load(path)
contract.pop(required_field)
assert checker.validate(contract)
@pytest.mark.parametrize(
"path,bad_action",
(
(WRITER_PATH, "editorial_decision=accept"),
(EVALUATOR_PATH, "writer_decision=accept"),
),
)
def test_generator_mode_action_enum_is_pinned(path, bad_action):
contract = load(path)
contract["failure_conditions"][0]["action"] = bad_action
assert checker.validate(contract)
@pytest.mark.parametrize(
"path,source",
(
(
WRITER_PATH,
("pre_commitment_artifacts", "acceptance_criteria_paraphrase"),
),
(EVALUATOR_PATH, ("disagreement_handling",)),
),
)
def test_generator_sc9_reads_mode_specific_source(path, source):
contract = load(path)
target = contract
for key in source:
target = target[key]
target["minimum_dimensions" if len(source) == 2 else "paraphrase_minimum_dimensions"] = 99
assert any(
warning.startswith("SC-9 WARNING")
for warning in checker.warn_suspicious(contract, None)
)
@pytest.mark.parametrize("path", (WRITER_PATH, EVALUATOR_PATH))
@pytest.mark.parametrize("warning", ("SC-5", "SC-11"))
def test_generator_modes_do_not_receive_reviewer_only_warnings(path, warning):
assert not any(
item.startswith(f"{warning} WARNING")
for item in checker.warn_suspicious(load(path), None)
)