Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
d043281a18 Merge pull request 'Sa'ueia workshop first cut: table, checker, sound proposal' (#5) from weksa/sa-ueia-workshop into main 2026-11-04 12:56:50 +00:00
Weksa
3db34fb634 Sa'ueia workshop first cut: table, checker, sound proposal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:49:50 +00:00
813509f250 Merge pull request 'Respell Sa'auei'a as Sa'ueia (spelling only)' (#4) from weksa/sa-ueia-spelling into main 2026-11-04 10:48:37 +00:00
Weksa
1e79c0c9e6 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 <noreply@anthropic.com>
2026-11-04 09:41:57 +00:00
157892a95a Merge pull request 'speaker-state onto main (stance in packet, admit-only LoweringInput) + build notes' (#3) from weksa-lowering/speaker-state-on-main into main 2026-11-03 19:27:33 +00:00
6 changed files with 323 additions and 10 deletions

View file

@ -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.

View file

@ -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,10 +88,46 @@ 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.
## 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:

View file

@ -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))

View file

@ -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

View file

@ -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()

View file

@ -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