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:
commit
46c734b7aa
9 changed files with 1851 additions and 0 deletions
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
130
language_packs/data/en_numerics_v1/glosses.jsonl
Normal file
130
language_packs/data/en_numerics_v1/glosses.jsonl
Normal 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)."}
|
||||
130
language_packs/data/en_numerics_v1/lexicon.jsonl
Normal file
130
language_packs/data/en_numerics_v1/lexicon.jsonl
Normal 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"}
|
||||
16
language_packs/data/en_numerics_v1/manifest.json
Normal file
16
language_packs/data/en_numerics_v1/manifest.json
Normal 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"
|
||||
}
|
||||
459
language_packs/numerics_loader.py
Normal file
459
language_packs/numerics_loader.py
Normal 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
|
||||
405
scripts/generate_en_numerics_v1.py
Normal file
405
scripts/generate_en_numerics_v1.py
Normal 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()
|
||||
209
tests/test_adr_0128_loader.py
Normal file
209
tests/test_adr_0128_loader.py
Normal 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") == ()
|
||||
253
tests/test_adr_0128_numeric_formats.py
Normal file
253
tests/test_adr_0128_numeric_formats.py
Normal 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
|
||||
220
tests/test_adr_0128_pack_ratification.py
Normal file
220
tests/test_adr_0128_pack_ratification.py
Normal 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
|
||||
Loading…
Reference in a new issue