* docs: consolidate governance anchors and clean up test registries * refactor(cli): decompose cli into dedicated modules * test: fix broken test baselines and formatting * docs: add domain boundary READMEs for governance anchors * test: update baseline for determination lane * test: fix capability_pass expectation * test: fix CORE_SHOWCASE_SKIP_BUDGET enforcement * chore: cleanup CLI extraction and unreachable code
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
"""Diagnostic-only contract tests for ``binding.quantity_entity``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
|
|
import generate.problem_frame_contracts as problem_frame_contracts
|
|
from generate.kernel_facts import SourceSpan
|
|
from generate.problem_frame import QuantityKindDisposition
|
|
from generate.problem_frame_builder import build_problem_frame
|
|
from generate.problem_frame_contracts import assess_contracts, assess_quantity_entity
|
|
|
|
|
|
FAMILY_ID = "binding.quantity_entity"
|
|
CANDIDATE_ORGAN = "quantity_entity_binding"
|
|
|
|
|
|
def _proposal(frame):
|
|
proposals = tuple(
|
|
proposal
|
|
for proposal in frame.proposals
|
|
if proposal.family_id == FAMILY_ID
|
|
)
|
|
assert len(proposals) == 1
|
|
return proposals[0]
|
|
|
|
|
|
def _assessment(frame):
|
|
assessments = tuple(
|
|
assessment
|
|
for assessment in assess_contracts(frame)
|
|
if assessment.candidate_organ == CANDIDATE_ORGAN
|
|
)
|
|
assert len(assessments) == 1
|
|
return assessments[0]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"problem_text",
|
|
(
|
|
"A school has 100 students.",
|
|
"There are 12 apples in the basket.",
|
|
),
|
|
)
|
|
def test_exact_local_count_binding_is_proposed_then_assessed(
|
|
problem_text: str,
|
|
) -> None:
|
|
frame = build_problem_frame(problem_text)
|
|
proposal = _proposal(frame)
|
|
assessment = _assessment(frame)
|
|
|
|
assert proposal.status == "proposed"
|
|
assert proposal.diagnostic_only is True
|
|
assert proposal.serving_allowed is False
|
|
assert not hasattr(proposal, "runnable")
|
|
assert assessment.runnable is True
|
|
assert assessment.missing_bindings == ()
|
|
assert assessment.unresolved_hazards == ()
|
|
assert len(frame.quantity_kind_dispositions) == 1
|
|
assert frame.quantity_kind_dispositions[0].quantity_kind == "count"
|
|
assert frame.quantity_kind_dispositions[0].unit_mention_id is None
|
|
assert all(
|
|
span.text == problem_text[span.start:span.end]
|
|
for span in (*proposal.evidence_spans, *assessment.evidence_spans)
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_proposal_free_frame_does_not_dispatch_quantity_entity_contract() -> None:
|
|
frame = build_problem_frame("A school has 100 students.")
|
|
proposal_free = dataclasses.replace(frame, proposals=())
|
|
|
|
assert CANDIDATE_ORGAN not in {
|
|
assessment.candidate_organ
|
|
for assessment in assess_contracts(proposal_free)
|
|
}
|
|
assert assess_quantity_entity(proposal_free).runnable is False
|
|
assert (
|
|
"quantity_entity_proposal_required"
|
|
in assess_quantity_entity(proposal_free).missing_bindings
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"problem_text",
|
|
(
|
|
"There are 12.",
|
|
"Students wait in the hall.",
|
|
"There are 12 apples and oranges.",
|
|
"There are 12 and 13 apples.",
|
|
"A school has 20% of the students.",
|
|
"A basket holds 3 apples per child.",
|
|
"There are 3 more than 2 apples.",
|
|
"Tom gave Ana 3 marbles.",
|
|
"There are 12 of them.",
|
|
"There are 12. Apples fill the basket.",
|
|
"There are 12 apples, oranges, and pears.",
|
|
),
|
|
)
|
|
def test_confusers_do_not_dispatch_quantity_entity(problem_text: str) -> None:
|
|
frame = build_problem_frame(problem_text)
|
|
|
|
assert FAMILY_ID not in {proposal.family_id for proposal in frame.proposals}
|
|
assert frame.quantity_kind_dispositions == ()
|
|
assert CANDIDATE_ORGAN not in {
|
|
assessment.candidate_organ
|
|
for assessment in assess_contracts(frame)
|
|
}
|
|
|
|
|
|
def test_state_change_surface_does_not_backdoor_foundational_families() -> None:
|
|
frame = build_problem_frame("Tom gave Ana 3 marbles.")
|
|
|
|
assert FAMILY_ID not in {proposal.family_id for proposal in frame.proposals}
|
|
assert "state_change.transition" not in {
|
|
proposal.family_id for proposal in frame.proposals
|
|
}
|
|
|
|
|
|
def test_unit_entity_span_conflict_refuses_measurement_claim() -> None:
|
|
frame = build_problem_frame("The tank has 84 degrees.")
|
|
assessment = _assessment(frame)
|
|
|
|
assert assessment.runnable is False
|
|
assert "quantity_kind_unresolved" in assessment.missing_bindings
|
|
assert "unit_kind_conflict" in assessment.missing_bindings
|
|
|
|
|
|
def test_synthetic_or_text_mismatched_proposal_evidence_refuses() -> None:
|
|
frame = build_problem_frame("A school has 100 students.")
|
|
proposal = _proposal(frame)
|
|
synthetic = SourceSpan("synthetic", 0, 9)
|
|
frame = dataclasses.replace(
|
|
frame,
|
|
proposals=(dataclasses.replace(proposal, evidence_spans=(synthetic,)),),
|
|
)
|
|
|
|
assessment = assess_quantity_entity(frame)
|
|
|
|
assert assessment.runnable is False
|
|
assert "provenance_span_inexact" in assessment.missing_bindings
|
|
|
|
|
|
def test_quantity_kind_disposition_rejects_illegal_unit_states() -> None:
|
|
span = SourceSpan("12", 0, 2)
|
|
|
|
with pytest.raises(ValueError, match="count.*must not carry a unit"):
|
|
QuantityKindDisposition(
|
|
"quantity",
|
|
"entity",
|
|
"count",
|
|
"unit",
|
|
(span,),
|
|
)
|
|
with pytest.raises(ValueError, match="measurement.*require a unit"):
|
|
QuantityKindDisposition(
|
|
"quantity",
|
|
"entity",
|
|
"measurement",
|
|
None,
|
|
(span,),
|
|
)
|
|
|
|
|
|
def test_quantity_entity_replay_is_deterministic() -> None:
|
|
problem_text = "A school has 100 students."
|
|
|
|
first = build_problem_frame(problem_text)
|
|
second = build_problem_frame(problem_text)
|
|
|
|
assert first.proposals == second.proposals
|
|
assert first.bindings == second.bindings
|
|
assert first.quantity_kind_dispositions == second.quantity_kind_dispositions
|
|
assert assess_contracts(first) == assess_contracts(second)
|
|
|
|
|
|
def test_existing_proposal_first_families_do_not_gain_quantity_entity_dispatch() -> None:
|
|
percent_partition = (
|
|
"A school has 100 students. Half of the students are girls, the other half are boys. "
|
|
"20% of the girls have dogs and 10% of the boys have dogs. "
|
|
"How many students own dogs?"
|
|
)
|
|
fraction_decrease = (
|
|
"A tank's temperature will decrease to 3/4 of its current temperature. "
|
|
"If the current temperature is 84 degrees, how many degrees will it decrease by?"
|
|
)
|
|
|
|
for problem_text in (percent_partition, fraction_decrease):
|
|
frame = build_problem_frame(problem_text)
|
|
assert FAMILY_ID not in {proposal.family_id for proposal in frame.proposals}
|
|
assert CANDIDATE_ORGAN not in {
|
|
assessment.candidate_organ
|
|
for assessment in assess_contracts(frame)
|
|
}
|