Metadata-Version: 2.5
Name: conlanggen
Version: 0.4.3
Summary: A partial-spec conlang generator: specify what you care about, sample the rest.
Project-URL: Homepage, https://github.com/bmckeen1919-lab/conlanggen
Project-URL: Repository, https://github.com/bmckeen1919-lab/conlanggen
Project-URL: Issues, https://github.com/bmckeen1919-lab/conlanggen/issues
Author: Brian McKeen
License: MIT License
        
        Copyright (c) 2026 Brian McKeen
        
        Contact: bmckeen1919@gmail.com
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
        ---
        
        Note on third-party data: this project does not include the Kwesho Kah
        dictionary or grammar, which are the copyrighted work of Kwesho. The Kah
        benchmark reads them from a local cache at runtime and they are not distributed
        here. See README.md.
License-File: LICENSE
Keywords: conlang,linguistics,procedural-generation,worldbuilding
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pre-commit>=3.7; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-pyyaml>=6; extra == 'dev'
Provides-Extra: phoible
Provides-Extra: wals
Description-Content-Type: text/markdown

# conlang

A partial-spec constructed-language generator.

You describe only what you care about — phonology, syllable shapes, a handful of
words — and the generator **samples the rest** from typologically-plausible
defaults. Same spec plus same seed always produces byte-identical output, and
sampled entries can be locked so they survive re-sampling.

```text
spec (YAML) ──▶ generate ──▶ model (canonical JSON) ──▶ render ──▶ dictionary + grammar sketch
                                   │
                                   ├── lint   (machine-checkable invariants)
                                   └── bench  (plausibility scorecard vs. Kah)
```

The model JSON is the single source of truth; the dictionary, grammar sketch,
and any future views are rendered from it.

## Install

```powershell
pip install conlanggen
```

The distribution is `conlanggen`, but the import package and the command are both
`conlang` — `import conlang`, `conlang generate`.

## Quickstart

```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"

# Generate from the example spec, and render a Markdown dictionary.
.\.venv\Scripts\python.exe -m conlang.cli generate --spec spec\example.yaml --render

# Happy with it? Freeze it into an explicit spec so the values stick.
conlang accept --model out\veshtari.json
```

Output lands in `out/`: a model JSON and a rendered `.md`.

```powershell
# Re-lint or re-render an existing model.
conlang lint --model out\veshtari.json
conlang render --model out\veshtari.json --out out\veshtari.md
```

## Translating a fixed story

The toolkit ships a repeatable pipeline for translating the fixed 111-sentence
story *The Quiet Morning* into a **generated** language: `conlang new` → `grow` →
`translate` → `validate-translation` → `extras`. The runbook is
[`docs/pipeline.md`](docs/pipeline.md).

## Writing a spec

Everything is optional. Omitted fields are sampled.

```yaml
name: Veshtari
seed: 42

phonology:
  consonants: [p, t, k, m, n, s, l, r, v, sh, ch]
  vowels: [a, e, i, o, u]

syllables: [CV, CVC, V]   # C = consonant slot, V = vowel slot
word_count: 150

lexicon:                  # pin specific words; they are locked by default
  - gloss: water
    root: vela
```

Accepted keys: `name`, `seed`, `phonology.{consonants,vowels,ipa,orthography}`,
`syllables`, `concepts`, `word_count`, `lexicon`, `affixes`, `onsets`, `codas`,
`forbidden`.

An explicit, unsized `lexicon` is treated as the complete word list (that is
what `accept` writes back); otherwise the built-in concept list is used and
`word_count` trims or extends it.

If your `syllables` include `CCV`/`CCVC`, the generator samples a legal onset
set unless you supply `onsets` yourself, and `accept` records whichever was
used so a frozen spec regenerates identically.

### IPA inventories

By default a symbol *is* its spelling (`sh` is written `sh`). Set
`phonology.ipa: true` to declare the inventory phonemic: each symbol is an IPA
phoneme, and the tool derives a romanised **orthography** for the dictionary.

```yaml
name: Shibo
seed: 11

phonology:
  ipa: true
  consonants: [p, t, k, ʃ, tʃ, m, n, s, l]
  vowels: [a, e, i, o, u]
  orthography:        # optional; corrects or extends the built-in defaults
    tʃ: ch
    ʃ: sh
```

`/ʃ/` spells `sh` and `/tʃ/` spells `ch` from a built-in table; anything absent
spells itself, so a plain Latin inventory is unchanged. `orthography` overrides
the table (set an entry back to its own symbol to cancel a default), and the
resolved map is stored on the inventory. The dictionary and word-form tables
then show a `spelling` column beside the `ipa` one, and `accept` writes the map
back so a frozen spec regenerates identically. Lint warns on a map entry that is
not an inventory symbol, or two symbols that would spell the same thing.

## Freezing a language (`accept`)

A partial spec samples whatever you leave out. Once you like the result,
`conlang accept` writes it back as a **fully-explicit spec**: the phonology,
syllable shapes, constraints, affixes, and every root become pinned entries.
Regenerating from that spec reproduces the same language exactly — sampled
values now survive re-sampling, and changing the seed can no longer disturb
them.

```powershell
conlang generate --spec spec\example.yaml --out out\veshtari.json
conlang accept  --model out\veshtari.json --out spec/veshtari.yaml
conlang generate --spec spec/veshtari.yaml --out out/veshtari.json
```

Two fields legitimately change across the round trip: `origin` becomes
`specified` for every accepted entry (it is pinned now, not sampled), and
`spec_hash` changes because the spec text is different. Everything linguistic
is identical, and `tests/test_invariants.py` asserts exactly that.

## Guarantees and lint rules

Reproducibility is the one **hard** guarantee: `spec + seed ⇒ identical bytes`.
Everything else is reported by `lint` as errors or warnings, and a broken
language is still generated (conlangers want to explore broken states).

| rule | severity | meaning |
| --- | --- | --- |
| `schema` | error/warning | schema version present and known |
| `lexeme.root` | error | lexeme has a non-empty root |
| `phonotactics.lexeme` | error | every root is a legal word |
| `phonotactics.word` | error | every inflected form is a legal word |
| `lexicon.homophones` | warning | two roots collide (Kah has polysemy too) |
| `morphology.paradigm` | warning | a word is missing a form for a category its class realises |
| `morphology.class` | error | a word names an inflection class the language does not define |
| `lexicon.nonempty` | warning | the lexicon is empty |
| `orthography.unknown` | warning | the spelling map names a symbol that is not in the inventory |
| `orthography.collision` | warning | two symbols would spell the same thing |

Every rule above has a test in `tests/test_lint.py`, `tests/test_lint_invariants.py`,
or `tests/test_morphology.py`.

## Phonotactics

A word is legal when its `C`/`V` skeleton can be partitioned into the language's
syllable templates **and** every resulting onset and coda is allowed. Two details
matter more than they look:

- **Constraints are checked while syllabifying, not after.** A word often has
  several valid partitions; `deplango` is `de|plan|go` with legal onsets, and
  also `de|pla|ngo` with an illegal one. Verdict must depend on the phonology,
  not on which partition the search happens to return first.
- **Onsets and codas are compared by segment, not by string.** An onset of
  `ny` + `m` renders as `"nym"` — three characters, two segments. Matching
  strings would silently reject legal words in any language with a digraph.

When a template set admits onset clusters (`CCV`, `CCVC`), the generator samples
a legal onset set and **builds roots from it**, so generated words are legal by
construction rather than by luck. `constraints.onsets` in the model records what
was sampled; an explicit `onsets` in the spec always wins.

Affixes carry **allomorphs**: conditioned surface forms. A single form cannot
serve both consonant-final and vowel-final roots, because the root's coda plus a
consonant-initial affix resyllabify into an onset cluster. When the primary form
would be illegal, `build_words` uses the first alternative that is legal — which
is what real languages do. It is a rare fallback (~2 in 8,500 affixed forms), but
without it those words would be illegal and lint would fail the language.

## The Kah benchmark

Kah (Kwesho's language) is hand-made, so the benchmark does **not** try to
regenerate it. It does two things:

1. **Adapter fixture** — parses the real Kwesho dictionary cache into the model
   schema, proving the schema can represent real data.
2. **Plausibility scorecard** — compares a generated language against Kah.

The scorecard deliberately reports **three separate numbers**, because a word can
fail for two unrelated reasons and collapsing them makes the report unreadable:

| metric | question |
| --- | --- |
| symbol coverage | which Kah sounds does this language have? |
| usable sounds | how many Kah headwords use only sounds this language has? |
| legal shapes | of those, how many fit the phonotactics? |

Scoring the built-in `kah` preset against Kah is circular, so the report says so
explicitly when it detects it. Treat the scorecard as a lever, not a target: the
presets are deliberately small, and a language that only allows `CV` syllables is
a legitimate (if monotonous) language type, not a bug to tune away.

```powershell
conlang bench --spec spec\example.yaml
# or point at a cache explicitly:
conlang bench --spec spec\example.yaml --kah C:\path\to\kah-lexicon.txt
```

Kah's dictionary is Kwesho's copyrighted work and is **never** vendored into this
repository. Without `--kah`, the benchmark looks for the glossary at a per-user
cache path (`%LOCALAPPDATA%\conlang\kah-lexicon.txt` on Windows,
`~/.cache/conlang/kah-lexicon.txt` elsewhere, or `$XDG_CACHE_HOME/conlang/`);
override it with `--kah` or `CONLANG_KAH_LEXICON`.

On the reference machine, the adapter parses **8,153** entries with a **97.8%**
validity rate.

## Evolving a language

A generated model can be the parent of a daughter, evolved by ordered sound
changes. A rule is `old > new` (`new` empty deletes the segment), optionally
conditioned by an environment after `/`. The environment names the context on
each side of the target, with `_` marking the target slot, `#` the word boundary,
and `V`/`C` the vowel/consonant classes:

```powershell
conlang generate --spec spec\example.yaml --out out\proto.json
conlang evolve --model out\proto.json --rule "a > e" --rule "p > f / V_V" --out out\daughter.json
conlang evolve --model out\proto.json --rule "t > d / #_" --rule "s > z / V_" --out out\voiced.json
```

`V_V` is "between vowels", `#_` is word-initial, `_#` is word-final. Rules are
applied left to right so a later rule sees an earlier one's output (feeding).

The daughter is a full model: its inventory gains any new sounds, its lexicon
and affixes are rewritten, and its word forms are rebuilt. It records where it
came from in `lineage`:

```json
"lineage": { "parent": "proto", "rules": ["a > e", "p > f / V_V"] }
```

That is the whole point of the field: the derivation is data, so a daughter can
itself be evolved, and `accept` keeps the chain across a round trip. Mergers
introduced by a change show up as `lexicon.homophones` warnings, never errors.

## Syntax

Each language samples a constituent order (`SOV`, `SVO`, or `VSO`, weighted
toward the first two) and an alignment (`accusative` or `ergative`). Both are
recorded in the model and survive `accept`. A transitive clause is linearised
from the lexicon and inflected: the subject and object take case by alignment
(accusative marks `NOM`/`ACC`; ergative marks `ERG`/`ABS`) and the verb agrees
with the subject:

```python
from conlang.syntax import sample_clause
words, gloss = sample_clause(language)
# e.g. (['pyople', 'klavragprekbra', 'blonyto'], 'water.ERG say.3SG fire.ABS')
```

Case and agreement are applied when the clause is built, not woven into every
word's standing paradigm — a deliberate split, so a noun's form count does not
double again for case.

## Layout

```text
src/conlang/
  model/        canonical pydantic schema (the source of truth)
  sampling/     deterministic RNG, inventory presets, concepts, defaults
  phonology/    tokenization, syllabification, legality checks
  lexicon/      seeded root generation, stable per-item seeding
  morphology/   affixes, inflection classes, and word-form assembly
  soundchange/  ordered sound changes; derive a daughter language
  syntax/       word order, alignment, simple clause linearisation
  render/       Markdown dictionary + grammar sketch
  lint/         machine-checkable invariants
  bench/        Kah adapter + scorecard
  cli.py        thin command-line view over the library
```

## Development

```powershell
.\.venv\Scripts\python.exe -m pytest
.\.venv\Scripts\python.exe -m ruff check .
.\.venv\Scripts\python.exe -m mypy src
```

## Roadmap

Shipped so far:

- **Phonology** — inventory, phonotactics, seeded word generation.
- **IPA inventories** — declare the inventory phonemic (`ipa: true`) and the tool
  derives a romanised orthography alongside the phonemic forms.
- **Morphology** — affix breadth, inflection classes, and stacking (a word can
  carry several categories at once).
- **Diachrony** — `conlang evolve`: ordered sound changes derive a daughter
  language, now with **environments** (`p > f / V_V`, `#`, `_`), and the chain
  recorded in `lineage`.
- **Syntax** — sampled word order and alignment, case marking with subject
  agreement, the **predication tier** (adpositions, noun phrases, intransitives,
  copula/existential), **clause sequencing** (parataxis and conjunctions), and
  **aspect** (progressive/perfect, stacking with tense).
- **Translation pipeline** — `conlang new` / `grow` / `translate` /
  `validate-translation` / `extras`: build a generated language and translate the
  fixed story into it, including a grammar-match **port** from a sibling language.

Next:

- Optional data packs: PHOIBLE (inventories) and WALS (typology) as CC-BY
  adapters behind the built-in defaults.
- Semantics/pragmatics and writing systems (deferred from v1).

## License

MIT (code). Kah dictionary and grammar content remains Kwesho's and is not part
of this repository.