fix(grounding): normalize explicit percent conversion without exempting data operands
This commit is contained in:
1 parent
6894824b26
commit
936fcd0ba3
2 files changed
+69
-2
No files matched your search
@@ -1803,6 +1803,7 @@ class _PolicyMixin:
|
||||
records: Sequence[EvidenceRecord],
|
||||
*,
|
||||
money: bool = False,
|
||||
percent: bool = False,
|
||||
scalar_parameters: Sequence[float] = (),
|
||||
) -> tuple[float, list[float]] | str | None:
|
||||
"""Evaluate a declaration's note as an observation-anchored formula.
|
||||
@@ -1813,6 +1814,8 @@ class _PolicyMixin:
|
||||
subtracted term still needs an observed anchor. Literal 1 is the
|
||||
multiplicative identity (also used in ratio-minus-one expressions),
|
||||
and square/cube exponents belong to the operator, not the operands.
|
||||
A percent claim's explicit trailing *100 is normalized as a unit
|
||||
conversion before checking the remaining data operands.
|
||||
"""
|
||||
if declaration is None or not declaration.note.strip():
|
||||
return "no_formula"
|
||||
@@ -1820,6 +1823,21 @@ class _PolicyMixin:
|
||||
if evaluated is None:
|
||||
return "formula_not_evaluable"
|
||||
result, operands, tree = evaluated
|
||||
# A percent claim may spell its final unit conversion explicitly.
|
||||
# Normalize ONLY an outer, trailing literal *100 back to a fraction;
|
||||
# do not broaden result matching or exempt any inner data operand.
|
||||
# Numeric magnitude elsewhere never decides whether an operand is data.
|
||||
if (
|
||||
percent
|
||||
and isinstance(tree.body, ast.BinOp)
|
||||
and isinstance(tree.body.op, ast.Mult)
|
||||
and isinstance(tree.body.right, ast.Constant)
|
||||
and _is_number(tree.body.right.value)
|
||||
and tree.body.right.value == 100
|
||||
):
|
||||
result /= 100
|
||||
operands = operands[:-1]
|
||||
tree = ast.Expression(body=tree.body.left)
|
||||
# Two instruments' bars and no resolved symbol: arithmetic anchored on
|
||||
# the session's pools, or on one instrument's prints the ref names,
|
||||
# would look anchored with nothing to anchor it to. Only evidence that
|
||||
@@ -1906,7 +1924,7 @@ class _PolicyMixin:
|
||||
) -> list[dict[str, Any]]:
|
||||
"""A derived figure must be the arithmetic its note states."""
|
||||
derivation = self._derivation(
|
||||
declaration, symbol, records, money=figure.currency,
|
||||
declaration, symbol, records, money=figure.currency, percent=figure.percent,
|
||||
scalar_parameters=scalar_parameters,
|
||||
)
|
||||
if isinstance(derivation, str):
|
||||
@@ -1997,7 +2015,7 @@ class _PolicyMixin:
|
||||
)
|
||||
]
|
||||
derivation = self._derivation(
|
||||
declaration, symbol, records, money=figure.currency,
|
||||
declaration, symbol, records, money=figure.currency, percent=figure.percent,
|
||||
scalar_parameters=scalar_parameters,
|
||||
)
|
||||
if not isinstance(derivation, str) and self._result_matches(figure, derivation[0]):
|
||||
|
||||
@@ -91,3 +91,52 @@ def test_a_short_parameter_label_must_not_match_inside_an_unrelated_word(tmp_pat
|
||||
)
|
||||
assert not verdict.valid
|
||||
assert any(issue.get("reason") == "operand_not_observed" for issue in verdict.issues)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("formula", ["(125 + 75) / 10000 * 100", "((125 + 75) / 10000) × 100.0"])
|
||||
def test_explicit_percent_conversion_keeps_data_operands_grounded(tmp_path, formula):
|
||||
verdict = _ledger(tmp_path, (125, 75, 10000)).validate_final_answer(
|
||||
f"Ratio: 2.00%.\n```figures\n2.00% | derived | {formula} | measure\n```"
|
||||
)
|
||||
assert verdict.valid, verdict.issues
|
||||
|
||||
|
||||
@pytest.mark.parametrize("values", [(125,), (10000,), (125, 75), (75, 10000)])
|
||||
def test_percent_conversion_does_not_supply_missing_data(tmp_path, values):
|
||||
verdict = _ledger(tmp_path, values).validate_final_answer(
|
||||
"Ratio: 2.00%.\n```figures\n2.00% | derived | (125 + 75) / 10000 * 100 | measure\n```"
|
||||
)
|
||||
assert not verdict.valid
|
||||
assert verdict.issues[0]["reason"] in {"operand_not_observed", "additive_operand_not_observed"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("figure", ["1.99%", "200.00%", "-2.00%"])
|
||||
def test_explicit_percent_conversion_does_not_widen_value_or_sign_matching(tmp_path, figure):
|
||||
verdict = _ledger(tmp_path, (125, 75, 10000)).validate_final_answer(
|
||||
f"Ratio: {figure}.\n```figures\n{figure} | derived | (125 + 75) / 10000 * 100 | measure\n```"
|
||||
)
|
||||
assert not verdict.valid
|
||||
assert verdict.issues[0]["reason"] == "derivation_result_mismatch"
|
||||
|
||||
|
||||
def test_percent_conversion_cannot_exempt_an_inner_hundred(tmp_path):
|
||||
verdict = _ledger(tmp_path, (125,)).validate_final_answer(
|
||||
"Ratio: 125.00%.\n```figures\n125.00% | derived | 125 / 100 * 100 | measure\n```"
|
||||
)
|
||||
assert not verdict.valid
|
||||
assert verdict.issues[0]["reason"] == "operand_not_observed"
|
||||
|
||||
|
||||
def test_percent_conversion_can_convert_one_observed_fraction(tmp_path):
|
||||
verdict = _ledger(tmp_path, (0.125,)).validate_final_answer(
|
||||
"Ratio: 12.50%.\n```figures\n12.50% | derived | 0.125 * 100 | measure\n```"
|
||||
)
|
||||
assert verdict.valid, verdict.issues
|
||||
|
||||
|
||||
def test_non_percent_claim_does_not_gain_a_free_multiplier(tmp_path):
|
||||
verdict = _ledger(tmp_path, (125, 75, 10000)).validate_final_answer(
|
||||
"Score: 2.00.\n```figures\n2.00 | derived | (125 + 75) / 10000 * 100 | measure\n```"
|
||||
)
|
||||
assert not verdict.valid
|
||||
assert verdict.issues[0]["reason"] == "operand_not_observed"
|
||||
Reference in new issue
Block a user