From 1e79c0c9e68e80597b077c2046b1f5cc0bda1699 Mon Sep 17 00:00:00 2001 From: Weksa Date: Wed, 4 Nov 2026 09:41:57 +0000 Subject: [PATCH 1/2] Respell Sa'auei'a as Sa'ueia Spelling only, with Deru's permission (2026-11-04 #zyphos): language-design.md (6), README.md (2), state/map.yaml (2) as scoped. Other hits left for a ruling. Co-Authored-By: Claude Sonnet 5.5 --- settings/eusocial-interbeing/README.md | 4 ++-- settings/eusocial-interbeing/language-design.md | 12 ++++++------ state/map.yaml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/settings/eusocial-interbeing/README.md b/settings/eusocial-interbeing/README.md index ceb2502..0b1f400 100644 --- a/settings/eusocial-interbeing/README.md +++ b/settings/eusocial-interbeing/README.md @@ -20,7 +20,7 @@ register, and history. - Weksa owns the language-project pipeline: ontology projection, grammar, morphology, phonology, diachrony, rendering, glossing, and critique. - A future language project owns one inhabitant language. Airawa imperial - register, pre-imperial Airawa registers, Sa'auei'a family-unit speech, ritual + register, pre-imperial Airawa registers, Sa'ueia family-unit speech, ritual breeding-ground registers, and ecological-partner contact systems should not collapse into one universal naming soup. @@ -40,7 +40,7 @@ surfaces: - Airawa continent: mother-tree memory networks, archival dependency, disconnected networks, and the Airawa Empire's biological-memetic infrastructure. -- Sa'auei'a continent: advanced nomadic civilization, family-unit political +- Sa'ueia continent: advanced nomadic civilization, family-unit political movement, sacred breeding-ground commons, remembered routes, and reciprocal ecological relationships. diff --git a/settings/eusocial-interbeing/language-design.md b/settings/eusocial-interbeing/language-design.md index 942f7a9..cacb314 100644 --- a/settings/eusocial-interbeing/language-design.md +++ b/settings/eusocial-interbeing/language-design.md @@ -49,7 +49,7 @@ rights, witnessed warrior promise, and honorable scar evidence. See ### Provisional Airawa Phonology Notes Airawa phonology is not finalized, but the current design pressure distinguishes -it from Sa'auei'a softness. Airawa names should feel sharper, more dorsal and +it from Sa'ueia softness. Airawa names should feel sharper, more dorsal and coronal, and more willing to close around hard edges. Provisional tendencies: @@ -58,9 +58,9 @@ Provisional tendencies: - sibilant and postalveolar fricatives should carry social texture: `s`, `z`, `sh` / `zh`; - `r` may be trilled in some Airawa registers; -- syllables can tolerate clipped or closed shapes where Sa'auei'a forms would +- syllables can tolerate clipped or closed shapes where Sa'ueia forms would flow longer and wetter; -- avoid making native Airawa names read like smooth Sa'auei'a route names unless +- avoid making native Airawa names read like smooth Sa'ueia route names unless the form is explicitly a loan, exonym, or cross-continental register. The current room consensus pushes this further for introductory speech: Airawa @@ -75,9 +75,9 @@ voiced postalveolar fricative /zh/ [ʒ]. The name is being considered for a Zyphos Face drawn from a non-imperial Airawa archival/fungal-road tradition: an archivist shaped by memory as kinship, not content. -## Sa'auei'a Pressures +## Sa'ueia Pressures -Sa'auei'a language work must account for: +Sa'ueia language work must account for: - advanced nomadic civilization without permanent urban dominance - squad-sized family units @@ -88,7 +88,7 @@ Sa'auei'a language work must account for: - reciprocity routes and seasonal corridors - camps, commons, and partner sites as distinct name targets -The Sa'auei'a continent should not read as empty wilderness. Its language should +The Sa'ueia continent should not read as empty wilderness. Its language should have ways to name civilization that moves, recurs, bargains, and is remembered by ecology. diff --git a/state/map.yaml b/state/map.yaml index 95009bf..7d62e94 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -221,10 +221,10 @@ open_questions: - What is the minimal Rust to Dust language-project slice that proves the engine without hard-coding the setting into it? - What minimal Eusocial Interbeing language-project slice can prove naming for - Airawa, Sa'auei'a, ecological partner, and imperial registers without + Airawa, Sa'ueia, ecological partner, and imperial registers without collapsing them into one universal language? - Which Zyphos language project should be first: Airawa, imperial Airawa, - Sa'auei'a, or a narrow naming-brief interlingua? + Sa'ueia, or a narrow naming-brief interlingua? - What is the minimal Airawa starter lexicon and permission model needed to translate the first chieftain/visitor/mother-tree scene honestly? - How should Airawa encode honorable scars: bodily evidence, honor claim, debt From 3db34fb634748da39dd8401ec25f6b4b46a559b2 Mon Sep 17 00:00:00 2001 From: Weksa Date: Wed, 4 Nov 2026 10:49:50 +0000 Subject: [PATCH 2/2] Sa'ueia workshop first cut: table, checker, sound proposal Co-Authored-By: Claude Sonnet 5.5 --- .../eusocial-interbeing/language-design.md | 36 +++++++ settings/eusocial-interbeing/sa-ueia/check.py | 102 ++++++++++++++++++ .../eusocial-interbeing/sa-ueia/table.yaml | 81 ++++++++++++++ .../eusocial-interbeing/sa-ueia/test_check.py | 94 ++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 settings/eusocial-interbeing/sa-ueia/check.py create mode 100644 settings/eusocial-interbeing/sa-ueia/table.yaml create mode 100644 settings/eusocial-interbeing/sa-ueia/test_check.py diff --git a/settings/eusocial-interbeing/language-design.md b/settings/eusocial-interbeing/language-design.md index cacb314..9a727ee 100644 --- a/settings/eusocial-interbeing/language-design.md +++ b/settings/eusocial-interbeing/language-design.md @@ -92,6 +92,42 @@ The Sa'ueia continent should not read as empty wilderness. Its language should have ways to name civilization that moves, recurs, bargains, and is remembered by ecology. +## Sa'ueia Sound (Proposal For Deru To Cut) + +Status: proposal. Deru approved the outline in #zyphos on 2026-11-04; only the +species name Sa'ueia is canon. Everything below is a working draft in +`sa-ueia/table.yaml`, each entry marked `deru` or `weksa-placeholder`, and +`sa-ueia/check.py` enforces what is mechanical. + +Taken from Deru's words: + +- five vowels: `a e i o u`; +- no `h`, `q`, `y`; `k` where a `q` might have been; +- short words, soft and open; +- suffixing morphology; the one suffix so far is `-um`, a verb suffix; +- identical letters merge at a morpheme join: `weru` + `um` gives `werum`, + `ila` + `werum` gives `ilawerum` (walk of knowledge); +- a vowel only where one is needed. + +First roots, all Deru's: `ila` walk, `weru` know, `Olune` breeding ground +(unanalyzed). + +Proposed, for Deru to accept or cut: + +- the apostrophe in Sa'ueia is orthographic, and is not counted as a letter; +- "short" means at most 10 letters in a joined word, as a warning only; +- no doubled letter inside a morpheme, so a double can only arise at a join + and the merge rule removes it. + +Open, not guessed: + +- the consonant inventory. Only `l m n r s w` appear in Deru's words so far, + and that is attestation, not an inventory; +- whether the know root is `weru` or `wer-` (both give `werum`); +- syllable shapes, stress, and what "vowel only where needed" means once the + inventory is known (the checker does not test it); +- the phonological role of the apostrophe. + ## Naming Brief Shape A Weksa naming brief should capture at least: diff --git a/settings/eusocial-interbeing/sa-ueia/check.py b/settings/eusocial-interbeing/sa-ueia/check.py new file mode 100644 index 0000000..179c935 --- /dev/null +++ b/settings/eusocial-interbeing/sa-ueia/check.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Sa'ueia workshop checker: join morphemes by table.yaml's rules, flag violations. + +Reads table.yaml next to this file (or a path argument). The consonant inventory +is open, so no consonant is ever flagged; only rules in the table are enforced. +Exit status is 1 when any error is found; warnings do not fail the run. +""" + +from __future__ import annotations + +import sys +from dataclasses import dataclass +from pathlib import Path + +import yaml + +TABLE = Path(__file__).with_name("table.yaml") +PROVENANCES = {"deru", "weksa-placeholder"} + + +@dataclass(frozen=True) +class Finding: + severity: str # "error" | "warning" + where: str + message: str + + def __str__(self) -> str: + return f"{self.severity}: {self.where}: {self.message}" + + +def letters(form: str) -> str: + return form.replace("'", "").lower() + + +def join(morphemes: list[str]) -> str: + """Concatenate, merging one identical letter at each join.""" + out = "" + for m in morphemes: + if out and m and out[-1] == m[0]: + m = m[1:] + out += m + return out + + +def check_form(form: str, rules: dict, where: str, *, inside_morpheme: bool = False) -> list[Finding]: + found = [] + text = letters(form) + for ch in rules["forbidden_letters"]["value"]: + if ch in text: + found.append(Finding("error", where, f"{form!r} contains forbidden letter {ch!r}")) + for a, b in zip(text, text[1:]): + if a == b and (not inside_morpheme or rules["no_doubled_letters_inside_morpheme"]["value"]): + found.append(Finding("error", where, f"{form!r} has doubled {a!r}")) + limit = rules["max_word_letters"] + if not inside_morpheme and len(text) > limit["value"]: + found.append(Finding(limit["severity"], where, f"{form!r} is {len(text)} letters, over {limit['value']}")) + return found + + +def check_table(table: dict) -> list[Finding]: + rules = table["rules"] + found = [] + for r_name, r in rules.items(): + if r.get("provenance") not in PROVENANCES: + found.append(Finding("error", f"rules.{r_name}", "missing or unknown provenance")) + if table["consonants"]["inventory"] != "open": + found.append(Finding("error", "consonants", "inventory is not Deru's to guess; keep it open until given")) + forms: dict[str, str] = {} + for kind in ("roots", "suffixes"): + for entry in table[kind]: + form = entry["form"] + where = f"{kind}.{form}" + if entry.get("provenance") not in PROVENANCES: + found.append(Finding("error", where, "missing or unknown provenance")) + forms[form] = kind + found += check_form(form, rules, where, inside_morpheme=True) + for comp in table["compositions"]: + where = "compositions." + "+".join(comp["morphemes"]) + if comp.get("provenance") not in PROVENANCES: + found.append(Finding("error", where, "missing or unknown provenance")) + for m in comp["morphemes"]: + if m not in forms and not any(m == f2 for f2 in (c["expected"] for c in table["compositions"])): + found.append(Finding("error", where, f"morpheme {m!r} is in no table row or earlier composition")) + got = join(comp["morphemes"]) + if got != comp["expected"]: + found.append(Finding("error", where, f"joins to {got!r}, table says {comp['expected']!r}")) + found += check_form(comp["expected"], rules, where) + return found + + +def main(argv: list[str]) -> int: + path = Path(argv[1]) if len(argv) > 1 else TABLE + findings = check_table(yaml.safe_load(path.read_text(encoding="utf-8"))) + for f in findings: + print(f) + print(f"{sum(f.severity == 'error' for f in findings)} error(s), " + f"{sum(f.severity == 'warning' for f in findings)} warning(s)") + return 1 if any(f.severity == "error" for f in findings) else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/settings/eusocial-interbeing/sa-ueia/table.yaml b/settings/eusocial-interbeing/sa-ueia/table.yaml new file mode 100644 index 0000000..6d83972 --- /dev/null +++ b/settings/eusocial-interbeing/sa-ueia/table.yaml @@ -0,0 +1,81 @@ +# Sa'ueia workshop table, first cut. +# Source: Deru's words in #zyphos, 2026-11-04. Setting owner: Deru (the Zyphos vault). +# Every entry carries provenance: +# deru - said by Deru, or part of the sound outline Deru approved +# weksa-placeholder - Weksa's stand-in so the checker can run; Deru may cut it +# Only the species name Sa'ueia is canon. Everything else here is a working +# draft, even when its provenance is deru. + +roots: + - form: ila + gloss: walk + provenance: deru + - form: weru + gloss: know + provenance: deru + note: > + Deru gave "werum" (knowledge, maybe a verb) and was asked whether -um + suffixes wer- or weru. "weru" is the reading under which the join rule + does visible work (weru + um -> werum); wer- gives the same surface form. + Open for Deru. + - form: olune + gloss: breeding ground + provenance: deru + analysis: unanalyzed # no morpheme boundaries known + - form: sa'ueia + gloss: the species name + provenance: deru + canon: true + analysis: unanalyzed + note: Apostrophe is part of the canon spelling; its phonological role is open. + +suffixes: + - form: um + gloss: verb suffix + provenance: deru + +rules: + vowels: + value: [a, e, i, o, u] + provenance: deru + forbidden_letters: + value: [h, q, y] + provenance: deru + note: k is used where q might have been. + join_merge_identical: + value: true + provenance: deru + note: > + When one morpheme ends in the letter the next begins with, they merge to + one letter at the join (weru + um -> werum, ila + werum -> ilawerum). + morphology_direction: + value: suffixing + provenance: deru + short_words: + provenance: deru + checked: false # no number from Deru + max_word_letters: + value: 10 + provenance: weksa-placeholder + severity: warning + note: Stand-in for "short words"; apostrophes not counted. Deru sets the real bound. + no_doubled_letters_inside_morpheme: + value: true + provenance: weksa-placeholder + note: Follows from the merge rule only if doubles never occur inside a morpheme either. + vowel_only_where_needed: + provenance: deru + checked: false # not mechanically decidable without the consonant inventory + +consonants: + inventory: open # Deru has not given it. The checker does not guess. + seen_in_deru_words: [l, m, n, r, s, w] # attested above, not an inventory + +# Joined forms the checker must reproduce. Each: morphemes in order, expected surface. +compositions: + - morphemes: [weru, um] + expected: werum + provenance: deru # "werum" is Deru's form + - morphemes: [ila, werum] + expected: ilawerum + provenance: deru # "Ilawerum", walk of knowledge diff --git a/settings/eusocial-interbeing/sa-ueia/test_check.py b/settings/eusocial-interbeing/sa-ueia/test_check.py new file mode 100644 index 0000000..790d689 --- /dev/null +++ b/settings/eusocial-interbeing/sa-ueia/test_check.py @@ -0,0 +1,94 @@ +import copy +import unittest + +import yaml + +import check + + +def table(): + return yaml.safe_load(check.TABLE.read_text(encoding="utf-8")) + + +def errors(t): + return [f for f in check.check_table(t) if f.severity == "error"] + + +class JoinTests(unittest.TestCase): + def test_identical_letters_merge_at_join(self): + self.assertEqual(check.join(["weru", "um"]), "werum") + self.assertEqual(check.join(["ila", "werum"]), "ilawerum") + + def test_merge_is_one_letter_not_all(self): + self.assertEqual(check.join(["ila", "aba"]), "ilaba") + self.assertEqual(check.join(["ila", "ala"]), "ilala") + + def test_different_letters_concatenate(self): + self.assertEqual(check.join(["ila", "um"]), "ilaum") + + def test_merge_applies_only_at_joins(self): + # a doubled letter inside a morpheme is not collapsed by join + self.assertEqual(check.join(["aa", "b"]), "aab") + + +class TableTests(unittest.TestCase): + def test_shipped_table_is_clean(self): + self.assertEqual(errors(table()), []) + + def test_forbidden_letter_in_root_flagged(self): + t = table() + t["roots"].append({"form": "haru", "gloss": "x", "provenance": "weksa-placeholder"}) + self.assertTrue(any("forbidden letter 'h'" in str(f) for f in errors(t))) + + def test_q_and_y_flagged(self): + for ch in "qy": + t = table() + t["roots"].append({"form": f"a{ch}a", "gloss": "x", "provenance": "deru"}) + self.assertTrue(any(f"forbidden letter '{ch}'" in str(f) for f in errors(t)), ch) + + def test_k_allowed(self): + t = table() + t["roots"].append({"form": "kela", "gloss": "x", "provenance": "weksa-placeholder"}) + self.assertEqual(errors(t), []) + + def test_missing_or_unknown_provenance_flagged(self): + for bad in (None, "guess"): + t = table() + t["suffixes"][0]["provenance"] = bad + self.assertTrue(any("provenance" in str(f) for f in errors(t)), bad) + + def test_unmerged_composition_flagged(self): + t = table() + t["compositions"][0]["expected"] = "weruum" + msgs = [str(f) for f in errors(t)] + self.assertTrue(any("joins to 'werum'" in m for m in msgs)) + self.assertTrue(any("doubled 'u'" in m for m in msgs)) + + def test_doubled_letter_inside_morpheme_flagged(self): + t = table() + t["roots"].append({"form": "ilaa", "gloss": "x", "provenance": "weksa-placeholder"}) + self.assertTrue(any("doubled 'a'" in str(f) for f in errors(t))) + + def test_unknown_morpheme_in_composition_flagged(self): + t = table() + t["compositions"].append({"morphemes": ["ila", "zo"], "expected": "ilazo", "provenance": "weksa-placeholder"}) + self.assertTrue(any("'zo'" in str(f) for f in errors(t))) + + def test_long_word_is_warning_not_error(self): + t = table() + t["compositions"].append({"morphemes": ["ila", "werum", "ila", "werum"], "expected": "ilawerumilawerum", + "provenance": "weksa-placeholder"}) + self.assertEqual(errors(t), []) + self.assertTrue(any(f.severity == "warning" for f in check.check_table(t))) + + def test_consonant_inventory_must_stay_open(self): + t = table() + t["consonants"]["inventory"] = ["l", "m"] + self.assertTrue(any("inventory" in str(f) for f in errors(t))) + + def test_apostrophe_is_not_a_letter(self): + self.assertEqual(check.letters("Sa'ueia"), "saueia") + + +if __name__ == "__main__": + unittest.main()