feat(adr-0179-ex2): bare-decimal grounding in the shared round-trip primitive
EX-2 — the ONE shared-primitive (serving-path) touch of extraction richness.
_value_grounds already grounds the symbol form $N.NN (currency) and N/M (fraction);
a decimal written WITHOUT a symbol ('0.75') is never a single token (the tokenizer
splits on '.') so it failed to ground — refusing correct products like 0003
(48*24*0.75=864). Now a bare decimal 'N.M' grounds when both digit-runs appear,
symmetric with the $N.NN and N/M branches. Only returns True on a match; non-matching
decimals fall through (refuse).
wrong=0 (the load-bearing obligation, this being a shared/serving-path change):
- serving 3/47/0 BYTE-IDENTICAL (verified).
- round-trip + candidate-graph tests 51/51.
- lane-SHA gate (pinned eval lanes unchanged) — verified before merge.
Payoff: 0003 unblocked in sealed practice (search_chain -> 864 = gold; +1 flip).
Decimal cases that now ground but mis-compose become sealed eliminations (learning
signal); serving untouched. 6 EX-2 tests (decimal grounds/refuses, integer
unchanged, serving byte-identical, 0003 resolves).
This commit is contained in:
parent
16fbc600e6
commit
939fa56671
2 changed files with 76 additions and 0 deletions
|
|
@ -359,6 +359,15 @@ def _value_grounds(value_token: str, haystack_tokens: frozenset[str]) -> bool:
|
|||
m = re.fullmatch(r"(\d+)/(\d+)", value_token)
|
||||
if m is not None:
|
||||
return m.group(1) in haystack_tokens and m.group(2) in haystack_tokens
|
||||
# ADR-0179 EX-2 — bare decimal "N.M" (the currency branch above handles the
|
||||
# symbol form $N.NN; a decimal written without a symbol, e.g. "0.75", is never
|
||||
# a single token because the word-boundary tokenizer splits on ".", so it
|
||||
# grounds exactly when both digit-runs appear as tokens — symmetric with the
|
||||
# $N.NN and N/M widenings. Only returns True on a match; non-matching decimals
|
||||
# fall through to the existing paths (which ultimately refuse).
|
||||
if "." in value_token and re.fullmatch(r"\d+\.\d+", value_token) is not None:
|
||||
if all(part in haystack_tokens for part in value_token.split(".")):
|
||||
return True
|
||||
if "-" in value_token and not value_token[0].isdigit():
|
||||
try:
|
||||
from language_packs.numerics_loader import parse_compound_cardinal
|
||||
|
|
|
|||
67
tests/test_adr_0179_ex2_decimal_grounding.py
Normal file
67
tests/test_adr_0179_ex2_decimal_grounding.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""ADR-0179 EX-2 — bare-decimal grounding in the shared round-trip primitive.
|
||||
|
||||
`_value_grounds` already grounds the symbol form `$N.NN` (currency) and `N/M`
|
||||
(fraction); a decimal written without a symbol (`0.75`) was never a single token
|
||||
(the tokenizer splits on `.`) and so failed to ground — refusing correct products
|
||||
like 0003 (`48×24×0.75 = 864`). EX-2 grounds a bare decimal when both digit-runs
|
||||
appear, symmetric with those branches.
|
||||
|
||||
This is the ONE shared-primitive (serving-path) change in extraction richness, so
|
||||
the load-bearing test is wrong=0: serving stays 3/47/0 byte-identical.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from generate.math_roundtrip import _tokens, _value_grounds
|
||||
|
||||
|
||||
class TestBareDecimalGrounding:
|
||||
def test_decimal_grounds_when_digit_runs_present(self) -> None:
|
||||
toks = _tokens("They sell for $0.75 each.") # -> tokens include "0","75"
|
||||
assert _value_grounds("0.75", toks) is True
|
||||
|
||||
def test_decimal_refuses_when_a_run_absent(self) -> None:
|
||||
toks = _tokens("They sell for 80 cents.") # no "0"/"75"
|
||||
assert _value_grounds("0.75", toks) is False
|
||||
|
||||
def test_plain_decimal_in_text_grounds(self) -> None:
|
||||
toks = _tokens("It moved 2.5 meters.") # "2","5" present
|
||||
assert _value_grounds("2.5", toks) is True
|
||||
|
||||
def test_integer_grounding_unchanged(self) -> None:
|
||||
toks = _tokens("He has 48 boxes.")
|
||||
assert _value_grounds("48", toks) is True
|
||||
assert _value_grounds("49", toks) is False
|
||||
|
||||
|
||||
class TestWrongZeroPreserved:
|
||||
def test_serving_byte_identical(self) -> None:
|
||||
# the load-bearing obligation: the shared-primitive change must not shift
|
||||
# the serving contract.
|
||||
from evals.gsm8k_math.train_sample.v1.runner import (
|
||||
_CASES_PATH,
|
||||
_load_cases,
|
||||
build_report,
|
||||
)
|
||||
|
||||
counts = build_report(_load_cases(_CASES_PATH))["counts"]
|
||||
assert counts == {"correct": 3, "wrong": 0, "refused": 47}
|
||||
|
||||
|
||||
class TestUnblocksDecimalProduct:
|
||||
def test_0003_class_product_now_resolves(self) -> None:
|
||||
# 48 boxes x 24 each x $0.75 each = 864 — previously refused (0.75 ungrounded)
|
||||
from generate.derivation.multistep import search_chain
|
||||
|
||||
case = next(
|
||||
json.loads(line)
|
||||
for line in Path(
|
||||
"evals/gsm8k_math/train_sample/v1/cases.jsonl"
|
||||
).read_text().splitlines()
|
||||
if "student council" in line
|
||||
)
|
||||
res = search_chain(case["question"])
|
||||
assert res is not None and res.answer == float(case["answer_numeric"])
|
||||
Loading…
Reference in a new issue