First capability-axis iteration after ADR-0131.G baseline. Extends the
candidate-graph parser's <value> slot to recognize:
- Money symbol literals: $N and $N.NN (1-2 decimals); $N.NNN refused
- Money word forms: N dollars / N cents
- Hyphenated multi-word cardinals: twenty-five, ninety-nine, ...
All money values normalize to integer cents, unit 'cents' — pack-aligned
with en_units_v1's canonical_unit='cent' for the money dimension.
en_numerics_v1's parse_compound_cardinal handles hyphenated cardinals.
Parser changes (generate/):
- math_candidate_parser.py: _VALUE alternation widened; _resolve_value
refactored to return _ResolvedValue|None carrying optional unit
override; _INITIAL_HAS_RE unit slot made optional; dollar/dollars →
cents normalization at candidate build.
- math_roundtrip.py: new _unit_grounds helper (money-aware); _value_grounds
widened for the three new literal shapes; roundtrip_admissible uses
_unit_grounds for the unit check.
- math_candidate_graph.py: _initial_admissible and _question_admissible
use _unit_grounds.
New axis lane (evals/math_capability_axes/G3_numerics/v1/):
- 26 curated cases (20 positive across 4 classes + 6 refusal probes)
- runner.py wraps _score_one_candidate_graph; byte-equal report.json
- 20/20 positive solved correct; 6/6 refusal probes refused typed;
solved_wrong == 0; overall_pass == True
Tests: 27/27 in 0.19s. 420 existing candidate-parser/math-parser/pack
tests still green. GSM8K probe safety rail (admitted_wrong == 0)
preserved.
Honest scope-limit (documented in ADR): admission_rate on the GSM8K
probe stays at 0/50 because (a) the probe currently consults the legacy
parser path, not the candidate-graph pipeline G.3 extends, and (b) most
money-bearing GSM8K cases fail first on verb (G.1) or multi-clause (G.4)
shape, not on the money literal. The axis lane is the load-bearing
measurement for this iteration. Reserved follow-up: a small probe-
infra ADR to switch run_coverage_probe.py to the candidate-graph
pipeline.
Out of scope, deferred to G.3.1: fractions end-to-end (resolver supports
N/M but no axis cases), multi-currency (¢ € £ ¥ ₱), space-separated
multi-word cardinals (one hundred), word-number-adjective compositions
(five full boxes).
9.8 KiB
ADR-0131.G.3 — Numeric Literals (money + hyphenated cardinals)
Status: Proposed Date: 2026-05-23 Author: CORE main agent (Opus 4.7) Depends on: ADR-0131.G (capability-axis iteration discipline), ADR-0126 (candidate-graph parser), ADR-0127 (units pack), ADR-0128 (numerics pack) Parent: ADR-0131 (composite math-expert promotion gate) Foundation for: ADR-0131.G.3.1 (fractions + multi-currency follow-up)
Context
ADR-0131.G pinned the GSM8K coverage probe as a diff-able admission
number with the iteration discipline that each subsequent
ADR-0131.G. extends a single capability axis with its own
curated coverage cases, independent of GSM8K. G.3 is the first
literal-recognition axis: extending the candidate-graph parser's
<value> slot to recognize money literals and hyphenated multi-word
cardinals by consuming the already-ratified en_units_v1
(ADR-0127) and en_numerics_v1 (ADR-0128) packs.
The baseline train_sample_coverage_report.json (#181) shows the
target shapes appear in real refusals: Tina makes $18.00 an hour,
Aaron and his brother Carson each saved up $40, hyphenated forms
like 10 one-hour videos. Most refusals fail on structural
clause shape first (verb, multi-clause), so admission lift on the
probe requires sibling iterations (G.1 verbs, G.4 multi-clause) to
land alongside. G.3 alone is necessary but not sufficient for
probe lift; the load-bearing measurement for this iteration is the
axis lane.
Decision
Scope (v1 — closed set)
- Money symbol literal —
$Nand$N.NN(1–2 decimal places).$N.NNN(3+ decimals) refused. - Money word form —
<N> dollarsand<N> cents. Recognized via existing unit-slot path; normalized at candidate build. - Hyphenated multi-word cardinal —
twenty-five,ninety-nine, etc. Resolved vialanguage_packs.numerics_loader.parse_compound_cardinal.
Canonical money normalization (load-bearing)
All money values normalize to integer cents, unit 'cents'.
This is pinned by en_units_v1:
{"surface": "money", "canonical_unit": "cent",
"is_canonical_for_dimension": true, "dimension": "money"}
cent is the canonical surface; lookup_unit("cent").plural is
"cents". The parser canonicalizes both money symbols and the
word forms N dollars / N cents to plural cents so the
question-side canonicalization (How many cents does X have? →
Unknown.unit='cents') matches by exact equality.
dollar deliberately does not become the surface unit, even
though it's more user-natural. Diverging from a ratified pack's
canonical_unit because the alternative sounds nicer is the small
precedent that erodes the "consult the pack" architecture. The
realizer/formatter is the right layer for user-facing display of
"$40.00" — that's a separate concern.
Refusal probes (closed set)
$N.NNN+(3+ decimal places) — out-of-scope precision.N/0— division by zero.- Unrecognized hyphenated compositions (
five-and-a-half,gobbledy-gook) — not resolvable viaparse_compound_cardinal. - Percent literals (
50%) — out-of-scope.
Out-of-scope, explicitly deferred to G.3.1+
- Fractions in initial-possession / operations (
N/M). The_resolve_valueresolver supportsN/Mtoken-level (returnsFraction(N,M)→ float), but no axis cases exercise it end-to-end because the initial-possession regex's "of " substance-qualifier handling needs widening to admit"Bob has 3/4 of a cup."cleanly. Token-level recognition is ratified now; pipeline-level use is G.3.1. - Multi-currency (
¢ € £ ¥ ₱). US-only in v1. - Word-number compositions with adjective insertion (
five full boxesper ADR-0127 substance-qualifier precedent). - Multi-token space-separated cardinals (
one hundred,two thousand).parse_compound_cardinalsupports these; the parser doesn't yet match them in the value slot because they'd span the unit slot boundary. Defer. - Scientific notation, locale separators, percentages.
What changed in code
generate/math_candidate_parser.py
- New constants for the three widened value shapes (
_MONEY_SYMBOL,_SLASH_FRACTION,_HYPHENATED_CARDINAL);_VALUEwidened to include them in alternation. _resolve_value(token)refactored to return_ResolvedValue | None(wasint). Returnsunit_override='cents'for money symbols; returnsNone(refusal) for out-of-scope shapes._INITIAL_HAS_REunit slot made optional (money symbols carry their unit implicitly); trailing(?:in|of|for|with)preposition phrases discardable.extract_initial_candidates+_build_op_candidateupdated to honorunit_override, apply dollar/dollars → cents normalization via_money_unit_normalization, and emit no candidate when neither the resolver nor the regex provides a unit.
generate/math_roundtrip.py
- New
_unit_grounds(unit_token, source_span, haystack_tokens)helper: word-token containment plus money-aware grounding (when unit iscent/cents, accept$in source ordollar/dollarsin tokens). _value_groundswidened to handle money symbols (digit-parts of$N.NNmust each be in source tokens), slash fractions (numerator + denominator both ground), hyphenated cardinals (every component grounds OR the compound's digit form grounds).roundtrip_admissibleunit check upgraded from_token_into_unit_grounds.
generate/math_candidate_graph.py
_initial_admissibleand_question_admissibleunit checks upgraded to_unit_grounds.
New files
evals/math_capability_axes/G3_numerics/v1/cases.jsonl— 26 curated cases (5 per positive class + 6 refusal probes).evals/math_capability_axes/G3_numerics/v1/runner.py— pure adapter overevals.gsm8k_math.runner._score_one_candidate_graph; byte-equalreport.jsonacross runs.tests/test_adr_0131_G3_numerics.py— 27 tests.
Evidence
- Axis lane (
python3 -m evals.math_capability_axes.G3_numerics.v1.runner):- 20/20 positive cases solved correct
- 6/6 refusal probes refused with typed reason
solved_wrong == 0(safety rail intact)correct_rate_on_positive_cases == 1.0- report.json byte-equal across two runs
- Test suite: 27/27 pass in 0.19s.
- Regression: 420 existing candidate-parser + math-parser + pack tests pass with the widening.
- GSM8K probe (
evals/gsm8k_math/train_sample/v1/run_coverage_probe.py): unchanged at 0/50 admission (probe uses the legacy parser path, not the candidate-graph pipeline this iteration extends).admitted_wrong == 0(safety rail) preserved.
Honest scope-limit disclosure
ADR-0131.G's discipline says each iteration's GSM8K-probe gate is
"admission_rate strictly increases OR a refused-reason family is
deliberately reduced." Neither holds for G.3 in isolation, because:
- The
run_coverage_probe.pyprobe goes throughevals.gsm8k_math.runner._score_one→parse_problem(the legacy first-match-wins parser, ADR pre-0126), not the candidate-graph pipeline G.3 extends. - Even if the probe consulted the candidate graph, most
money-bearing GSM8K cases (
Tina makes $18.00 an hour) fail first on the verb (makes, rate-introducing) or multi-clause shape (Aaron and his brother Carson each saved up $40); the money literal is a downstream refusal cause.
G.3's load-bearing gate is therefore the axis lane (full end-to-end correctness on 20 curated cases that do exercise the new capability through the candidate-graph pipeline). Probe lift will accumulate as sibling axes (G.1 verb classes, G.4 multi- clause) land alongside.
Reserved follow-up: a small probe-infrastructure ADR should
switch run_coverage_probe.py to call
_score_one_candidate_graph instead of _score_one, so future
G. iterations show probe lift. That's a one-line change but
needs its own scoped PR (it shifts the probe's measurement
substrate; the resulting admission_rate delta should be
explicitly attributed, not buried inside a capability-axis PR).
Composition with other in-flight work
- L9 / ADR-0131.G.1 (verb classes): lands the verbs that unlock
Tina makes $18.00 an hourshape on the probe; G.3 already handles the$18.00literal so admission flips when G.1 lands. - L10 / ADR-0131.G.2 (comparatives): independent; no literal-class interaction.
- L12 / ADR-0131.G.4 (multi-clause): unlocks
Aaron and his brother Carson each saved up $40shape; G.3 already handles the$40literal. - ADR-0131.G.3.1 (deferred follow-up): fractions end-to-end,
multi-currency (
¢ € £ ¥ ₱), space-separated multi-word cardinals (one hundred), word-number-adjective compositions (five full boxes).
CLAUDE.md PR-checklist answers
- Capability/performance/security added or protected: Adds pack-consuming literal recognition for money symbols, money word forms, and hyphenated multi-word cardinals at the candidate-graph parser layer.
- Invariant proving the field remains valid:
solved_wrong == 0on the axis lane (27 tests);admitted_wrong == 0on the GSM8K probe (preserved). - CLI/eval lane proving correctness:
python3 -m evals.math_capability_axes.G3_numerics.v1.runnerandpytest tests/test_adr_0131_G3_numerics.py. - Avoided hidden normalization / stochastic fallback /
approximate recall / unreviewed mutation: Yes. All lookups
are deterministic, pack-driven (
en_units_v1,en_numerics_v1). Money normalization is a single fixed rule (cents). Refusal-first preserved at every layer. - Trust boundary: Inputs are user-controlled text → parser
regex; widened regex stays bounded (closed alternation set,
no backreferences, no catastrophic-backtracking-risk
patterns). Pack loader paths consult
language_packs/data/<pack_id>/only; no dynamic imports, no filesystem traversal beyond pack root.