405 lines
15 KiB
Python
405 lines
15 KiB
Python
"""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 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 / "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()
|