mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
1d14d5c13a
* feat(evals): claim-standing stance seed set (#655 §8 seed, v0.1) 32 cross-model-authored synthetic claim/candidate items (16 en + 16 zh-TW) covering every closed stance label plus missing-evidence, metadata-only, irrelevant, and full-text slots; five closed schemas (seed set, expert label, adjudicated ground truth, subject output with no scalar-score field, score report); criterion-keyed expert label guide; asset validator with slot/binding/simplified-char/registration invariants; and the §8 mechanical scorer (no model in the loop, macro and micro separate, blocked rows never imputed). The suite is deliberately unregistered until an implementation PR carries a valid baseline row; design_target is construction intent, never ground truth. Refs #655 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z2pMsRNu386PDHeNvxR5r * fix(evals): harden #655 stance seed per dual-track review R1 - schema-level §5.1 cross-field rules in all three record schemas (relevant+not_checked requires a failure state; not_relevant forces not_checked/null/null); expert files must cover all 32 items - scorer: abstention on a gold-performed row lands in an explicit NOT_CHECKED confusion column (recall cannot be inflated by refusing); blocked/partial rows keep their failure classes visible; full-row accuracy includes evidence scope; adjudication must bind two distinct experts; strict duplicate-key-rejecting JSON loads - validator: strict loads, dead branches removed, Counter-based slot check, simplified-char list purged of Traditional homographs, discipline included in the zh screen - README documents accepted seed boundaries (runtime failure classes via future fault injection; expert-packet byte sealing belongs to the implementation PR); 17 tests including enum-sync across schemas Refs #655 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z2pMsRNu386PDHeNvxR5r * fix(evals): #655 stance seed R2 — expert-gate tightening + blocked-row failure visibility - adjudication binding requires distinct file hashes, not just distinct expert ids; new validate-expert-file subcommand checks one expert file's complete distinct coverage of the 32 items - subject schema requires a failure state on every blocked/partial row, so a silent null cannot vanish from the failure-class report - README/CHANGELOG state the actual report granularity: counts for evidence-scope and failure-class dimensions; accuracy strata by those dimensions belong to the future baseline-row compiler - 20 tests Refs #655 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z2pMsRNu386PDHeNvxR5r --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
250 lines
8.9 KiB
Python
250 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline validator for the #655 claim-standing stance seed set.
|
|
|
|
Validates `evals/heldout/claim_standing_probe/heldout_stance_set.json` against
|
|
its closed schema and the structural invariants below. File-only; no network,
|
|
model, retrieval, or dispatch surface.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import Counter
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from jsonschema import Draft202012Validator
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
SUITE_ROOT = REPO_ROOT / "evals" / "heldout" / "claim_standing_probe"
|
|
SET_PATH = SUITE_ROOT / "heldout_stance_set.json"
|
|
SET_SCHEMA = SUITE_ROOT / "heldout_stance_set.schema.json"
|
|
EXPERT_SCHEMA = SUITE_ROOT / "expert_label.schema.json"
|
|
SUITE_REGISTRY = SUITE_ROOT.parent / "suite_registry.json"
|
|
|
|
EXPECTED_SLOTS = {
|
|
"support": 2,
|
|
"contradict": 2,
|
|
"mixed": 2,
|
|
"notaddr": 2,
|
|
"insuff": 2,
|
|
"ambig": 2,
|
|
"absmiss": 1,
|
|
"metaonly": 1,
|
|
"irrel": 1,
|
|
"fulltext": 1,
|
|
}
|
|
SLOT_STANCE = {
|
|
"support": "support",
|
|
"contradict": "contradict",
|
|
"mixed": "mixed",
|
|
"notaddr": "not_addressed",
|
|
"insuff": "INSUFFICIENT_EVIDENCE",
|
|
"ambig": "AMBIGUOUS",
|
|
"fulltext": "support",
|
|
}
|
|
SLOT_COVERAGE = {"fulltext": "session_held_full_text", "metaonly": "metadata_only"}
|
|
# Heuristic screen only: a curated list of characters that are simplified-only
|
|
# in ordinary academic prose. A hit is a likely defect, not proof; characters
|
|
# with common Traditional readings (e.g. 后, 云, 从) are deliberately excluded.
|
|
SIMPLIFIED_CHARS = set(
|
|
"们会学习语记观认证论试变对关发体历礼国广边办应问题网络电产农动传"
|
|
"亚区医药币岁苏软热顾风龙买卖读书写这为么样点让还时实现"
|
|
)
|
|
|
|
|
|
class AssetError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _fail(message: str) -> None:
|
|
raise AssetError(message)
|
|
|
|
|
|
def _reject_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
value: dict[str, Any] = {}
|
|
for key, item in pairs:
|
|
if key in value:
|
|
raise ValueError(f"duplicate JSON key {key!r}")
|
|
value[key] = item
|
|
return value
|
|
|
|
|
|
def _strict_loads(raw: bytes, label: str) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(
|
|
raw.decode("utf-8"),
|
|
object_pairs_hook=_reject_pairs,
|
|
parse_constant=lambda token: (_ for _ in ()).throw(
|
|
ValueError(f"non-finite JSON value {token!r}")
|
|
),
|
|
)
|
|
except (UnicodeError, ValueError) as exc:
|
|
_fail(f"{label}: invalid strict JSON: {exc}")
|
|
if not isinstance(value, dict):
|
|
_fail(f"{label}: JSON root must be an object")
|
|
return value
|
|
|
|
|
|
def load_set() -> dict[str, Any]:
|
|
return _strict_loads(SET_PATH.read_bytes(), "seed set")
|
|
|
|
|
|
def _slot(item_id: str) -> str:
|
|
return item_id.split("-")[2]
|
|
|
|
|
|
def validate_set(value: dict[str, Any]) -> None:
|
|
schema = json.loads(SET_SCHEMA.read_bytes())
|
|
errors = sorted(
|
|
Draft202012Validator(schema).iter_errors(value), key=lambda e: str(e.path)
|
|
)
|
|
if errors:
|
|
first = errors[0]
|
|
_fail(f"schema: {list(first.path)!r}: {first.message}")
|
|
|
|
items = value["items"]
|
|
ids = [item["item_id"] for item in items]
|
|
if len(set(ids)) != len(ids):
|
|
_fail("item ids must be unique")
|
|
|
|
for prefix, language in (("csp-en-", "en"), ("csp-zh-", "zh-TW")):
|
|
subset = [item for item in items if item["item_id"].startswith(prefix)]
|
|
if len(subset) != sum(EXPECTED_SLOTS.values()):
|
|
_fail(
|
|
f"language block {language} must contain exactly "
|
|
f"{sum(EXPECTED_SLOTS.values())} items"
|
|
)
|
|
for item in subset:
|
|
if item["language"] != language:
|
|
_fail(f"{item['item_id']}: id prefix and language field disagree")
|
|
slots = Counter(_slot(item["item_id"]) for item in subset)
|
|
if slots != EXPECTED_SLOTS:
|
|
_fail(
|
|
f"language block {language} design-slot coverage is not exact: "
|
|
f"{dict(slots)!r}"
|
|
)
|
|
disciplines = [item["discipline"] for item in subset]
|
|
if len(set(disciplines)) != len(disciplines):
|
|
_fail(f"language block {language} disciplines must be distinct")
|
|
|
|
for item in items:
|
|
item_id = item["item_id"]
|
|
slot = _slot(item_id)
|
|
candidate = item["candidate"]
|
|
target = item["design_target"]
|
|
if candidate["doi"] != f"10.99999/{item_id}":
|
|
_fail(f"{item_id}: doi must be 10.99999/{item_id}")
|
|
if candidate["work_family_id"] != f"wf-{item_id}":
|
|
_fail(f"{item_id}: work_family_id must be wf-{item_id}")
|
|
|
|
if slot in SLOT_STANCE:
|
|
if (
|
|
item["relevance_design"] != "relevant"
|
|
or candidate["coverage"] != SLOT_COVERAGE.get(slot, "abstract")
|
|
or candidate["content_state"] != "available"
|
|
or target
|
|
!= {
|
|
"check_state": "performed",
|
|
"stance": SLOT_STANCE[slot],
|
|
"failure_state": None,
|
|
}
|
|
):
|
|
_fail(f"{item_id}: does not realize its design slot ({slot})")
|
|
elif slot in ("absmiss", "metaonly"):
|
|
if (
|
|
item["relevance_design"] != "relevant"
|
|
or candidate["coverage"] != SLOT_COVERAGE.get(slot, "abstract")
|
|
or candidate["content_state"] != "abstract_missing"
|
|
or candidate["evidence_text"] is not None
|
|
or target
|
|
!= {
|
|
"check_state": "not_checked",
|
|
"stance": None,
|
|
"failure_state": "abstract_missing",
|
|
}
|
|
):
|
|
_fail(
|
|
f"{item_id}: missing-evidence slot must target "
|
|
"not_checked with failure_state abstract_missing"
|
|
)
|
|
else:
|
|
# The slot table is exact, so only "irrel" can reach here.
|
|
if item["relevance_design"] != "not_relevant":
|
|
_fail(f"{item_id}: the irrelevant-candidate slot must be not_relevant")
|
|
if target != {
|
|
"check_state": "not_checked",
|
|
"stance": None,
|
|
"failure_state": None,
|
|
}:
|
|
_fail(f"{item_id}: not_relevant target is not_checked with null failure")
|
|
|
|
if item["language"] == "zh-TW":
|
|
text = "".join(
|
|
[
|
|
item["claim_text"],
|
|
item["discipline"],
|
|
candidate["title"],
|
|
candidate["venue"],
|
|
candidate["evidence_text"] or "",
|
|
]
|
|
)
|
|
hits = sorted(set(text) & SIMPLIFIED_CHARS)
|
|
if hits:
|
|
_fail(f"{item_id}: simplified-Chinese characters present: {hits!r}")
|
|
|
|
registry = _strict_loads(SUITE_REGISTRY.read_bytes(), "suite registry")
|
|
if value["suite"] in registry:
|
|
_fail(
|
|
"the seed set is unmeasured: claim_standing_probe must not be "
|
|
"registered in suite_registry.json until the implementation PR "
|
|
"carries a valid baseline row"
|
|
)
|
|
|
|
|
|
def validate_expert_file(value: dict[str, Any], set_value: dict[str, Any]) -> None:
|
|
schema = json.loads(EXPERT_SCHEMA.read_bytes())
|
|
errors = sorted(
|
|
Draft202012Validator(schema).iter_errors(value), key=lambda e: str(e.path)
|
|
)
|
|
if errors:
|
|
first = errors[0]
|
|
_fail(f"expert file schema: {list(first.path)!r}: {first.message}")
|
|
labeled = [row["item_id"] for row in value["labels"]]
|
|
if len(set(labeled)) != len(labeled):
|
|
_fail("expert file labels the same item more than once")
|
|
set_ids = {item["item_id"] for item in set_value["items"]}
|
|
if set(labeled) != set_ids:
|
|
_fail("expert file must label exactly the seed set's 32 distinct items")
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
subparsers.add_parser("validate-assets", help="validate the shipped seed set")
|
|
expert_parser = subparsers.add_parser(
|
|
"validate-expert-file",
|
|
help="validate one expert label file against the seed set",
|
|
)
|
|
expert_parser.add_argument("--expert-file", type=Path, required=True)
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
set_value = load_set()
|
|
if args.command == "validate-expert-file":
|
|
validate_expert_file(
|
|
_strict_loads(args.expert_file.read_bytes(), "expert file"),
|
|
set_value,
|
|
)
|
|
print("expert label file: complete distinct coverage of the seed set")
|
|
return 0
|
|
validate_set(set_value)
|
|
except AssetError as exc:
|
|
print(str(exc))
|
|
return 1
|
|
print("claim-standing stance seed set: all invariants hold (32 items)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|