ADR-0127 expanded from minimum-viable to EXHAUSTIVE: - 13 dimensions (7 base + 6 derived via dimension algebra) - ~150 unit lemmas covering imperial+metric for length/time/mass/ volume/money/temperature/area, with full irregular-plural set - Multi-word structural rules (square <length>, <length>/<time>) - ~25 container nouns, ~6 rate connectors, ~15 symbol/affixes - Conversion graph EXHAUSTIVE within each dimension (~80 edges) - Temperature edges carry affine offset field (F/C/K) - Ratification adds: exhaustive-coverage gate, NIST/ISO provenance, dimension-algebra closure ADR-0128 NEW: en_numerics_v1 sibling pack - Exhaustive cardinals (0..20 + tens + magnitudes) + compound rule - Exhaustive ordinals (1st..31st covers calendar/position) - Fractions (named + symbol + slash-form + mixed numbers) - Multipliers (double/triple/twice/half + N-times rule) - Quantifiers with semantic_type (indefinite -> refusal preserves wrong==0) - Comparison anchors migrated from math_roundtrip.py hard-coded tables (ratifiability) - Number-format regexes (digits/decimals/percentages/fractions/ mixed) with test corpus - Cross-pack consistency check with en_units_v1 fraction symbols Joint exit gate: train sample re-run with both packs mounted. If passed -> sealed holdout. If missed -> real Path-B trigger (full deterministic design failed).
14 KiB
ADR-0128 — en_numerics_v1 Pack
Status: Proposed (scope-only; sibling to ADR-0127) Date: 2026-05-23 Author: CORE agents + reviewers Depends on:
- ADR-0126 (candidate-graph parser + round-trip filter)
- ADR-0127 (
en_units_v1— units pack, cross-referenced for shared symbol/affix table) Supersedes: none Sibling: ADR-0127. Both packs are jointly required for the units-aware candidate-graph parser to produce a fair empirical result on the GSM8K-math lane. Either pack failing ratification independently blocks the joint exit gate.
Context
ADR-0127 introduces en_units_v1 (dimensions, units, conversions,
containers, rate connectors). That pack handles the physical /
economic substrate. It does NOT handle the linguistic forms a
quantity can take in English math word problems:
- Cardinal number words ("seventeen", "two hundred and fifty")
- Ordinal words ("third", "twentieth")
- Fractions as words ("two-thirds", "a quarter") and as symbols
(
½,¾) - Multiplier words ("twice", "triple", "half")
- Quantifiers ("all", "some", "each", "every", "many", "few")
- Comparison anchors ("more", "fewer", "less", "additional")
- Numeric format strings ("1,000", "1.5", "1/2", "75%")
Today these are scattered:
WORD_NUMBERStable hard-coded ingenerate/math_roundtrip.py(one through twelve only)_COMPARE_VERB/ comparison anchors hard-coded ingenerate/math_parser.py- Fraction handling absent
- Percentage handling absent
- Multi-digit number-word parsing ("two hundred") absent
The GSM8K train sample shows the cost: e.g., "Half of the kids
are going to soccer camp" (refused — half not handled),
"3/4 of its temperature" (refused — 3/4 not recognized as
fractional form). These are unrecognized quantity forms, not
unrecognized units; they sit on a different lexical axis from
ADR-0127's substrate.
A units pack without a numerics pack solves "5 feet + 8 inches" but still refuses "half a foot." Both packs are needed for the architecture to get a fair empirical reading on the train sample.
Decision
Add a language_packs/data/en_numerics_v1/ ratified semantic pack
that exhaustively encodes the English linguistic forms of
quantities. Parser changes are minimal — most pack content
replaces today's scattered hard-coded tables with ratified
lookups.
Pack content (en_numerics_v1) — EXHAUSTIVE SCOPE
Structure mirrors en_arithmetic_v1 / en_units_v1:
lexicon.jsonl + manifest.json + glosses.jsonl +
.mastery_report.json.
Cardinal number words (entry_id prefix en-num-card-):
EXHAUSTIVE for grade-school range:
zerothroughtwenty(21 entries)- Tens:
thirty,forty,fifty,sixty,seventy,eighty,ninety(7 entries) - Compound rule:
<tens-word>-<unit-word>(e.g., "twenty-one", "ninety-nine") — structural composition, not enumerated hundred,thousand,million,billion(4 entries)- Compound rule:
<N> hundred [and <M>],<N> thousand [<conjunction> <M>]— structural composition
Each entry carries surface, numeric_value (int), morphology
(cardinal).
Ordinal number words (entry_id prefix en-num-ord-):
EXHAUSTIVE for grade-school range (1st–31st covers most calendar / position references in word problems):
firstthroughtenth(10 entries — irregular morphology)- Suffix rule:
<cardinal>thfor 11–31 (eleventh, twelfth, thirteenth, …, thirty-first) — structural composition with spelling-irregularity table (fifthnotfiveth,eighthnoteightth,twelfthnottwelveth, etc.) twentieth,thirtieth,hundredth,thousandth(4 entries)
Each entry carries surface, position (int), morphology
(ordinal).
Fraction words (entry_id prefix en-num-frac-):
- Named fractions:
half(½),third(⅓),quarter(¼),fifth(⅕),sixth(⅙),seventh(⅐),eighth(⅛),ninth(⅑),tenth(⅒),sixteenth(¹/₁₆) — ~10 entries - Compound rule:
<cardinal>/<ordinal-as-denominator>(e.g., "two-thirds", "three-quarters") — structural composition - Article-bound:
a half,a quarter,a thirdresolve to the same numeric value as the bare form - Symbol cross-link to
en_units_v1symbol table for½,¼,¾,⅓,⅔,⅛,⅜,⅝,⅞
Each entry carries surface, numerator (int), denominator
(int), decimal_value (float — 1/3 = 0.333… etc.),
morphology (fraction).
Multiplier words (entry_id prefix en-num-mult-) — closed
set:
double (×2), triple (×3), quadruple (×4), quintuple (×5),
twice (×2), thrice (×3), half (×0.5 — also a fraction
word; both entries cross-reference). Plus structural rule
N times for arbitrary integer multipliers.
Quantifiers (entry_id prefix en-num-quant-) — closed set:
all, none, some, both, each, every, many, few,
several, most, any, no, single.
Each declares its semantic_type: total, empty, partial,
paired, distributive, indefinite. The parser uses this to
decide whether the quantifier yields a determinate value
(both = 2, single = 1) or is undeterminate and triggers
refusal (some, many, few — refuse rather than guess; this
preserves wrong == 0).
Comparison anchors (entry_id prefix en-num-compare-):
Migrated from generate/math_roundtrip.py's hard-coded
COMPARE_ADDITIVE_ANCHORS / COMPARE_MULTIPLICATIVE_ANCHORS.
- Additive:
more,fewer,less,additional,extra,missing,remaining - Multiplicative:
twice,thrice,times,half,double,triple,quadruple,third,quarter
Each cross-references its multiplier or fraction entry where applicable (avoiding duplicate truth).
Number formats (entry_id prefix en-num-format-) —
structural rules, not enumeration:
- Digit groups:
1,000(US thousand separator),10,000,1,000,000—(\d{1,3})(?:,\d{3})+→ strip commas, parse as int - Decimals:
1.5,3.14,0.25—\d+\.\d+→ parse as float - Slash-fractions:
1/2,3/4,7/8—(\d+)/(\d+)→ parse asFraction - Mixed numbers:
1 1/2,2 3/4—(\d+) (\d+)/(\d+)→whole + numerator/denominator - Percentages:
75%,1.5%—\d+(?:\.\d+)?%→ divide by 100 - Negative numbers:
-3,-0.5— leading minus (rarely in grade-school but cheap to include)
Each format rule declares its regex pattern, parser_function
(by name), and output_type (int, float, Fraction).
Cross-references with en_units_v1
- Fraction symbols
½,¼,¾, etc. appear in both packs. Single source of truth lives inen_numerics_v1;en_units_v1symbol-affix table contains a reference entry pointing to the numerics pack viacross_pack_id. Ratification cross-pack consistency check (see ADR-0127 ratification invariants and below) verifies the references resolve. - Percentage (
%) cross-references between packs because it's both a numeric format and a dimensionless modifier. - Multipliers (
double,twice) cross-reference because they also appear as comparison anchors inen_units_v1's parser- consumed register.
Ratification invariants (en_numerics_v1-specific)
- Cardinal exhaustiveness — every English cardinal 0..20, every "tens" form, every magnitude word (hundred/thousand/million) present.
- Ordinal exhaustiveness — every English ordinal 1st..31st present (covers month days + most grade-school position references).
- Fraction exhaustiveness — every named fraction 1/2 through 1/10 present + the irregular set (sixteenth, thirty-second).
- Cross-pack consistency — every fraction-symbol entry in
en_units_v1resolves to a fraction entry inen_numerics_v1. Verified by joint pack-mount ratification. - Quantifier semantic-type completeness — every quantifier
lemma carries a
semantic_typefrom the closed set{total, empty, partial, paired, distributive, indefinite}. - Format regex test corpus — each format rule has a
minimum of 10 positive + 10 negative test strings in
tests/test_adr_0128_numeric_formats.py. Format ambiguity (e.g.,1.000could be1or1000) is refused perwrong == 0, not guessed.
Parser integration
-
generate/math_roundtrip.py— replace hard-codedWORD_NUMBERSandCOMPARE_*_ANCHORStables with calls intoen_numerics_v1loader. Behavior preserved (current entries are a subset); future extensions land in the pack, not the source. -
generate/math_candidate_parser.py— new value-token normalization helpernormalize_value_token(token) -> Quantity | Fraction | Nonethat consultsen_numerics_v1format rules. Handles "two-thirds" →Fraction(2,3),75%→0.75,1,500→1500, etc. -
Round-trip filter (
roundtrip_admissible) — the existing_value_groundshelper is extended to ground word-forms via the numerics pack loader (current hard-codedWORD_NUMBERScapped at "twelve" widens to the full cardinal table). -
Quantifier-driven refusal — when a candidate's value-token resolves to an
indefinitequantifier (some,many,few), the parser emits NO candidate for that sentence. Empty list → refusal at decision rule. Preserveswrong == 0.
What ADR-0128 explicitly does NOT do
- Does NOT replace
en_units_v1. The packs are siblings. - Does NOT introduce new operation kinds. ADR-0126 + 0122 cover the operation grammar; this pack supplies value-token forms.
- Does NOT model number-system alternates (Roman numerals, Chinese numerals, etc.) — out of scope for English math problems.
- Does NOT model implicit numbers ("a dozen" / "a few") as
determinate values —
a dozen = 12is encoded via the container'sdefault_size(ADR-0127's container entry, not here);a fewisindefiniteand triggers refusal. - Does NOT add new exit gates. Joint exit criterion with ADR-0127 (re-run train sample with both packs mounted).
Invariants preserved / added
| Invariant | Preserved or added | How |
|---|---|---|
wrong == 0 |
Preserved | Indefinite quantifiers trigger refusal; format ambiguity triggers refusal |
trace_hash byte-equality |
Preserved | Pack lookup is deterministic |
| Pack-binding (ADR-0114a #10) | Reinforced | Value-token resolution cites en-num-* entry_ids in SolutionTrace.provenance |
| Round-trip admissibility | Strengthened | Word-form numbers ground via pack lookup, not regex enumeration |
| Replay equivalence | Preserved | Same pack version → same lookup result |
versor_condition(F) < 1e-6 |
Untouched | No runtime field changes |
| Manifest checksum SHA-256 of bytes-on-disk | Required | Mastery report self-seals |
Exit criterion
Joint with ADR-0127 — both packs ratified, both mounted, re-run train sample:
correct >= 10 / 50 on evals/gsm8k_math/train_sample/v1/cases.jsonl
wrong == 0
If passed: run sealed holdout once, freeze in ADR-0127/0128 joint results.
If missed: Path-B trigger — full deterministic design (candidate-graph + units + numerics) failed to move GSM8K. Demote benchmark, re-target math expert promotion.
Alternatives considered
A. Fold numerics into en_units_v1 as one big pack.
Rejected per ADR-0127 discussion: domain-distinct lexicons, risk-isolation, future i18n composability.
B. Keep hard-coded tables in math_roundtrip.py / math_parser.py.
Rejected — violates pack-binding (ADR-0114a #10); future language packs (Spanish, German) would have to duplicate the hard-coded tables; ratification is impossible.
C. Use a third-party number-parsing library (word2number,
num2words, etc.). Rejected — same opacity / non-auditability
critique as ADR-0127 alt B.
D. Ship only cardinals + fractions; defer quantifiers /
ordinals / formats. Rejected — quantifiers (half, some) are
specifically what the GSM8K train sample refuses on. Partial
pack delivers partial empirical signal — same trap ADR-0127
addresses re: scope mismatch.
Implementation plan (proposed sub-phases)
| Phase | Module | Description |
|---|---|---|
| 0128.1 | language_packs/data/en_numerics_v1/ |
Pack content (lexicon + manifest + glosses + mastery report) |
| 0128.2 | language_packs/loader.py |
lookup_cardinal, lookup_ordinal, lookup_fraction, lookup_quantifier, match_number_format |
| 0128.3 | generate/math_roundtrip.py |
Replace hard-coded WORD_NUMBERS + COMPARE_*_ANCHORS with pack-backed lookups |
| 0128.4 | generate/math_candidate_parser.py |
normalize_value_token helper; quantifier-driven refusal |
| 0128.5 | tests/test_adr_0128_*.py |
Pack ratification (exhaustiveness gates) + parser integration + format regex corpus |
| 0128.6 | Joint with ADR-0127.7 | Re-run train sample with both packs mounted |
Regression gates:
core test --suite smoke -qcore test --suite math -qcore test --suite packs -q- ADR-0126 P3+P4 tests
- ADR-0127 pack ratification
PR checklist (when proposing for merge)
What capability did this add?
→ Exhaustive English linguistic-form ontology for quantities;
sibling substrate to en_units_v1 (ADR-0127).
What invariant proves the field remains valid?
→ Wrong==0 preserved via indefinite-quantifier refusal +
format-ambiguity refusal; cardinal/ordinal/fraction
exhaustiveness ratification gates.
Which CLI suite/eval proves the lane?
→ smoke + math + packs + joint train_sample re-run with
ADR-0127.
Did this avoid hidden normalization, stochastic fallback,
approximate recall, unreviewed mutation?
→ Yes. Pack lookup is deterministic; lexicon is bounded and
ratified; no learned tokenization; no fuzzy matching.
If it touches user input, what trust boundary was enforced?
→ Pack file paths validated via safe_pack_id (ADR-0051).
Format regexes carry test corpora documenting accepted vs
rejected inputs; user input is matched against ratified
patterns only.