* feat(kernel): operationalize ProblemFrame and deprecate legacy parsing Make #829 kernel substrate the preferred construction path via build_problem_frame, legacy parsing audit, no-new-legacy agent rules, morphology planner v2, and guard tests. No serving score or report changes. * fix(kernel): require actionable morphology planner targets * fix(kernel): clean ProblemFrame builder diagnostics * fix(kernel): make morphology planner targets actionable
105 lines
No EOL
3.5 KiB
Python
105 lines
No EOL
3.5 KiB
Python
"""Tests for generate/problem_frame_builder.py."""
|
|
from __future__ import annotations
|
|
|
|
from fractions import Fraction
|
|
|
|
from generate.problem_frame_builder import build_problem_frame
|
|
from language_packs.scalar_equivalence import extract_scalar_candidates, list_unsupported_surfaces
|
|
|
|
|
|
def _frame_names(text: str) -> tuple[str, ...]:
|
|
return tuple(f.name for f in build_problem_frame(text).process_frames)
|
|
|
|
|
|
def _hazard_categories(text: str) -> tuple[str, ...]:
|
|
return tuple(sorted({h.category for h in build_problem_frame(text).hazards}))
|
|
|
|
|
|
def test_percent_text_produces_scalar_facts_and_hazards_without_solving() -> None:
|
|
text = "Mia spent 50% of her money."
|
|
frame = build_problem_frame(text)
|
|
|
|
assert frame.scalars
|
|
assert any(s.canonical == Fraction(1, 2) for s in frame.scalars)
|
|
assert "percent_change_vs_percent_of" in _hazard_categories(text)
|
|
assert frame.question_target is None
|
|
|
|
|
|
def test_transfer_text_produces_transfer_process_frame_without_solving() -> None:
|
|
text = "Tom gave Ana 3 marbles."
|
|
frame = build_problem_frame(text)
|
|
|
|
assert "transfer" in _frame_names(text)
|
|
assert any(r.relation_type == "transfer" for r in frame.candidate_relations)
|
|
assert not frame.question_target
|
|
|
|
|
|
def test_container_text_produces_container_process_frame_without_solving() -> None:
|
|
text = "There are 4 full boxes and 3 loose crayons."
|
|
frame = build_problem_frame(text)
|
|
|
|
assert "container_packing" in _frame_names(text)
|
|
assert frame.units or frame.scalars
|
|
|
|
|
|
def test_travel_text_produces_travel_process_frame_without_solving() -> None:
|
|
text = "A car drove 30 miles each way."
|
|
frame = build_problem_frame(text)
|
|
|
|
assert "travel" in _frame_names(text)
|
|
|
|
|
|
def test_ambiguous_quarter_surfaces_carry_hazards() -> None:
|
|
text = "A quarter of the class left."
|
|
frame = build_problem_frame(text)
|
|
|
|
categories = _hazard_categories(text)
|
|
assert "unbound_base_quantity" in categories
|
|
assert any(
|
|
h.surface == "quarter" or h.category.startswith("quarter_")
|
|
for h in frame.hazards
|
|
)
|
|
|
|
|
|
def test_third_ordinal_carries_hazard() -> None:
|
|
text = "Sam finished in third place."
|
|
frame = build_problem_frame(text)
|
|
|
|
assert "third_ordinal" in _hazard_categories(text)
|
|
assert not any(s.canonical == Fraction(1, 3) for s in frame.scalars)
|
|
|
|
|
|
def test_unsupported_scalar_surfaces_do_not_broaden_adr_0128() -> None:
|
|
text = "The remaining amount is .5 of the total."
|
|
frame = build_problem_frame(text)
|
|
|
|
assert frame.scalars == ()
|
|
assert ".5" in list_unsupported_surfaces()
|
|
assert extract_scalar_candidates(text) == ()
|
|
|
|
|
|
def test_exact_spans_slice_original_text() -> None:
|
|
text = "Mia spent 50% of her money."
|
|
frame = build_problem_frame(text)
|
|
|
|
for scalar in frame.scalars:
|
|
assert scalar.source_span is not None
|
|
assert scalar.source_surface is not None
|
|
start, end = scalar.source_span
|
|
assert text[start:end] == scalar.source_surface
|
|
|
|
for quantity in frame.quantities:
|
|
span = quantity.provenance.source_spans[0]
|
|
assert text[span.start:span.end] == span.text
|
|
|
|
|
|
def test_deterministic_ordering_across_repeated_runs() -> None:
|
|
text = "Tom gave Ana 3 marbles and spent 50% of her money."
|
|
frame_a = build_problem_frame(text)
|
|
frame_b = build_problem_frame(text)
|
|
|
|
assert frame_a.scalars == frame_b.scalars
|
|
assert frame_a.units == frame_b.units
|
|
assert frame_a.process_frames == frame_b.process_frames
|
|
assert frame_a.hazards == frame_b.hazards
|
|
assert frame_a.candidate_relations == frame_b.candidate_relations |