Merge pull request #163 from AssetOverflow/opus2/adr-0128-en-numerics-v1

feat(packs): ADR-0128.1+0128.2 — en_numerics_v1 pack + loader
This commit is contained in:
Shay 2026-05-23 07:09:37 -07:00 committed by GitHub
commit 46c734b7aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1851 additions and 0 deletions

View file

@ -0,0 +1,29 @@
{
"pack_id": "en_numerics_v1",
"version": "1.0.0",
"provenance": "adr-0128:operator_seed:2026-05-23",
"lexicon_sha256": "a5bd05ae975491c941a3e4951d785937f3a3bb228da816152259462447b612fc",
"glosses_sha256": "0a0abfc0401352080fd285090106219ea240b0d0762d19db3bd557b2c537d767",
"manifest_sha256": "b02b0991128ce605caf6a1c87c5b06dc81e773f8509d624bd7af6aee2a505544",
"entry_counts": {
"cardinal": 32,
"ordinal": 40,
"fraction": 16,
"multiplier": 7,
"quantifier": 13,
"comparison_anchor": 16,
"number_format": 6
},
"total_entries": 130,
"ratified_at": "2026-05-23",
"ratification_invariants": [
"cardinal_exhaustive_0_to_20",
"cardinal_tens_complete",
"cardinal_magnitudes_complete",
"ordinal_exhaustive_1_to_31",
"fraction_named_1_2_through_1_10",
"fraction_irregular_sixteenth_thirty_second",
"quantifier_semantic_type_closed_set",
"number_format_regex_corpus_gated"
]
}

View file

@ -0,0 +1,130 @@
{"entry_id":"en-num-card-000","gloss":"Cardinal number word for the integer 0."}
{"entry_id":"en-num-card-001","gloss":"Cardinal number word for the integer 1."}
{"entry_id":"en-num-card-002","gloss":"Cardinal number word for the integer 2."}
{"entry_id":"en-num-card-003","gloss":"Cardinal number word for the integer 3."}
{"entry_id":"en-num-card-004","gloss":"Cardinal number word for the integer 4."}
{"entry_id":"en-num-card-005","gloss":"Cardinal number word for the integer 5."}
{"entry_id":"en-num-card-006","gloss":"Cardinal number word for the integer 6."}
{"entry_id":"en-num-card-007","gloss":"Cardinal number word for the integer 7."}
{"entry_id":"en-num-card-008","gloss":"Cardinal number word for the integer 8."}
{"entry_id":"en-num-card-009","gloss":"Cardinal number word for the integer 9."}
{"entry_id":"en-num-card-010","gloss":"Cardinal number word for the integer 10."}
{"entry_id":"en-num-card-011","gloss":"Cardinal number word for the integer 11."}
{"entry_id":"en-num-card-012","gloss":"Cardinal number word for the integer 12."}
{"entry_id":"en-num-card-013","gloss":"Cardinal number word for the integer 13."}
{"entry_id":"en-num-card-014","gloss":"Cardinal number word for the integer 14."}
{"entry_id":"en-num-card-015","gloss":"Cardinal number word for the integer 15."}
{"entry_id":"en-num-card-016","gloss":"Cardinal number word for the integer 16."}
{"entry_id":"en-num-card-017","gloss":"Cardinal number word for the integer 17."}
{"entry_id":"en-num-card-018","gloss":"Cardinal number word for the integer 18."}
{"entry_id":"en-num-card-019","gloss":"Cardinal number word for the integer 19."}
{"entry_id":"en-num-card-020","gloss":"Cardinal number word for the integer 20."}
{"entry_id":"en-num-card-021","gloss":"Cardinal tens word for the integer 30."}
{"entry_id":"en-num-card-022","gloss":"Cardinal tens word for the integer 40."}
{"entry_id":"en-num-card-023","gloss":"Cardinal tens word for the integer 50."}
{"entry_id":"en-num-card-024","gloss":"Cardinal tens word for the integer 60."}
{"entry_id":"en-num-card-025","gloss":"Cardinal tens word for the integer 70."}
{"entry_id":"en-num-card-026","gloss":"Cardinal tens word for the integer 80."}
{"entry_id":"en-num-card-027","gloss":"Cardinal tens word for the integer 90."}
{"entry_id":"en-num-card-028","gloss":"Cardinal magnitude word for the integer 100."}
{"entry_id":"en-num-card-029","gloss":"Cardinal magnitude word for the integer 1000."}
{"entry_id":"en-num-card-030","gloss":"Cardinal magnitude word for the integer 1000000."}
{"entry_id":"en-num-card-031","gloss":"Cardinal magnitude word for the integer 1000000000."}
{"entry_id":"en-num-ord-001","gloss":"Ordinal number word for position 1."}
{"entry_id":"en-num-ord-002","gloss":"Ordinal number word for position 2."}
{"entry_id":"en-num-ord-003","gloss":"Ordinal number word for position 3."}
{"entry_id":"en-num-ord-004","gloss":"Ordinal number word for position 4."}
{"entry_id":"en-num-ord-005","gloss":"Ordinal number word for position 5."}
{"entry_id":"en-num-ord-006","gloss":"Ordinal number word for position 6."}
{"entry_id":"en-num-ord-007","gloss":"Ordinal number word for position 7."}
{"entry_id":"en-num-ord-008","gloss":"Ordinal number word for position 8."}
{"entry_id":"en-num-ord-009","gloss":"Ordinal number word for position 9."}
{"entry_id":"en-num-ord-010","gloss":"Ordinal number word for position 10."}
{"entry_id":"en-num-ord-011","gloss":"Ordinal number word for position 11."}
{"entry_id":"en-num-ord-012","gloss":"Ordinal number word for position 12."}
{"entry_id":"en-num-ord-013","gloss":"Ordinal number word for position 13."}
{"entry_id":"en-num-ord-014","gloss":"Ordinal number word for position 14."}
{"entry_id":"en-num-ord-015","gloss":"Ordinal number word for position 15."}
{"entry_id":"en-num-ord-016","gloss":"Ordinal number word for position 16."}
{"entry_id":"en-num-ord-017","gloss":"Ordinal number word for position 17."}
{"entry_id":"en-num-ord-018","gloss":"Ordinal number word for position 18."}
{"entry_id":"en-num-ord-019","gloss":"Ordinal number word for position 19."}
{"entry_id":"en-num-ord-020","gloss":"Ordinal number word for position 20."}
{"entry_id":"en-num-ord-021","gloss":"Ordinal number word for position 21."}
{"entry_id":"en-num-ord-022","gloss":"Ordinal number word for position 22."}
{"entry_id":"en-num-ord-023","gloss":"Ordinal number word for position 23."}
{"entry_id":"en-num-ord-024","gloss":"Ordinal number word for position 24."}
{"entry_id":"en-num-ord-025","gloss":"Ordinal number word for position 25."}
{"entry_id":"en-num-ord-026","gloss":"Ordinal number word for position 26."}
{"entry_id":"en-num-ord-027","gloss":"Ordinal number word for position 27."}
{"entry_id":"en-num-ord-028","gloss":"Ordinal number word for position 28."}
{"entry_id":"en-num-ord-029","gloss":"Ordinal number word for position 29."}
{"entry_id":"en-num-ord-030","gloss":"Ordinal number word for position 30."}
{"entry_id":"en-num-ord-031","gloss":"Ordinal number word for position 31."}
{"entry_id":"en-num-ord-032","gloss":"Ordinal number word for position 40."}
{"entry_id":"en-num-ord-033","gloss":"Ordinal number word for position 50."}
{"entry_id":"en-num-ord-034","gloss":"Ordinal number word for position 60."}
{"entry_id":"en-num-ord-035","gloss":"Ordinal number word for position 70."}
{"entry_id":"en-num-ord-036","gloss":"Ordinal number word for position 80."}
{"entry_id":"en-num-ord-037","gloss":"Ordinal number word for position 90."}
{"entry_id":"en-num-ord-038","gloss":"Ordinal number word for position 100."}
{"entry_id":"en-num-ord-039","gloss":"Ordinal number word for position 1000."}
{"entry_id":"en-num-ord-040","gloss":"Ordinal number word for position 1000000."}
{"entry_id":"en-num-frac-001","gloss":"Named fraction word for 1/2."}
{"entry_id":"en-num-frac-002","gloss":"Named fraction word for 1/3."}
{"entry_id":"en-num-frac-003","gloss":"Named fraction word for 1/4."}
{"entry_id":"en-num-frac-004","gloss":"Named fraction word for 1/5."}
{"entry_id":"en-num-frac-005","gloss":"Named fraction word for 1/6."}
{"entry_id":"en-num-frac-006","gloss":"Named fraction word for 1/7."}
{"entry_id":"en-num-frac-007","gloss":"Named fraction word for 1/8."}
{"entry_id":"en-num-frac-008","gloss":"Named fraction word for 1/9."}
{"entry_id":"en-num-frac-009","gloss":"Named fraction word for 1/10."}
{"entry_id":"en-num-frac-010","gloss":"Named fraction word for 1/16."}
{"entry_id":"en-num-frac-011","gloss":"Named fraction word for 1/32."}
{"entry_id":"en-num-frac-012","gloss":"Unicode fraction symbol for 2/3."}
{"entry_id":"en-num-frac-013","gloss":"Unicode fraction symbol for 3/4."}
{"entry_id":"en-num-frac-014","gloss":"Unicode fraction symbol for 3/8."}
{"entry_id":"en-num-frac-015","gloss":"Unicode fraction symbol for 5/8."}
{"entry_id":"en-num-frac-016","gloss":"Unicode fraction symbol for 7/8."}
{"entry_id":"en-num-mult-001","gloss":"Multiplier word scaling a quantity by 2.0."}
{"entry_id":"en-num-mult-002","gloss":"Multiplier word scaling a quantity by 3.0."}
{"entry_id":"en-num-mult-003","gloss":"Multiplier word scaling a quantity by 4.0."}
{"entry_id":"en-num-mult-004","gloss":"Multiplier word scaling a quantity by 5.0."}
{"entry_id":"en-num-mult-005","gloss":"Multiplier word scaling a quantity by 2.0."}
{"entry_id":"en-num-mult-006","gloss":"Multiplier word scaling a quantity by 3.0."}
{"entry_id":"en-num-mult-007","gloss":"Multiplier word scaling a quantity by 0.5."}
{"entry_id":"en-num-quant-001","gloss":"Quantifier (total). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-002","gloss":"Quantifier (empty). Determinate value 0."}
{"entry_id":"en-num-quant-003","gloss":"Quantifier (indefinite). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-004","gloss":"Quantifier (paired). Determinate value 2."}
{"entry_id":"en-num-quant-005","gloss":"Quantifier (distributive). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-006","gloss":"Quantifier (distributive). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-007","gloss":"Quantifier (indefinite). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-008","gloss":"Quantifier (indefinite). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-009","gloss":"Quantifier (indefinite). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-010","gloss":"Quantifier (partial). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-011","gloss":"Quantifier (indefinite). No determinate value; indefinite quantifiers trigger refusal."}
{"entry_id":"en-num-quant-012","gloss":"Quantifier (empty). Determinate value 0."}
{"entry_id":"en-num-quant-013","gloss":"Quantifier (total). Determinate value 1."}
{"entry_id":"en-num-compare-add-001","gloss":"Additive comparison anchor 'more'."}
{"entry_id":"en-num-compare-add-002","gloss":"Additive comparison anchor 'fewer'."}
{"entry_id":"en-num-compare-add-003","gloss":"Additive comparison anchor 'less'."}
{"entry_id":"en-num-compare-add-004","gloss":"Additive comparison anchor 'additional'."}
{"entry_id":"en-num-compare-add-005","gloss":"Additive comparison anchor 'extra'."}
{"entry_id":"en-num-compare-add-006","gloss":"Additive comparison anchor 'missing'."}
{"entry_id":"en-num-compare-add-007","gloss":"Additive comparison anchor 'remaining'."}
{"entry_id":"en-num-compare-mul-001","gloss":"Multiplicative comparison anchor 'twice'."}
{"entry_id":"en-num-compare-mul-002","gloss":"Multiplicative comparison anchor 'thrice'."}
{"entry_id":"en-num-compare-mul-003","gloss":"Multiplicative comparison anchor 'times'."}
{"entry_id":"en-num-compare-mul-004","gloss":"Multiplicative comparison anchor 'half'."}
{"entry_id":"en-num-compare-mul-005","gloss":"Multiplicative comparison anchor 'double'."}
{"entry_id":"en-num-compare-mul-006","gloss":"Multiplicative comparison anchor 'triple'."}
{"entry_id":"en-num-compare-mul-007","gloss":"Multiplicative comparison anchor 'quadruple'."}
{"entry_id":"en-num-compare-mul-008","gloss":"Multiplicative comparison anchor 'third'."}
{"entry_id":"en-num-compare-mul-009","gloss":"Multiplicative comparison anchor 'quarter'."}
{"entry_id":"en-num-format-001","gloss":"Number-format rule 'thousand_separated' (regex emits int)."}
{"entry_id":"en-num-format-002","gloss":"Number-format rule 'decimal' (regex emits float)."}
{"entry_id":"en-num-format-003","gloss":"Number-format rule 'slash_fraction' (regex emits Fraction)."}
{"entry_id":"en-num-format-004","gloss":"Number-format rule 'mixed_number' (regex emits Fraction)."}
{"entry_id":"en-num-format-005","gloss":"Number-format rule 'percentage' (regex emits float)."}
{"entry_id":"en-num-format-006","gloss":"Number-format rule 'signed_integer' (regex emits int)."}

View file

@ -0,0 +1,130 @@
{"entry_id":"en-num-card-000","surface":"zero","lemma":"zero","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":0,"morphology":"cardinal"}
{"entry_id":"en-num-card-001","surface":"one","lemma":"one","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":1,"morphology":"cardinal"}
{"entry_id":"en-num-card-002","surface":"two","lemma":"two","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":2,"morphology":"cardinal"}
{"entry_id":"en-num-card-003","surface":"three","lemma":"three","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":3,"morphology":"cardinal"}
{"entry_id":"en-num-card-004","surface":"four","lemma":"four","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":4,"morphology":"cardinal"}
{"entry_id":"en-num-card-005","surface":"five","lemma":"five","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":5,"morphology":"cardinal"}
{"entry_id":"en-num-card-006","surface":"six","lemma":"six","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":6,"morphology":"cardinal"}
{"entry_id":"en-num-card-007","surface":"seven","lemma":"seven","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":7,"morphology":"cardinal"}
{"entry_id":"en-num-card-008","surface":"eight","lemma":"eight","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":8,"morphology":"cardinal"}
{"entry_id":"en-num-card-009","surface":"nine","lemma":"nine","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":9,"morphology":"cardinal"}
{"entry_id":"en-num-card-010","surface":"ten","lemma":"ten","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":10,"morphology":"cardinal"}
{"entry_id":"en-num-card-011","surface":"eleven","lemma":"eleven","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":11,"morphology":"cardinal"}
{"entry_id":"en-num-card-012","surface":"twelve","lemma":"twelve","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":12,"morphology":"cardinal"}
{"entry_id":"en-num-card-013","surface":"thirteen","lemma":"thirteen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":13,"morphology":"cardinal"}
{"entry_id":"en-num-card-014","surface":"fourteen","lemma":"fourteen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":14,"morphology":"cardinal"}
{"entry_id":"en-num-card-015","surface":"fifteen","lemma":"fifteen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":15,"morphology":"cardinal"}
{"entry_id":"en-num-card-016","surface":"sixteen","lemma":"sixteen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":16,"morphology":"cardinal"}
{"entry_id":"en-num-card-017","surface":"seventeen","lemma":"seventeen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":17,"morphology":"cardinal"}
{"entry_id":"en-num-card-018","surface":"eighteen","lemma":"eighteen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":18,"morphology":"cardinal"}
{"entry_id":"en-num-card-019","surface":"nineteen","lemma":"nineteen","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","unit"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":19,"morphology":"cardinal"}
{"entry_id":"en-num-card-020","surface":"twenty","lemma":"twenty","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":20,"morphology":"cardinal"}
{"entry_id":"en-num-card-021","surface":"thirty","lemma":"thirty","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":30,"morphology":"cardinal"}
{"entry_id":"en-num-card-022","surface":"forty","lemma":"forty","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":40,"morphology":"cardinal"}
{"entry_id":"en-num-card-023","surface":"fifty","lemma":"fifty","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":50,"morphology":"cardinal"}
{"entry_id":"en-num-card-024","surface":"sixty","lemma":"sixty","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":60,"morphology":"cardinal"}
{"entry_id":"en-num-card-025","surface":"seventy","lemma":"seventy","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":70,"morphology":"cardinal"}
{"entry_id":"en-num-card-026","surface":"eighty","lemma":"eighty","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":80,"morphology":"cardinal"}
{"entry_id":"en-num-card-027","surface":"ninety","lemma":"ninety","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.integer"],"morphology_tags":["cardinal","decade-anchor"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":90,"morphology":"cardinal"}
{"entry_id":"en-num-card-028","surface":"hundred","lemma":"hundred","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.magnitude"],"morphology_tags":["cardinal","magnitude"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":100,"morphology":"cardinal-magnitude"}
{"entry_id":"en-num-card-029","surface":"thousand","lemma":"thousand","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.magnitude"],"morphology_tags":["cardinal","magnitude"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":1000,"morphology":"cardinal-magnitude"}
{"entry_id":"en-num-card-030","surface":"million","lemma":"million","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.magnitude"],"morphology_tags":["cardinal","magnitude"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":1000000,"morphology":"cardinal-magnitude"}
{"entry_id":"en-num-card-031","surface":"billion","lemma":"billion","language":"en","pos":"NUM","semantic_domains":["numerics.cardinal","mathematics.value.magnitude"],"morphology_tags":["cardinal","magnitude"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numeric_value":1000000000,"morphology":"cardinal-magnitude"}
{"entry_id":"en-num-ord-001","surface":"first","lemma":"first","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":1,"morphology":"ordinal"}
{"entry_id":"en-num-ord-002","surface":"second","lemma":"second","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":2,"morphology":"ordinal"}
{"entry_id":"en-num-ord-003","surface":"third","lemma":"third","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":3,"morphology":"ordinal"}
{"entry_id":"en-num-ord-004","surface":"fourth","lemma":"fourth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":4,"morphology":"ordinal"}
{"entry_id":"en-num-ord-005","surface":"fifth","lemma":"fifth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":5,"morphology":"ordinal"}
{"entry_id":"en-num-ord-006","surface":"sixth","lemma":"sixth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":6,"morphology":"ordinal"}
{"entry_id":"en-num-ord-007","surface":"seventh","lemma":"seventh","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":7,"morphology":"ordinal"}
{"entry_id":"en-num-ord-008","surface":"eighth","lemma":"eighth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":8,"morphology":"ordinal"}
{"entry_id":"en-num-ord-009","surface":"ninth","lemma":"ninth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":9,"morphology":"ordinal"}
{"entry_id":"en-num-ord-010","surface":"tenth","lemma":"tenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":10,"morphology":"ordinal"}
{"entry_id":"en-num-ord-011","surface":"eleventh","lemma":"eleventh","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":11,"morphology":"ordinal"}
{"entry_id":"en-num-ord-012","surface":"twelfth","lemma":"twelfth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":12,"morphology":"ordinal"}
{"entry_id":"en-num-ord-013","surface":"thirteenth","lemma":"thirteenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":13,"morphology":"ordinal"}
{"entry_id":"en-num-ord-014","surface":"fourteenth","lemma":"fourteenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":14,"morphology":"ordinal"}
{"entry_id":"en-num-ord-015","surface":"fifteenth","lemma":"fifteenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":15,"morphology":"ordinal"}
{"entry_id":"en-num-ord-016","surface":"sixteenth","lemma":"sixteenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":16,"morphology":"ordinal"}
{"entry_id":"en-num-ord-017","surface":"seventeenth","lemma":"seventeenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":17,"morphology":"ordinal"}
{"entry_id":"en-num-ord-018","surface":"eighteenth","lemma":"eighteenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":18,"morphology":"ordinal"}
{"entry_id":"en-num-ord-019","surface":"nineteenth","lemma":"nineteenth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":19,"morphology":"ordinal"}
{"entry_id":"en-num-ord-020","surface":"twentieth","lemma":"twentieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":20,"morphology":"ordinal"}
{"entry_id":"en-num-ord-021","surface":"twenty-first","lemma":"twenty-first","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":21,"morphology":"ordinal"}
{"entry_id":"en-num-ord-022","surface":"twenty-second","lemma":"twenty-second","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":22,"morphology":"ordinal"}
{"entry_id":"en-num-ord-023","surface":"twenty-third","lemma":"twenty-third","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":23,"morphology":"ordinal"}
{"entry_id":"en-num-ord-024","surface":"twenty-fourth","lemma":"twenty-fourth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":24,"morphology":"ordinal"}
{"entry_id":"en-num-ord-025","surface":"twenty-fifth","lemma":"twenty-fifth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":25,"morphology":"ordinal"}
{"entry_id":"en-num-ord-026","surface":"twenty-sixth","lemma":"twenty-sixth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":26,"morphology":"ordinal"}
{"entry_id":"en-num-ord-027","surface":"twenty-seventh","lemma":"twenty-seventh","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":27,"morphology":"ordinal"}
{"entry_id":"en-num-ord-028","surface":"twenty-eighth","lemma":"twenty-eighth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":28,"morphology":"ordinal"}
{"entry_id":"en-num-ord-029","surface":"twenty-ninth","lemma":"twenty-ninth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":29,"morphology":"ordinal"}
{"entry_id":"en-num-ord-030","surface":"thirtieth","lemma":"thirtieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":30,"morphology":"ordinal"}
{"entry_id":"en-num-ord-031","surface":"thirty-first","lemma":"thirty-first","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":31,"morphology":"ordinal"}
{"entry_id":"en-num-ord-032","surface":"fortieth","lemma":"fortieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":40,"morphology":"ordinal"}
{"entry_id":"en-num-ord-033","surface":"fiftieth","lemma":"fiftieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":50,"morphology":"ordinal"}
{"entry_id":"en-num-ord-034","surface":"sixtieth","lemma":"sixtieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":60,"morphology":"ordinal"}
{"entry_id":"en-num-ord-035","surface":"seventieth","lemma":"seventieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":70,"morphology":"ordinal"}
{"entry_id":"en-num-ord-036","surface":"eightieth","lemma":"eightieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":80,"morphology":"ordinal"}
{"entry_id":"en-num-ord-037","surface":"ninetieth","lemma":"ninetieth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":90,"morphology":"ordinal"}
{"entry_id":"en-num-ord-038","surface":"hundredth","lemma":"hundredth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":100,"morphology":"ordinal"}
{"entry_id":"en-num-ord-039","surface":"thousandth","lemma":"thousandth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":1000,"morphology":"ordinal"}
{"entry_id":"en-num-ord-040","surface":"millionth","lemma":"millionth","language":"en","pos":"ADJ","semantic_domains":["numerics.ordinal","mathematics.value.position"],"morphology_tags":["ordinal"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"position":1000000,"morphology":"ordinal"}
{"entry_id":"en-num-frac-001","surface":"half","lemma":"half","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":2,"decimal_value":0.5,"morphology":"fraction","symbol":"½"}
{"entry_id":"en-num-frac-002","surface":"third","lemma":"third","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":3,"decimal_value":0.3333333333333333,"morphology":"fraction","symbol":"⅓"}
{"entry_id":"en-num-frac-003","surface":"quarter","lemma":"quarter","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":4,"decimal_value":0.25,"morphology":"fraction","symbol":"¼"}
{"entry_id":"en-num-frac-004","surface":"fifth","lemma":"fifth","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":5,"decimal_value":0.2,"morphology":"fraction","symbol":"⅕"}
{"entry_id":"en-num-frac-005","surface":"sixth","lemma":"sixth","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":6,"decimal_value":0.16666666666666666,"morphology":"fraction","symbol":"⅙"}
{"entry_id":"en-num-frac-006","surface":"seventh","lemma":"seventh","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":7,"decimal_value":0.14285714285714285,"morphology":"fraction","symbol":"⅐"}
{"entry_id":"en-num-frac-007","surface":"eighth","lemma":"eighth","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":8,"decimal_value":0.125,"morphology":"fraction","symbol":"⅛"}
{"entry_id":"en-num-frac-008","surface":"ninth","lemma":"ninth","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":9,"decimal_value":0.1111111111111111,"morphology":"fraction","symbol":"⅑"}
{"entry_id":"en-num-frac-009","surface":"tenth","lemma":"tenth","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":10,"decimal_value":0.1,"morphology":"fraction","symbol":"⅒"}
{"entry_id":"en-num-frac-010","surface":"sixteenth","lemma":"sixteenth","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":16,"decimal_value":0.0625,"morphology":"fraction"}
{"entry_id":"en-num-frac-011","surface":"thirty-second","lemma":"thirty-second","language":"en","pos":"NOUN","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","named"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":1,"denominator":32,"decimal_value":0.03125,"morphology":"fraction"}
{"entry_id":"en-num-frac-012","surface":"⅔","lemma":"⅔","language":"en","pos":"SYM","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","symbol"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":2,"denominator":3,"decimal_value":0.6666666666666666,"morphology":"fraction-symbol","spelled_form":"two-thirds"}
{"entry_id":"en-num-frac-013","surface":"¾","lemma":"¾","language":"en","pos":"SYM","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","symbol"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":3,"denominator":4,"decimal_value":0.75,"morphology":"fraction-symbol","spelled_form":"three-quarters"}
{"entry_id":"en-num-frac-014","surface":"⅜","lemma":"⅜","language":"en","pos":"SYM","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","symbol"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":3,"denominator":8,"decimal_value":0.375,"morphology":"fraction-symbol","spelled_form":"three-eighths"}
{"entry_id":"en-num-frac-015","surface":"⅝","lemma":"⅝","language":"en","pos":"SYM","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","symbol"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":5,"denominator":8,"decimal_value":0.625,"morphology":"fraction-symbol","spelled_form":"five-eighths"}
{"entry_id":"en-num-frac-016","surface":"⅞","lemma":"⅞","language":"en","pos":"SYM","semantic_domains":["numerics.fraction","mathematics.value.rational"],"morphology_tags":["fraction","symbol"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"numerator":7,"denominator":8,"decimal_value":0.875,"morphology":"fraction-symbol","spelled_form":"seven-eighths"}
{"entry_id":"en-num-mult-001","surface":"double","lemma":"double","language":"en","pos":"ADJ","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":2.0,"morphology":"multiplier"}
{"entry_id":"en-num-mult-002","surface":"triple","lemma":"triple","language":"en","pos":"ADJ","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":3.0,"morphology":"multiplier"}
{"entry_id":"en-num-mult-003","surface":"quadruple","lemma":"quadruple","language":"en","pos":"ADJ","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":4.0,"morphology":"multiplier"}
{"entry_id":"en-num-mult-004","surface":"quintuple","lemma":"quintuple","language":"en","pos":"ADJ","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":5.0,"morphology":"multiplier"}
{"entry_id":"en-num-mult-005","surface":"twice","lemma":"twice","language":"en","pos":"ADV","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":2.0,"morphology":"multiplier"}
{"entry_id":"en-num-mult-006","surface":"thrice","lemma":"thrice","language":"en","pos":"ADV","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":3.0,"morphology":"multiplier"}
{"entry_id":"en-num-mult-007","surface":"half","lemma":"half","language":"en","pos":"ADJ","semantic_domains":["numerics.multiplier","mathematics.value.scalar"],"morphology_tags":["multiplier"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"factor":0.5,"morphology":"multiplier"}
{"entry_id":"en-num-quant-001","surface":"all","lemma":"all","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","total"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"total","morphology":"quantifier"}
{"entry_id":"en-num-quant-002","surface":"none","lemma":"none","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","empty"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"empty","morphology":"quantifier","determinate_value":0}
{"entry_id":"en-num-quant-003","surface":"some","lemma":"some","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","indefinite"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"indefinite","morphology":"quantifier"}
{"entry_id":"en-num-quant-004","surface":"both","lemma":"both","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","paired"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"paired","morphology":"quantifier","determinate_value":2}
{"entry_id":"en-num-quant-005","surface":"each","lemma":"each","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","distributive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"distributive","morphology":"quantifier"}
{"entry_id":"en-num-quant-006","surface":"every","lemma":"every","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","distributive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"distributive","morphology":"quantifier"}
{"entry_id":"en-num-quant-007","surface":"many","lemma":"many","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","indefinite"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"indefinite","morphology":"quantifier"}
{"entry_id":"en-num-quant-008","surface":"few","lemma":"few","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","indefinite"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"indefinite","morphology":"quantifier"}
{"entry_id":"en-num-quant-009","surface":"several","lemma":"several","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","indefinite"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"indefinite","morphology":"quantifier"}
{"entry_id":"en-num-quant-010","surface":"most","lemma":"most","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","partial"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"partial","morphology":"quantifier"}
{"entry_id":"en-num-quant-011","surface":"any","lemma":"any","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","indefinite"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"indefinite","morphology":"quantifier"}
{"entry_id":"en-num-quant-012","surface":"no","lemma":"no","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","empty"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"empty","morphology":"quantifier","determinate_value":0}
{"entry_id":"en-num-quant-013","surface":"single","lemma":"single","language":"en","pos":"DET","semantic_domains":["numerics.quantifier","mathematics.value.quantifier"],"morphology_tags":["quantifier","total"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"semantic_type":"total","morphology":"quantifier","determinate_value":1}
{"entry_id":"en-num-compare-add-001","surface":"more","lemma":"more","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-add-002","surface":"fewer","lemma":"fewer","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-add-003","surface":"less","lemma":"less","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-add-004","surface":"additional","lemma":"additional","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-add-005","surface":"extra","lemma":"extra","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-add-006","surface":"missing","lemma":"missing","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-add-007","surface":"remaining","lemma":"remaining","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.additive"],"morphology_tags":["comparison-anchor","additive"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"additive","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-001","surface":"twice","lemma":"twice","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-002","surface":"thrice","lemma":"thrice","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-003","surface":"times","lemma":"times","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-004","surface":"half","lemma":"half","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-005","surface":"double","lemma":"double","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-006","surface":"triple","lemma":"triple","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-007","surface":"quadruple","lemma":"quadruple","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-008","surface":"third","lemma":"third","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-compare-mul-009","surface":"quarter","lemma":"quarter","language":"en","pos":"ADJ","semantic_domains":["numerics.compare.multiplicative"],"morphology_tags":["comparison-anchor","multiplicative"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"anchor_class":"multiplicative","morphology":"comparison-anchor"}
{"entry_id":"en-num-format-001","surface":"thousand_separated","lemma":"thousand_separated","language":"en","pos":"X","semantic_domains":["numerics.format"],"morphology_tags":["number-format"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"format_id":"thousand_separated","regex":"^\\d{1,3}(?:,\\d{3})+$","parser_function":"parse_thousand_separated","output_type":"int","morphology":"number-format"}
{"entry_id":"en-num-format-002","surface":"decimal","lemma":"decimal","language":"en","pos":"X","semantic_domains":["numerics.format"],"morphology_tags":["number-format"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"format_id":"decimal","regex":"^-?\\d+\\.\\d+$","parser_function":"parse_decimal","output_type":"float","morphology":"number-format"}
{"entry_id":"en-num-format-003","surface":"slash_fraction","lemma":"slash_fraction","language":"en","pos":"X","semantic_domains":["numerics.format"],"morphology_tags":["number-format"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"format_id":"slash_fraction","regex":"^-?\\d+/\\d+$","parser_function":"parse_slash_fraction","output_type":"Fraction","morphology":"number-format"}
{"entry_id":"en-num-format-004","surface":"mixed_number","lemma":"mixed_number","language":"en","pos":"X","semantic_domains":["numerics.format"],"morphology_tags":["number-format"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"format_id":"mixed_number","regex":"^-?\\d+ \\d+/\\d+$","parser_function":"parse_mixed_number","output_type":"Fraction","morphology":"number-format"}
{"entry_id":"en-num-format-005","surface":"percentage","lemma":"percentage","language":"en","pos":"X","semantic_domains":["numerics.format"],"morphology_tags":["number-format"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"format_id":"percentage","regex":"^-?\\d+(?:\\.\\d+)?%$","parser_function":"parse_percentage","output_type":"float","morphology":"number-format"}
{"entry_id":"en-num-format-006","surface":"signed_integer","lemma":"signed_integer","language":"en","pos":"X","semantic_domains":["numerics.format"],"morphology_tags":["number-format"],"provenance_ids":["adr-0128:operator_seed:2026-05-23"],"format_id":"signed_integer","regex":"^-?\\d+$","parser_function":"parse_signed_integer","output_type":"int","morphology":"number-format"}

View file

@ -0,0 +1,16 @@
{
"pack_id": "en_numerics_v1",
"language": "en",
"role": "operational_base",
"script": "Latin",
"normalization_policy": "unitize_versor",
"source_manifest": "en_numerics_v1.lexicon.jsonl",
"determinism_class": "D0",
"checksum": "a5bd05ae975491c941a3e4951d785937f3a3bb228da816152259462447b612fc",
"version": "1.0.0",
"gate_engaged": true,
"oov_policy": "tagged_fallback",
"glosses_checksum": "0a0abfc0401352080fd285090106219ea240b0d0762d19db3bd557b2c537d767",
"definitional_layer": false,
"provenance": "adr-0128:operator_seed:2026-05-23"
}

View file

@ -0,0 +1,459 @@
"""Loader API for the ``en_numerics_v1`` ratified semantic pack.
Sub-phase ADR-0128.2. Lives in a sibling module to ``loader.py`` to
avoid a merge conflict with the concurrent ADR-0127 ``en_units_v1``
loader work; the two modules will be folded together in a follow-up
PR once both packs ratify.
The public API exposes typed dataclasses for each entry kind plus
lookup / compositional helpers:
lookup_cardinal(token) -> CardinalEntry | None
lookup_ordinal(token) -> OrdinalEntry | None
lookup_fraction(token) -> FractionEntry | None
lookup_quantifier(token) -> QuantifierEntry | None
lookup_multiplier(token) -> MultiplierEntry | None
lookup_comparison_anchor(token) -> ComparisonAnchorEntry | None
match_number_format(token) -> ParsedNumber | None
parse_compound_cardinal(text) -> int | None
Lookups are case-insensitive on surface and stable across calls.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from fractions import Fraction
from functools import lru_cache
from pathlib import Path
from typing import Mapping, Optional, Tuple
PACK_ID = "en_numerics_v1"
PACK_ROOT = Path(__file__).resolve().parent / "data" / PACK_ID
# Closed set of quantifier semantic types (ADR-0128 ratification invariant).
QUANTIFIER_SEMANTIC_TYPES: frozenset[str] = frozenset({
"total", "empty", "partial", "paired", "distributive", "indefinite",
})
# ---------------------------------------------------------------------------
# Entry dataclasses (frozen — immutable per CLAUDE.md immutability rule).
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class CardinalEntry:
entry_id: str
surface: str
numeric_value: int
morphology: str
@dataclass(frozen=True)
class OrdinalEntry:
entry_id: str
surface: str
position: int
morphology: str
@dataclass(frozen=True)
class FractionEntry:
entry_id: str
surface: str
numerator: int
denominator: int
decimal_value: float
morphology: str
symbol: Optional[str] = None
spelled_form: Optional[str] = None
@dataclass(frozen=True)
class MultiplierEntry:
entry_id: str
surface: str
factor: float
morphology: str
@dataclass(frozen=True)
class QuantifierEntry:
entry_id: str
surface: str
semantic_type: str
morphology: str
determinate_value: Optional[int] = None
@property
def is_indefinite(self) -> bool:
return self.semantic_type == "indefinite"
@dataclass(frozen=True)
class ComparisonAnchorEntry:
entry_id: str
surface: str
anchor_class: str # "additive" | "multiplicative"
morphology: str
@dataclass(frozen=True)
class NumberFormatEntry:
entry_id: str
format_id: str
regex: str
parser_function: str
output_type: str
@dataclass(frozen=True)
class ParsedNumber:
"""Result of ``match_number_format`` — carries the parsed value plus
the format rule that matched."""
format_id: str
raw: str
value: object # int | float | Fraction
output_type: str
# ---------------------------------------------------------------------------
# Index — built once and cached for the process lifetime. Pack data is
# immutable on disk; returning the cached mapping is safe.
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class _PackIndex:
cardinals: Mapping[str, CardinalEntry]
ordinals: Mapping[str, OrdinalEntry]
fractions: Mapping[str, FractionEntry]
multipliers: Mapping[str, MultiplierEntry]
quantifiers: Mapping[str, QuantifierEntry]
comparison_anchors: Mapping[str, Tuple[ComparisonAnchorEntry, ...]]
number_formats: Tuple[NumberFormatEntry, ...]
fraction_symbols: Mapping[str, FractionEntry]
fraction_spelled: Mapping[str, FractionEntry]
def _read_jsonl(path: Path):
with path.open("r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
yield json.loads(line)
@lru_cache(maxsize=1)
def _index() -> _PackIndex:
lexicon = list(_read_jsonl(PACK_ROOT / "lexicon.jsonl"))
cardinals: dict[str, CardinalEntry] = {}
ordinals: dict[str, OrdinalEntry] = {}
fractions: dict[str, FractionEntry] = {}
multipliers: dict[str, MultiplierEntry] = {}
quantifiers: dict[str, QuantifierEntry] = {}
comparison_buckets: dict[str, list[ComparisonAnchorEntry]] = {}
number_formats: list[NumberFormatEntry] = []
fraction_symbols: dict[str, FractionEntry] = {}
fraction_spelled: dict[str, FractionEntry] = {}
for row in lexicon:
eid = row["entry_id"]
surface = row["surface"].lower()
if eid.startswith("en-num-card-"):
cardinals[surface] = CardinalEntry(
entry_id=eid, surface=row["surface"],
numeric_value=int(row["numeric_value"]),
morphology=row["morphology"],
)
elif eid.startswith("en-num-ord-"):
ordinals[surface] = OrdinalEntry(
entry_id=eid, surface=row["surface"],
position=int(row["position"]),
morphology=row["morphology"],
)
elif eid.startswith("en-num-frac-"):
fe = FractionEntry(
entry_id=eid, surface=row["surface"],
numerator=int(row["numerator"]),
denominator=int(row["denominator"]),
decimal_value=float(row["decimal_value"]),
morphology=row["morphology"],
symbol=row.get("symbol"),
spelled_form=row.get("spelled_form"),
)
fractions[surface] = fe
if fe.symbol:
fraction_symbols[fe.symbol] = fe
if fe.morphology == "fraction-symbol":
fraction_symbols.setdefault(fe.surface, fe)
if fe.spelled_form:
fraction_spelled[fe.spelled_form.lower()] = fe
elif eid.startswith("en-num-mult-"):
# ``half`` exists both as fraction and multiplier; both rows are
# accepted — disambiguation is by caller intent (which lookup).
multipliers[surface] = MultiplierEntry(
entry_id=eid, surface=row["surface"],
factor=float(row["factor"]),
morphology=row["morphology"],
)
elif eid.startswith("en-num-quant-"):
quantifiers[surface] = QuantifierEntry(
entry_id=eid, surface=row["surface"],
semantic_type=row["semantic_type"],
morphology=row["morphology"],
determinate_value=row.get("determinate_value"),
)
elif eid.startswith("en-num-compare-"):
anchor = ComparisonAnchorEntry(
entry_id=eid, surface=row["surface"],
anchor_class=row["anchor_class"],
morphology=row["morphology"],
)
comparison_buckets.setdefault(surface, []).append(anchor)
elif eid.startswith("en-num-format-"):
number_formats.append(NumberFormatEntry(
entry_id=eid,
format_id=row["format_id"],
regex=row["regex"],
parser_function=row["parser_function"],
output_type=row["output_type"],
))
return _PackIndex(
cardinals=cardinals,
ordinals=ordinals,
fractions=fractions,
multipliers=multipliers,
quantifiers=quantifiers,
comparison_anchors={k: tuple(v) for k, v in comparison_buckets.items()},
number_formats=tuple(number_formats),
fraction_symbols=fraction_symbols,
fraction_spelled=fraction_spelled,
)
# ---------------------------------------------------------------------------
# Public lookups
# ---------------------------------------------------------------------------
def lookup_cardinal(token: str) -> Optional[CardinalEntry]:
return _index().cardinals.get(token.lower())
def lookup_ordinal(token: str) -> Optional[OrdinalEntry]:
return _index().ordinals.get(token.lower())
def lookup_fraction(token: str) -> Optional[FractionEntry]:
if not token:
return None
idx = _index()
# Symbol lookup (single grapheme like '½') — case-preserving.
if token in idx.fraction_symbols:
return idx.fraction_symbols[token]
lower = token.lower()
# Article-bound forms: "a half", "a quarter" → same entry as bare form.
stripped = re.sub(r"^(a|an|the)\s+", "", lower)
if stripped in idx.fractions:
return idx.fractions[stripped]
if stripped in idx.fraction_spelled:
return idx.fraction_spelled[stripped]
# Compound form: "two-thirds", "three quarters", "five eighths"
compound = _parse_compound_fraction(stripped, idx)
if compound is not None:
return compound
return None
def lookup_quantifier(token: str) -> Optional[QuantifierEntry]:
return _index().quantifiers.get(token.lower())
def lookup_multiplier(token: str) -> Optional[MultiplierEntry]:
return _index().multipliers.get(token.lower())
def lookup_comparison_anchor(token: str) -> Optional[ComparisonAnchorEntry]:
"""Returns the first matching anchor entry. A surface may carry both
additive and multiplicative classes only via separate entries; use
:func:`lookup_comparison_anchors` to retrieve them all."""
bucket = _index().comparison_anchors.get(token.lower())
if not bucket:
return None
return bucket[0]
def lookup_comparison_anchors(token: str) -> Tuple[ComparisonAnchorEntry, ...]:
return _index().comparison_anchors.get(token.lower(), ())
def number_format_entries() -> Tuple[NumberFormatEntry, ...]:
return _index().number_formats
# ---------------------------------------------------------------------------
# Format matching
# ---------------------------------------------------------------------------
def _parse_thousand_separated(raw: str) -> int:
return int(raw.replace(",", ""))
def _parse_decimal(raw: str) -> float:
return float(raw)
def _parse_slash_fraction(raw: str) -> Fraction:
return Fraction(raw)
def _parse_mixed_number(raw: str) -> Fraction:
sign = 1
body = raw
if body.startswith("-"):
sign = -1
body = body[1:]
whole_str, frac_str = body.split(" ", 1)
whole = int(whole_str)
frac = Fraction(frac_str)
return sign * (whole + frac)
def _parse_percentage(raw: str) -> float:
return float(raw[:-1]) / 100.0
def _parse_signed_integer(raw: str) -> int:
return int(raw)
_FORMAT_PARSERS = {
"parse_thousand_separated": _parse_thousand_separated,
"parse_decimal": _parse_decimal,
"parse_slash_fraction": _parse_slash_fraction,
"parse_mixed_number": _parse_mixed_number,
"parse_percentage": _parse_percentage,
"parse_signed_integer": _parse_signed_integer,
}
def match_number_format(token: str) -> Optional[ParsedNumber]:
"""Return the parsed numeric value if ``token`` matches exactly one
ratified format rule. Returns ``None`` if no rule matches OR if more
than one rule matches (ambiguity is refused per ``wrong == 0``)."""
if not isinstance(token, str) or not token:
return None
idx = _index()
matches: list[NumberFormatEntry] = []
for fmt in idx.number_formats:
if re.fullmatch(fmt.regex, token):
matches.append(fmt)
if len(matches) != 1:
return None
fmt = matches[0]
parser = _FORMAT_PARSERS[fmt.parser_function]
try:
value = parser(token)
except (ValueError, ZeroDivisionError):
return None
return ParsedNumber(
format_id=fmt.format_id, raw=token, value=value,
output_type=fmt.output_type,
)
# ---------------------------------------------------------------------------
# Compound parsing
# ---------------------------------------------------------------------------
_PLURAL_TO_SINGULAR = {
"halves": "half", "thirds": "third", "quarters": "quarter",
"fourths": "quarter", # American "fourths" → same numeric form
"fifths": "fifth", "sixths": "sixth", "sevenths": "seventh",
"eighths": "eighth", "ninths": "ninth", "tenths": "tenth",
"sixteenths": "sixteenth",
}
def _parse_compound_fraction(text: str, idx: _PackIndex) -> Optional[FractionEntry]:
"""Resolve compounds like ``two-thirds`` / ``three quarters``.
Accepts a hyphen or a single space between the cardinal and the
pluralised denominator word. Plural is required ("two thirds" not
"two third") except for ``half`` where both forms exist in the wild
we accept "two halves" / "one half" / "a half"."""
if "-" in text:
parts = text.split("-", 1)
elif " " in text:
parts = text.split(" ", 1)
else:
return None
if len(parts) != 2:
return None
num_word, den_word = parts
cardinal = idx.cardinals.get(num_word)
if cardinal is None:
return None
denom_singular = _PLURAL_TO_SINGULAR.get(den_word)
if denom_singular is None:
# Allow already-singular for "one half" / "one third" patterns.
if den_word in idx.fractions:
denom_singular = den_word
else:
return None
base = idx.fractions.get(denom_singular)
if base is None:
return None
num = cardinal.numeric_value
den = base.denominator
if den == 0:
return None
return FractionEntry(
entry_id=f"{base.entry_id}+compound:{num}",
surface=text,
numerator=num,
denominator=den,
decimal_value=num / den,
morphology="fraction-compound",
symbol=None,
spelled_form=None,
)
def parse_compound_cardinal(text: str) -> Optional[int]:
"""Resolve compound English cardinals.
Handles:
* single-word forms ("seventeen")
* hyphenated tens-unit ("twenty-one", "ninety-nine")
* magnitude composition ("two hundred", "three hundred and fifty",
"two thousand five hundred and seventeen")
"""
if not text:
return None
text = text.strip().lower()
if not text:
return None
idx = _index()
if text in idx.cardinals:
return idx.cardinals[text].numeric_value
# Strip "and" connectors; normalise hyphens to spaces.
normalised = text.replace("-", " ")
tokens = [t for t in normalised.split() if t and t != "and"]
if not tokens:
return None
total = 0
current = 0
for tok in tokens:
if tok in idx.cardinals:
val = idx.cardinals[tok].numeric_value
if val == 100:
current = max(current, 1) * 100
elif val >= 1000:
current = max(current, 1) * val
total += current
current = 0
else:
current += val
else:
return None
return total + current

View file

@ -0,0 +1,405 @@
"""Deterministic generator for the en_numerics_v1 ratified semantic pack.
Writes lexicon.jsonl, glosses.jsonl, manifest.json, and a self-sealing
.mastery_report.json under language_packs/data/en_numerics_v1/.
Re-running yields byte-identical output. SHA-256 checksums hash the
bytes actually written to disk (CLAUDE.md rule).
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
PACK_ID = "en_numerics_v1"
PROVENANCE = "adr-0128:operator_seed:2026-05-23"
PACK_DIR = Path(__file__).resolve().parent.parent / "language_packs" / "data" / PACK_ID
def jline(d: dict) -> str:
return json.dumps(d, ensure_ascii=False, sort_keys=False, separators=(",", ":"))
# ---------------------------------------------------------------------------
# Cardinals: 0..20, tens (30..90), magnitudes (hundred/thousand/million/billion)
# ---------------------------------------------------------------------------
CARDINAL_UNITS = [
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
"sixteen", "seventeen", "eighteen", "nineteen", "twenty",
]
CARDINAL_TENS = [
("thirty", 30), ("forty", 40), ("fifty", 50), ("sixty", 60),
("seventy", 70), ("eighty", 80), ("ninety", 90),
]
CARDINAL_MAGNITUDES = [
("hundred", 100), ("thousand", 1000),
("million", 1_000_000), ("billion", 1_000_000_000),
]
# Ordinals — exhaustive 1st..31st + decade/magnitude forms
ORDINALS = [
("first", 1), ("second", 2), ("third", 3), ("fourth", 4), ("fifth", 5),
("sixth", 6), ("seventh", 7), ("eighth", 8), ("ninth", 9), ("tenth", 10),
("eleventh", 11), ("twelfth", 12), ("thirteenth", 13), ("fourteenth", 14),
("fifteenth", 15), ("sixteenth", 16), ("seventeenth", 17),
("eighteenth", 18), ("nineteenth", 19), ("twentieth", 20),
("twenty-first", 21), ("twenty-second", 22), ("twenty-third", 23),
("twenty-fourth", 24), ("twenty-fifth", 25), ("twenty-sixth", 26),
("twenty-seventh", 27), ("twenty-eighth", 28), ("twenty-ninth", 29),
("thirtieth", 30), ("thirty-first", 31),
("fortieth", 40), ("fiftieth", 50), ("sixtieth", 60), ("seventieth", 70),
("eightieth", 80), ("ninetieth", 90),
("hundredth", 100), ("thousandth", 1000), ("millionth", 1_000_000),
]
# Fractions — named 1/2..1/10 + sixteenth + thirty-second + symbol forms
FRACTIONS_NAMED = [
("half", 1, 2, "½"),
("third", 1, 3, ""),
("quarter", 1, 4, "¼"),
("fifth", 1, 5, ""),
("sixth", 1, 6, ""),
("seventh", 1, 7, ""),
("eighth", 1, 8, ""),
("ninth", 1, 9, ""),
("tenth", 1, 10, ""),
("sixteenth", 1, 16, None),
("thirty-second", 1, 32, None),
]
# Additional symbol-only fraction entries (no English-word form)
FRACTION_SYMBOL_ONLY = [
("", 2, 3, "two-thirds"),
("¾", 3, 4, "three-quarters"),
("", 3, 8, "three-eighths"),
("", 5, 8, "five-eighths"),
("", 7, 8, "seven-eighths"),
]
MULTIPLIERS = [
("double", 2.0), ("triple", 3.0), ("quadruple", 4.0),
("quintuple", 5.0), ("twice", 2.0), ("thrice", 3.0), ("half", 0.5),
]
QUANTIFIERS = [
("all", "total", None),
("none", "empty", 0),
("some", "indefinite", None),
("both", "paired", 2),
("each", "distributive", None),
("every", "distributive", None),
("many", "indefinite", None),
("few", "indefinite", None),
("several", "indefinite", None),
("most", "partial", None),
("any", "indefinite", None),
("no", "empty", 0),
("single", "total", 1),
]
COMPARE_ADDITIVE = [
"more", "fewer", "less", "additional", "extra", "missing", "remaining",
]
COMPARE_MULTIPLICATIVE = [
"twice", "thrice", "times", "half", "double", "triple",
"quadruple", "third", "quarter",
]
NUMBER_FORMATS = [
{
"format_id": "thousand_separated",
"regex": r"^\d{1,3}(?:,\d{3})+$",
"parser_function": "parse_thousand_separated",
"output_type": "int",
},
{
"format_id": "decimal",
"regex": r"^-?\d+\.\d+$",
"parser_function": "parse_decimal",
"output_type": "float",
},
{
"format_id": "slash_fraction",
"regex": r"^-?\d+/\d+$",
"parser_function": "parse_slash_fraction",
"output_type": "Fraction",
},
{
"format_id": "mixed_number",
"regex": r"^-?\d+ \d+/\d+$",
"parser_function": "parse_mixed_number",
"output_type": "Fraction",
},
{
"format_id": "percentage",
"regex": r"^-?\d+(?:\.\d+)?%$",
"parser_function": "parse_percentage",
"output_type": "float",
},
{
"format_id": "signed_integer",
"regex": r"^-?\d+$",
"parser_function": "parse_signed_integer",
"output_type": "int",
},
]
def make_entry(entry_id: str, surface: str, *, pos: str, domains, tags, extra):
e = {
"entry_id": entry_id,
"surface": surface,
"lemma": surface,
"language": "en",
"pos": pos,
"semantic_domains": list(domains),
"morphology_tags": list(tags),
"provenance_ids": [PROVENANCE],
}
e.update(extra)
return e
def build_lexicon():
rows = []
glosses = []
# Cardinals — units 0..20
for i, w in enumerate(CARDINAL_UNITS):
eid = f"en-num-card-{i:03d}"
rows.append(make_entry(
eid, w, pos="NUM",
domains=["numerics.cardinal", "mathematics.value.integer"],
tags=["cardinal", "unit" if i < 20 else "decade-anchor"],
extra={"numeric_value": i, "morphology": "cardinal"},
))
glosses.append({"entry_id": eid, "gloss": f"Cardinal number word for the integer {i}."})
# Cardinals — tens 30..90
for j, (w, val) in enumerate(CARDINAL_TENS, start=21):
eid = f"en-num-card-{j:03d}"
rows.append(make_entry(
eid, w, pos="NUM",
domains=["numerics.cardinal", "mathematics.value.integer"],
tags=["cardinal", "decade-anchor"],
extra={"numeric_value": val, "morphology": "cardinal"},
))
glosses.append({"entry_id": eid, "gloss": f"Cardinal tens word for the integer {val}."})
# Cardinals — magnitudes
for k, (w, val) in enumerate(CARDINAL_MAGNITUDES, start=28):
eid = f"en-num-card-{k:03d}"
rows.append(make_entry(
eid, w, pos="NUM",
domains=["numerics.cardinal", "mathematics.value.magnitude"],
tags=["cardinal", "magnitude"],
extra={"numeric_value": val, "morphology": "cardinal-magnitude"},
))
glosses.append({"entry_id": eid, "gloss": f"Cardinal magnitude word for the integer {val}."})
# Ordinals
for n, (w, pos_val) in enumerate(ORDINALS, start=1):
eid = f"en-num-ord-{n:03d}"
rows.append(make_entry(
eid, w, pos="ADJ",
domains=["numerics.ordinal", "mathematics.value.position"],
tags=["ordinal"],
extra={"position": pos_val, "morphology": "ordinal"},
))
glosses.append({"entry_id": eid, "gloss": f"Ordinal number word for position {pos_val}."})
# Fractions — named
for m, (w, num, den, sym) in enumerate(FRACTIONS_NAMED, start=1):
eid = f"en-num-frac-{m:03d}"
extra = {
"numerator": num,
"denominator": den,
"decimal_value": num / den,
"morphology": "fraction",
}
if sym:
extra["symbol"] = sym
rows.append(make_entry(
eid, w, pos="NOUN",
domains=["numerics.fraction", "mathematics.value.rational"],
tags=["fraction", "named"],
extra=extra,
))
glosses.append({"entry_id": eid, "gloss": f"Named fraction word for {num}/{den}."})
# Fractions — symbol-only forms (canonical home; en_units_v1 cross-references)
for m, (sym, num, den, name) in enumerate(FRACTION_SYMBOL_ONLY,
start=len(FRACTIONS_NAMED) + 1):
eid = f"en-num-frac-{m:03d}"
rows.append(make_entry(
eid, sym, pos="SYM",
domains=["numerics.fraction", "mathematics.value.rational"],
tags=["fraction", "symbol"],
extra={
"numerator": num,
"denominator": den,
"decimal_value": num / den,
"morphology": "fraction-symbol",
"spelled_form": name,
},
))
glosses.append({"entry_id": eid, "gloss": f"Unicode fraction symbol for {num}/{den}."})
# Multipliers
for i, (w, factor) in enumerate(MULTIPLIERS, start=1):
eid = f"en-num-mult-{i:03d}"
rows.append(make_entry(
eid, w, pos="ADJ" if w not in {"twice", "thrice"} else "ADV",
domains=["numerics.multiplier", "mathematics.value.scalar"],
tags=["multiplier"],
extra={"factor": factor, "morphology": "multiplier"},
))
glosses.append({"entry_id": eid, "gloss": f"Multiplier word scaling a quantity by {factor}."})
# Quantifiers
for i, (w, sem_type, det_val) in enumerate(QUANTIFIERS, start=1):
eid = f"en-num-quant-{i:03d}"
extra = {"semantic_type": sem_type, "morphology": "quantifier"}
if det_val is not None:
extra["determinate_value"] = det_val
rows.append(make_entry(
eid, w, pos="DET",
domains=["numerics.quantifier", "mathematics.value.quantifier"],
tags=["quantifier", sem_type],
extra=extra,
))
glosses.append({"entry_id": eid,
"gloss": f"Quantifier ({sem_type}). "
f"{'Determinate value ' + str(det_val) + '.' if det_val is not None else 'No determinate value; indefinite quantifiers trigger refusal.'}"})
# Comparison anchors — additive
for i, w in enumerate(COMPARE_ADDITIVE, start=1):
eid = f"en-num-compare-add-{i:03d}"
rows.append(make_entry(
eid, w, pos="ADJ",
domains=["numerics.compare.additive"],
tags=["comparison-anchor", "additive"],
extra={"anchor_class": "additive", "morphology": "comparison-anchor"},
))
glosses.append({"entry_id": eid,
"gloss": f"Additive comparison anchor '{w}'."})
# Comparison anchors — multiplicative
for i, w in enumerate(COMPARE_MULTIPLICATIVE, start=1):
eid = f"en-num-compare-mul-{i:03d}"
rows.append(make_entry(
eid, w, pos="ADJ",
domains=["numerics.compare.multiplicative"],
tags=["comparison-anchor", "multiplicative"],
extra={"anchor_class": "multiplicative", "morphology": "comparison-anchor"},
))
glosses.append({"entry_id": eid,
"gloss": f"Multiplicative comparison anchor '{w}'."})
# Number formats (declarative rule entries)
for i, fmt in enumerate(NUMBER_FORMATS, start=1):
eid = f"en-num-format-{i:03d}"
rows.append(make_entry(
eid, fmt["format_id"], pos="X",
domains=["numerics.format"],
tags=["number-format"],
extra={
"format_id": fmt["format_id"],
"regex": fmt["regex"],
"parser_function": fmt["parser_function"],
"output_type": fmt["output_type"],
"morphology": "number-format",
},
))
glosses.append({"entry_id": eid,
"gloss": f"Number-format rule '{fmt['format_id']}' "
f"(regex emits {fmt['output_type']})."})
return rows, glosses
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def main():
PACK_DIR.mkdir(parents=True, exist_ok=True)
rows, glosses = build_lexicon()
lex_text = "\n".join(jline(r) for r in rows) + "\n"
gloss_text = "\n".join(jline(g) for g in glosses) + "\n"
lex_path = PACK_DIR / "lexicon.jsonl"
gloss_path = PACK_DIR / "glosses.jsonl"
lex_path.write_text(lex_text, encoding="utf-8")
gloss_path.write_text(gloss_text, encoding="utf-8")
lex_checksum = sha256_bytes(lex_path.read_bytes())
gloss_checksum = sha256_bytes(gloss_path.read_bytes())
manifest = {
"pack_id": PACK_ID,
"language": "en",
"role": "operational_base",
"script": "Latin",
"normalization_policy": "unitize_versor",
"source_manifest": f"{PACK_ID}.lexicon.jsonl",
"determinism_class": "D0",
"checksum": lex_checksum,
"version": "1.0.0",
"gate_engaged": True,
"oov_policy": "tagged_fallback",
"glosses_checksum": gloss_checksum,
"definitional_layer": False,
"provenance": PROVENANCE,
}
manifest_path = PACK_DIR / "manifest.json"
manifest_text = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
manifest_path.write_text(manifest_text, encoding="utf-8")
# Self-sealing mastery report: hashes the bytes of the three pack files
# (lexicon, glosses, manifest) actually written to disk. Recomputing
# those checksums from disk and matching them seals the report.
counts = {
"cardinal": sum(1 for r in rows if r["entry_id"].startswith("en-num-card-")),
"ordinal": sum(1 for r in rows if r["entry_id"].startswith("en-num-ord-")),
"fraction": sum(1 for r in rows if r["entry_id"].startswith("en-num-frac-")),
"multiplier": sum(1 for r in rows if r["entry_id"].startswith("en-num-mult-")),
"quantifier": sum(1 for r in rows if r["entry_id"].startswith("en-num-quant-")),
"comparison_anchor": sum(1 for r in rows if r["entry_id"].startswith("en-num-compare-")),
"number_format": sum(1 for r in rows if r["entry_id"].startswith("en-num-format-")),
}
report = {
"pack_id": PACK_ID,
"version": "1.0.0",
"provenance": PROVENANCE,
"lexicon_sha256": lex_checksum,
"glosses_sha256": gloss_checksum,
"manifest_sha256": sha256_bytes(manifest_path.read_bytes()),
"entry_counts": counts,
"total_entries": len(rows),
"ratified_at": "2026-05-23",
"ratification_invariants": [
"cardinal_exhaustive_0_to_20",
"cardinal_tens_complete",
"cardinal_magnitudes_complete",
"ordinal_exhaustive_1_to_31",
"fraction_named_1_2_through_1_10",
"fraction_irregular_sixteenth_thirty_second",
"quantifier_semantic_type_closed_set",
"number_format_regex_corpus_gated",
],
}
report_path = PACK_DIR / f"{PACK_ID}.mastery_report.json"
report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8")
print(f"wrote {len(rows)} lexicon entries to {PACK_DIR}")
print(f"lexicon sha256 = {lex_checksum}")
print(f"glosses sha256 = {gloss_checksum}")
print(f"entry counts: {counts}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,209 @@
"""ADR-0128 sub-phase 0128.2 — loader API + compound composition.
Covers the public lookup surface plus the deterministic compound rules
(cardinal composition, compound-fraction composition, article-bound
fraction forms, case-insensitivity)."""
from __future__ import annotations
import pytest
from language_packs.numerics_loader import (
lookup_cardinal,
lookup_comparison_anchor,
lookup_comparison_anchors,
lookup_fraction,
lookup_multiplier,
lookup_ordinal,
lookup_quantifier,
parse_compound_cardinal,
)
# ---------------------------------------------------------------------------
# Cardinal lookup + compound composition
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,value", [
("zero", 0), ("seven", 7), ("seventeen", 17), ("twenty", 20),
("fifty", 50), ("ninety", 90), ("hundred", 100), ("thousand", 1000),
("million", 1_000_000),
])
def test_lookup_cardinal_simple(word, value):
entry = lookup_cardinal(word)
assert entry is not None and entry.numeric_value == value
def test_lookup_cardinal_case_insensitive():
assert lookup_cardinal("Seventeen").numeric_value == 17
assert lookup_cardinal("SEVENTEEN").numeric_value == 17
def test_lookup_cardinal_unknown_returns_none():
assert lookup_cardinal("seventeenish") is None
assert lookup_cardinal("") is None
@pytest.mark.parametrize("text,expected", [
("twenty-one", 21),
("twenty one", 21),
("ninety-nine", 99),
("one hundred", 100),
("two hundred", 200),
("two hundred and fifty", 250),
("three hundred and seventeen", 317),
("one thousand", 1000),
("two thousand five hundred", 2500),
("two thousand five hundred and seventeen", 2517),
("seventeen", 17),
("fifty", 50),
("thirty-two", 32),
("one million", 1_000_000),
])
def test_parse_compound_cardinal(text, expected):
assert parse_compound_cardinal(text) == expected
def test_parse_compound_cardinal_rejects_unknown_token():
assert parse_compound_cardinal("twenty banana") is None
assert parse_compound_cardinal("") is None
assert parse_compound_cardinal(" ") is None
# ---------------------------------------------------------------------------
# Ordinal lookup
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,pos", [
("first", 1), ("second", 2), ("third", 3), ("tenth", 10),
("twelfth", 12), ("twentieth", 20), ("twenty-first", 21),
("thirty-first", 31), ("hundredth", 100),
])
def test_lookup_ordinal(word, pos):
entry = lookup_ordinal(word)
assert entry is not None and entry.position == pos
def test_lookup_ordinal_case_insensitive():
assert lookup_ordinal("First").position == 1
# ---------------------------------------------------------------------------
# Fraction lookup + compound + article-bound
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("token,num,den", [
("half", 1, 2),
("a half", 1, 2),
("third", 1, 3),
("a third", 1, 3),
("quarter", 1, 4),
("the quarter", 1, 4),
("sixteenth", 1, 16),
("thirty-second", 1, 32),
])
def test_lookup_fraction_named_and_article_bound(token, num, den):
entry = lookup_fraction(token)
assert entry is not None
assert entry.numerator == num and entry.denominator == den
@pytest.mark.parametrize("token,num,den", [
("two-thirds", 2, 3),
("two thirds", 2, 3),
("three-quarters", 3, 4),
("three quarters", 3, 4),
("three-fourths", 3, 4), # American "fourths" alias
("five eighths", 5, 8),
("seven tenths", 7, 10),
("nine sixteenths", 9, 16),
])
def test_lookup_fraction_compound(token, num, den):
entry = lookup_fraction(token)
assert entry is not None
assert entry.numerator == num and entry.denominator == den
assert entry.decimal_value == pytest.approx(num / den)
@pytest.mark.parametrize("symbol,num,den", [
("½", 1, 2), ("¼", 1, 4), ("¾", 3, 4),
("", 1, 3), ("", 2, 3), ("", 1, 8),
("", 3, 8), ("", 5, 8), ("", 7, 8),
])
def test_lookup_fraction_symbol(symbol, num, den):
entry = lookup_fraction(symbol)
assert entry is not None
assert entry.numerator == num and entry.denominator == den
def test_lookup_fraction_unknown_returns_none():
assert lookup_fraction("zillionth") is None
assert lookup_fraction("") is None
# ---------------------------------------------------------------------------
# Multipliers
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,factor", [
("double", 2.0), ("triple", 3.0), ("quadruple", 4.0),
("quintuple", 5.0), ("twice", 2.0), ("thrice", 3.0), ("half", 0.5),
])
def test_lookup_multiplier(word, factor):
entry = lookup_multiplier(word)
assert entry is not None and entry.factor == factor
# ---------------------------------------------------------------------------
# Quantifiers
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,sem_type,det", [
("all", "total", None),
("none", "empty", 0),
("some", "indefinite", None),
("both", "paired", 2),
("each", "distributive", None),
("every", "distributive", None),
("many", "indefinite", None),
("few", "indefinite", None),
("several", "indefinite", None),
("most", "partial", None),
("any", "indefinite", None),
("no", "empty", 0),
("single", "total", 1),
])
def test_lookup_quantifier(word, sem_type, det):
entry = lookup_quantifier(word)
assert entry is not None
assert entry.semantic_type == sem_type
assert entry.determinate_value == det
def test_indefinite_quantifier_triggers_refusal_signal():
# The loader doesn't refuse on its own — it surfaces the signal that
# the parser will use to refuse. Test the signal is correctly tagged.
assert lookup_quantifier("some").is_indefinite is True
assert lookup_quantifier("all").is_indefinite is False
# ---------------------------------------------------------------------------
# Comparison anchors
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,anchor_class", [
("more", "additive"), ("fewer", "additive"), ("less", "additive"),
("additional", "additive"), ("extra", "additive"),
("missing", "additive"), ("remaining", "additive"),
("times", "multiplicative"), ("thrice", "multiplicative"),
("quadruple", "multiplicative"),
])
def test_lookup_comparison_anchor_single(word, anchor_class):
entry = lookup_comparison_anchor(word)
assert entry is not None and entry.anchor_class == anchor_class
def test_comparison_anchor_dual_class_for_half():
# 'half' / 'double' / 'twice' / 'triple' / 'third' / 'quarter' are
# multiplicative anchors but may also exist as multipliers/fractions.
# The comparison-anchor table puts them in multiplicative.
anchors = lookup_comparison_anchors("half")
assert any(a.anchor_class == "multiplicative" for a in anchors)
def test_comparison_anchor_unknown_returns_none():
assert lookup_comparison_anchor("zonk") is None
assert lookup_comparison_anchors("zonk") == ()

View file

@ -0,0 +1,253 @@
"""ADR-0128 sub-phase 0128.1 — number-format regex corpora.
Each format rule has 10 positive + 10 negative cases. Ambiguous
strings (e.g., ``1.000`` could be ``1`` or ``1000``) are refused via
``match_number_format`` returning ``None`` when multiple rules match.
The "wrong == 0" invariant is preserved by never guessing.
"""
from __future__ import annotations
from fractions import Fraction
import pytest
from language_packs.numerics_loader import match_number_format
# ---------------------------------------------------------------------------
# thousand_separated — e.g., 1,000 12,345
# ---------------------------------------------------------------------------
THOUSAND_SEP_POS = [
("1,000", 1000), ("10,000", 10_000), ("100,000", 100_000),
("1,000,000", 1_000_000), ("12,345", 12_345), ("999,999", 999_999),
("1,234,567", 1_234_567), ("23,000", 23_000), ("5,000", 5_000),
("987,654,321", 987_654_321),
]
THOUSAND_SEP_NEG = [
"1000", # no separator
"1,00", # malformed group
"1,0000", # malformed group
"1,000.5", # extra suffix
"1,000,", # trailing comma
",1,000", # leading comma
"abc", # non-numeric
"1,000a", # trailing letter
"1.000", # decimal not separator (per current locale rule)
"1, 000", # space inside
]
@pytest.mark.parametrize("raw,expected", THOUSAND_SEP_POS)
def test_thousand_separated_positive(raw, expected):
parsed = match_number_format(raw)
assert parsed is not None and parsed.format_id == "thousand_separated"
assert parsed.value == expected
@pytest.mark.parametrize("raw", THOUSAND_SEP_NEG)
def test_thousand_separated_negative(raw):
parsed = match_number_format(raw)
if parsed is not None:
assert parsed.format_id != "thousand_separated"
# ---------------------------------------------------------------------------
# decimal — 1.5, 3.14, 0.25, -0.5
# ---------------------------------------------------------------------------
DECIMAL_POS = [
("1.5", 1.5), ("3.14", 3.14), ("0.25", 0.25), ("100.001", 100.001),
("-0.5", -0.5), ("-1.25", -1.25), ("10.0", 10.0), ("0.0", 0.0),
("999.999", 999.999), ("1.0001", 1.0001),
]
DECIMAL_NEG = [
"1", # no fractional part
".5", # no leading digit
"1.", # trailing dot
"1.5.0", # two dots
"1,5", # wrong separator
"abc.def", # non-numeric
"1e5", # scientific
"1.5%", # percentage, different format
"1/2", # slash form
"--1.5", # double minus
]
@pytest.mark.parametrize("raw,expected", DECIMAL_POS)
def test_decimal_positive(raw, expected):
parsed = match_number_format(raw)
assert parsed is not None and parsed.format_id == "decimal"
assert parsed.value == pytest.approx(expected)
@pytest.mark.parametrize("raw", DECIMAL_NEG)
def test_decimal_negative(raw):
parsed = match_number_format(raw)
if parsed is not None:
assert parsed.format_id != "decimal"
# ---------------------------------------------------------------------------
# slash_fraction — 1/2, 3/4, 7/8
# ---------------------------------------------------------------------------
SLASH_POS = [
("1/2", Fraction(1, 2)), ("3/4", Fraction(3, 4)), ("7/8", Fraction(7, 8)),
("5/6", Fraction(5, 6)), ("1/3", Fraction(1, 3)), ("2/3", Fraction(2, 3)),
("11/16", Fraction(11, 16)), ("100/101", Fraction(100, 101)),
("9/10", Fraction(9, 10)), ("-1/2", Fraction(-1, 2)),
]
SLASH_NEG = [
"1/0", # zero denominator — parser refuses
"1/", # malformed
"/2", # malformed
"1 / 2", # spaces around slash
"1.5/2", # decimal numerator
"1/2/3", # double slash
"abc/def", # non-numeric
"1//2", # double slash
"1\\2", # wrong slash
"1 1/2", # mixed number — different format
]
@pytest.mark.parametrize("raw,expected", SLASH_POS)
def test_slash_fraction_positive(raw, expected):
parsed = match_number_format(raw)
assert parsed is not None and parsed.format_id == "slash_fraction"
assert parsed.value == expected
@pytest.mark.parametrize("raw", SLASH_NEG)
def test_slash_fraction_negative(raw):
parsed = match_number_format(raw)
if parsed is not None:
assert parsed.format_id != "slash_fraction"
# ---------------------------------------------------------------------------
# mixed_number — 1 1/2, 2 3/4
# ---------------------------------------------------------------------------
MIXED_POS = [
("1 1/2", Fraction(3, 2)), ("2 3/4", Fraction(11, 4)),
("3 1/3", Fraction(10, 3)), ("5 7/8", Fraction(47, 8)),
("10 1/4", Fraction(41, 4)), ("0 1/2", Fraction(1, 2)),
("100 1/100", Fraction(10001, 100)), ("1 9/10", Fraction(19, 10)),
("4 5/6", Fraction(29, 6)), ("-1 1/2", Fraction(-3, 2)),
]
MIXED_NEG = [
"1/2", # bare fraction, no whole part
"1 1/2", # double space
"1.5 1/2", # decimal whole
"1 1.5", # decimal fraction
"1 1/0", # zero denom — refused at parse
"1 -1/2", # internal sign
"abc 1/2", # non-numeric
"1 1 / 2", # spaces in fraction part
"1", # whole only
"one 1/2", # word number
]
@pytest.mark.parametrize("raw,expected", MIXED_POS)
def test_mixed_number_positive(raw, expected):
parsed = match_number_format(raw)
assert parsed is not None and parsed.format_id == "mixed_number"
assert parsed.value == expected
@pytest.mark.parametrize("raw", MIXED_NEG)
def test_mixed_number_negative(raw):
parsed = match_number_format(raw)
if parsed is not None:
assert parsed.format_id != "mixed_number"
# ---------------------------------------------------------------------------
# percentage — 75%, 1.5%, 100%
# ---------------------------------------------------------------------------
PERCENT_POS = [
("75%", 0.75), ("1.5%", 0.015), ("100%", 1.0), ("0%", 0.0),
("50%", 0.5), ("99.9%", 0.999), ("12.5%", 0.125), ("200%", 2.0),
("0.5%", 0.005), ("-25%", -0.25),
]
PERCENT_NEG = [
"75", # no percent sign
"%75", # leading percent
"75 %", # space before percent
"75%%", # double percent
"abc%", # non-numeric
"1.5.0%", # bad decimal
"75/100", # different format
"75.%", # trailing dot before percent
".5%", # no leading digit
"1,000%", # separator inside percent
]
@pytest.mark.parametrize("raw,expected", PERCENT_POS)
def test_percentage_positive(raw, expected):
parsed = match_number_format(raw)
assert parsed is not None and parsed.format_id == "percentage"
assert parsed.value == pytest.approx(expected)
@pytest.mark.parametrize("raw", PERCENT_NEG)
def test_percentage_negative(raw):
parsed = match_number_format(raw)
if parsed is not None:
assert parsed.format_id != "percentage"
# ---------------------------------------------------------------------------
# signed_integer — -3, 42, -0
# ---------------------------------------------------------------------------
SIGNED_INT_POS = [
("0", 0), ("1", 1), ("42", 42), ("-3", -3), ("-1", -1),
("100", 100), ("999", 999), ("-999", -999), ("7", 7), ("-7", -7),
]
SIGNED_INT_NEG = [
"1.5", # decimal — owned by decimal rule
"1,000", # owned by thousand_separated
"1/2", # owned by slash_fraction
"1 1/2", # owned by mixed_number
"75%", # owned by percentage
"abc", # non-numeric
"--3", # double minus
"+3", # leading plus — out of spec
"3a", # trailing letter
" 3", # leading space
]
@pytest.mark.parametrize("raw,expected", SIGNED_INT_POS)
def test_signed_integer_positive(raw, expected):
parsed = match_number_format(raw)
assert parsed is not None and parsed.format_id == "signed_integer"
assert parsed.value == expected
@pytest.mark.parametrize("raw", SIGNED_INT_NEG)
def test_signed_integer_negative(raw):
parsed = match_number_format(raw)
if parsed is not None:
assert parsed.format_id != "signed_integer"
# ---------------------------------------------------------------------------
# Ambiguity refusal — preserves wrong == 0
# ---------------------------------------------------------------------------
def test_empty_string_refused():
assert match_number_format("") is None
def test_pure_text_refused():
assert match_number_format("seventeen") is None
def test_zero_denominator_slash_refused():
# Regex matches, but parser raises ZeroDivisionError → None
assert match_number_format("1/0") is None
def test_zero_denominator_mixed_refused():
assert match_number_format("1 1/0") is None

View file

@ -0,0 +1,220 @@
"""ADR-0128 sub-phase 0128.1 — pack ratification invariants.
Each invariant from the ADR §"Ratification invariants" is its own test.
The pack is canonical for English linguistic quantity forms; any drift
must be intentional and ratified, not accidental.
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import pytest
from language_packs.numerics_loader import (
QUANTIFIER_SEMANTIC_TYPES,
PACK_ROOT,
_index,
lookup_cardinal,
lookup_fraction,
lookup_ordinal,
number_format_entries,
)
EXPECTED_CARDINALS_0_20 = [
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
"sixteen", "seventeen", "eighteen", "nineteen", "twenty",
]
EXPECTED_TENS = ["thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]
EXPECTED_MAGNITUDES = ["hundred", "thousand", "million", "billion"]
EXPECTED_ORDINALS_1_31 = [
"first", "second", "third", "fourth", "fifth", "sixth", "seventh",
"eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth",
"fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth",
"nineteenth", "twentieth", "twenty-first", "twenty-second",
"twenty-third", "twenty-fourth", "twenty-fifth", "twenty-sixth",
"twenty-seventh", "twenty-eighth", "twenty-ninth", "thirtieth",
"thirty-first",
]
EXPECTED_NAMED_FRACTIONS = [
("half", 2), ("third", 3), ("quarter", 4), ("fifth", 5), ("sixth", 6),
("seventh", 7), ("eighth", 8), ("ninth", 9), ("tenth", 10),
]
EXPECTED_IRREGULAR_FRACTIONS = [("sixteenth", 16), ("thirty-second", 32)]
# ---------------------------------------------------------------------------
# Invariant 1: Cardinal exhaustiveness — 0..20 + tens + magnitudes
# ---------------------------------------------------------------------------
def test_cardinal_exhaustiveness_0_to_20():
for i, word in enumerate(EXPECTED_CARDINALS_0_20):
entry = lookup_cardinal(word)
assert entry is not None, f"cardinal {word!r} missing"
assert entry.numeric_value == i
def test_cardinal_tens_complete():
expected = dict(zip(EXPECTED_TENS, range(30, 100, 10)))
for word, val in expected.items():
entry = lookup_cardinal(word)
assert entry is not None, f"tens cardinal {word!r} missing"
assert entry.numeric_value == val
def test_cardinal_magnitudes_complete():
expected = dict(zip(EXPECTED_MAGNITUDES, [100, 1000, 1_000_000, 1_000_000_000]))
for word, val in expected.items():
entry = lookup_cardinal(word)
assert entry is not None, f"magnitude cardinal {word!r} missing"
assert entry.numeric_value == val
# ---------------------------------------------------------------------------
# Invariant 2: Ordinal exhaustiveness — 1st..31st present
# ---------------------------------------------------------------------------
def test_ordinal_exhaustiveness_1_to_31():
for i, word in enumerate(EXPECTED_ORDINALS_1_31, start=1):
entry = lookup_ordinal(word)
assert entry is not None, f"ordinal {word!r} missing"
assert entry.position == i
def test_ordinal_irregular_spellings_preserved():
# The "th" rule is not literal; irregular morphology must be in the pack.
irregular_pairs = {"fifth": 5, "eighth": 8, "ninth": 9, "twelfth": 12}
for word, pos in irregular_pairs.items():
entry = lookup_ordinal(word)
assert entry is not None and entry.position == pos
# ---------------------------------------------------------------------------
# Invariant 3: Named fraction exhaustiveness 1/2..1/10
# ---------------------------------------------------------------------------
def test_fraction_named_1_2_through_1_10():
for word, denom in EXPECTED_NAMED_FRACTIONS:
entry = lookup_fraction(word)
assert entry is not None, f"named fraction {word!r} missing"
assert entry.numerator == 1 and entry.denominator == denom
def test_fraction_irregular_sixteenth_thirty_second():
for word, denom in EXPECTED_IRREGULAR_FRACTIONS:
entry = lookup_fraction(word)
assert entry is not None, f"irregular fraction {word!r} missing"
assert entry.numerator == 1 and entry.denominator == denom
def test_fraction_symbol_forms_present():
symbols = ["½", "¼", "¾", "", "", "", "", "", ""]
for sym in symbols:
entry = lookup_fraction(sym)
assert entry is not None, f"fraction symbol {sym!r} missing"
assert 0 < entry.decimal_value < 1
# ---------------------------------------------------------------------------
# Invariant 5: Quantifier semantic-type completeness (closed set)
# ---------------------------------------------------------------------------
def test_quantifier_semantic_types_are_closed_set():
idx = _index()
assert idx.quantifiers, "no quantifiers loaded"
for entry in idx.quantifiers.values():
assert entry.semantic_type in QUANTIFIER_SEMANTIC_TYPES, (
f"quantifier {entry.surface!r} carries "
f"semantic_type={entry.semantic_type!r} outside closed set"
)
def test_quantifier_indefinite_set_present():
# "wrong == 0" preservation: at least these indefinites must refuse.
for w in ("some", "many", "few", "several", "any"):
idx_entries = _index().quantifiers
assert w in idx_entries
assert idx_entries[w].is_indefinite
def test_quantifier_determinate_paired_both_equals_two():
both = _index().quantifiers["both"]
assert both.semantic_type == "paired"
assert both.determinate_value == 2
# ---------------------------------------------------------------------------
# Invariant 6: Format regex test corpus exists (cross-file gate)
# ---------------------------------------------------------------------------
def test_number_format_rules_present_and_well_formed():
entries = number_format_entries()
assert entries, "no number-format rules loaded"
seen = set()
for fmt in entries:
assert fmt.format_id and fmt.regex and fmt.parser_function
assert fmt.output_type in {"int", "float", "Fraction"}
assert fmt.format_id not in seen, f"duplicate format_id {fmt.format_id}"
seen.add(fmt.format_id)
# ---------------------------------------------------------------------------
# Manifest + mastery report self-seal — SHA-256 of bytes-on-disk
# ---------------------------------------------------------------------------
def _sha256(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def test_manifest_checksum_matches_lexicon_bytes_on_disk():
manifest = json.loads((PACK_ROOT / "manifest.json").read_text(encoding="utf-8"))
lex_sha = _sha256(PACK_ROOT / "lexicon.jsonl")
gloss_sha = _sha256(PACK_ROOT / "glosses.jsonl")
assert manifest["checksum"] == lex_sha
assert manifest["glosses_checksum"] == gloss_sha
assert manifest["pack_id"] == "en_numerics_v1"
assert manifest["gate_engaged"] is True
def test_mastery_report_self_seals():
report_path = PACK_ROOT / "en_numerics_v1.mastery_report.json"
report = json.loads(report_path.read_text(encoding="utf-8"))
# The report's own checksums must match the bytes on disk.
assert report["lexicon_sha256"] == _sha256(PACK_ROOT / "lexicon.jsonl")
assert report["glosses_sha256"] == _sha256(PACK_ROOT / "glosses.jsonl")
assert report["manifest_sha256"] == _sha256(PACK_ROOT / "manifest.json")
assert report["pack_id"] == "en_numerics_v1"
# Counts agree with what the loader can see.
idx = _index()
counts = report["entry_counts"]
assert counts["cardinal"] == len(idx.cardinals)
assert counts["ordinal"] == len(idx.ordinals)
# fractions count includes both spelled and symbol-only entries
assert counts["fraction"] >= len(EXPECTED_NAMED_FRACTIONS)
assert counts["quantifier"] == len(idx.quantifiers)
def test_every_lexicon_entry_has_gloss():
lex = {
json.loads(l)["entry_id"]
for l in (PACK_ROOT / "lexicon.jsonl").read_text("utf-8").splitlines() if l
}
glos = {
json.loads(l)["entry_id"]
for l in (PACK_ROOT / "glosses.jsonl").read_text("utf-8").splitlines() if l
}
missing = lex - glos
assert not missing, f"entries without glosses: {sorted(missing)[:5]}"
def test_entry_ids_unique():
ids = [
json.loads(l)["entry_id"]
for l in (PACK_ROOT / "lexicon.jsonl").read_text("utf-8").splitlines() if l
]
assert len(ids) == len(set(ids)), "duplicate entry_id in lexicon"
@pytest.mark.parametrize("required", [
"pack_id", "language", "role", "checksum", "glosses_checksum",
"version", "gate_engaged", "provenance",
])
def test_manifest_required_fields(required):
manifest = json.loads((PACK_ROOT / "manifest.json").read_text(encoding="utf-8"))
assert required in manifest