"""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) }