core/docs/decisions/ADR-0128-numerics-pack.md
Shay 6e7766f66e docs(ADR-0127+0128): exhaustive units pack + sibling numerics pack
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).
2026-05-23 06:48:22 -07:00

338 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_NUMBERS` table hard-coded in `generate/math_roundtrip.py`
(one through twelve only)
- `_COMPARE_VERB` / comparison anchors hard-coded in
`generate/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:
- `zero` through `twenty` (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 (1st31st covers most
calendar / position references in word problems):
- `first` through `tenth` (10 entries — irregular morphology)
- Suffix rule: `<cardinal>th` for 1131 (eleventh, twelfth,
thirteenth, …, thirty-first) — structural composition with
spelling-irregularity table (`fifth` not `fiveth`, `eighth` not
`eightth`, `twelfth` not `twelveth`, 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 third` resolve to
the same numeric value as the bare form
- **Symbol cross-link** to `en_units_v1` symbol 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 as `Fraction`
- 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 in `en_numerics_v1`;
`en_units_v1` symbol-affix table contains a *reference* entry
pointing to the numerics pack via `cross_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 in `en_units_v1`'s parser-
consumed register.
### Ratification invariants (`en_numerics_v1`-specific)
1. **Cardinal exhaustiveness** — every English cardinal 0..20,
every "tens" form, every magnitude word
(hundred/thousand/million) present.
2. **Ordinal exhaustiveness** — every English ordinal 1st..31st
present (covers month days + most grade-school position
references).
3. **Fraction exhaustiveness** — every named fraction 1/2 through
1/10 present + the irregular set (sixteenth, thirty-second).
4. **Cross-pack consistency** — every fraction-symbol entry in
`en_units_v1` resolves to a fraction entry in
`en_numerics_v1`. Verified by joint pack-mount ratification.
5. **Quantifier semantic-type completeness** — every quantifier
lemma carries a `semantic_type` from the closed set
`{total, empty, partial, paired, distributive, indefinite}`.
6. **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.000` could be `1` or `1000`) is *refused* per
`wrong == 0`, not guessed.
### Parser integration
1. **`generate/math_roundtrip.py`** — replace hard-coded
`WORD_NUMBERS` and `COMPARE_*_ANCHORS` tables with calls into
`en_numerics_v1` loader. Behavior preserved (current entries
are a subset); future extensions land in the pack, not the
source.
2. **`generate/math_candidate_parser.py`** — new value-token
normalization helper `normalize_value_token(token) -> Quantity
| Fraction | None` that consults `en_numerics_v1` format
rules. Handles "two-thirds" → `Fraction(2,3)`, `75%``0.75`,
`1,500``1500`, etc.
3. **Round-trip filter (`roundtrip_admissible`)** — the existing
`_value_grounds` helper is extended to ground word-forms via
the numerics pack loader (current hard-coded `WORD_NUMBERS`
capped at "twelve" widens to the full cardinal table).
4. **Quantifier-driven refusal** — when a candidate's value-token
resolves to an `indefinite` quantifier (`some`, `many`, `few`),
the parser emits NO candidate for that sentence. Empty list →
refusal at decision rule. Preserves `wrong == 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 = 12` is encoded via the
container's `default_size` (ADR-0127's container entry, not
here); `a few` is `indefinite` and 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 -q`
- `core test --suite math -q`
- `core 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.
```