@heroiclands/package-build 23.7.0 → 23.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -451,29 +451,32 @@ These fields are accepted by every note type. A field's value can still be irrel
451
451
 
452
452
  **Subtypes:** `social`, `nature`, `craft`, `lore`, `language`, `script`, `mystical`, `physical`, `combat`, `combattechnique`.
453
453
 
454
- | Field | Shape | Meaning |
455
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
456
- | `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
457
- | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
458
- | `data.masteryLevel` | number | Mastery before any modifier. |
459
- | `data.parentSkill` | an Address | The skill this one specialises. |
460
- | `data.names` | a language's name lists | How this language's speakers are named, for `package-build npc new`: `pattern` builds a name from placeholders such as "{given} {clan}" and literal text, `given` maps a gender to a list of given names, `female` and `male` required, and every other key is a list of names a placeholder of the same name draws from. A language skill note only. See [name lists](format-details.md#name-lists). |
461
- | `data.family` | an Address | The parent language, as a language skill note: this language reads the parent's words, and its parents' in turn, as its own. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
462
- | `data.abbreviation` | a string, such as `Prov.` | The tag a dictionary prints for the language, beside an entry's class and in an etymology, as `Prov.`; the language's full name where unstated. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
463
- | `data.classModifiers` | list of word class modifiers | The class modifiers the language uses, from the master word classes. A word's class may carry only these. Empty when unstated. A language skill note only. |
464
- | `data.fields` | list of `{ name, holds }` | The fields the language's words are grouped under. A language skill note only. |
465
- | `data.words` | list of `{ form, class, field, gloss, … }` | The language's words, one map each, in the order they print. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
466
- | `data.affixes` | list of `{ form, gloss, attaches?, examples? }` | The affixes a word's `built` pieces may name, each with its gloss. A language skill note only. |
467
- | `data.tongues` | list of `{ tag, meaning, language? }` | The tags a register row's `tongue` may name, each with what it means. A language skill note only. |
468
- | `data.register` | list of `{ name, tongue?, kind?, built?, why? }` | Every name the setting gives in the language's reach, with its tongue and pieces; the notes carrying each name are derived. A language skill note only. |
469
- | `data.retired` | list of `{ form, instead?, why }` | The forms the setting does not write, and why. A language skill note only. |
470
- | `data.scope` | a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }` | The notes the register covers. A language skill note only. |
471
- | `data.sounds` | a map — `{ letters?, edges?, clusters?, stress?, finals?, syllables?, joining?, joins?, marks?, skeleton?, frames?, readPast? }` | The letters the language writes and the rules its words keep. A language skill note only. |
472
- | `data.soundChanges` | list of `{ name, from?, to?, steps }` | Named sets of ordered changes, each making one tongue's words from another's. A language skill note only. |
473
- | `data.elements` | list of `{ form, variants?, position?, sense, examples? }` | The elements the language's names are built from. A language skill note only. |
474
- | `data.roots` | list of `{ root, sense, field? }` | The consonantal roots the language's words are set from. A language skill note only. |
475
- | `data.cognates` | list of `{ forms, proto?, gloss }` | Sets of forms one older word takes in several languages, stated on the eldest. A language skill note only. |
476
- | `data.callingNames` | a map — `{ closings, minSyllables }` | The short name a long given name is called by. A language skill note only. |
454
+ | Field | Shape | Meaning |
455
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
456
+ | `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
457
+ | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
458
+ | `data.masteryLevel` | number | Mastery before any modifier. |
459
+ | `data.parentSkill` | an Address | The skill this one specialises. |
460
+ | `data.names` | a language's name lists | How this language's speakers are named, for `package-build npc new`: `pattern` builds a name from placeholders such as "{given} {clan}" and literal text, `given` maps a gender to a list of given names, `female` and `male` required, and every other key is a list of names a placeholder of the same name draws from. A language skill note only. See [name lists](format-details.md#name-lists). |
461
+ | `data.family` | an Address | The parent language, as a language skill note: this language reads the parent's words, and its parents' in turn, as its own. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
462
+ | `data.abbreviation` | a string, such as `Prov.` | The tag a dictionary prints for the language, beside an entry's class and in an etymology, as `Prov.`; the language's full name where unstated. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
463
+ | `data.classModifiers` | list of word class modifiers | The class modifiers the language uses, from the master word classes. A word's class may carry only these. Empty when unstated. A language skill note only. |
464
+ | `data.fields` | list of `{ name, holds }` | The fields the language's words are grouped under. A language skill note only. |
465
+ | `data.words` | list of `{ form, class, field, gloss, … }` | The language's words, one map each, in the order they print. A language skill note only. See [a language's words](format-details.md#a-languages-words). |
466
+ | `data.affixes` | list of `{ form, gloss, attaches?, examples? }` | The affixes a word's `built` pieces may name, each with its gloss. A language skill note only. |
467
+ | `data.tongues` | list of `{ tag, meaning, language? }` | The tags a register row's `tongue` may name, each with what it means. A language skill note only. |
468
+ | `data.register` | list of `{ name, tongue?, kind?, built?, gloss?, why? }` | Every name the setting gives in the language's reach, with its tongue and pieces; the notes carrying each name are derived. A language skill note only. |
469
+ | `data.retired` | list of `{ form, instead?, why }` | The forms the setting does not write, and why. A language skill note only. |
470
+ | `data.scope` | a map — `{ folders?, cultures?, lore?, parents?, addresses?, paths?, types?, skip? }` | The notes the register covers. A language skill note only. |
471
+ | `data.sounds` | a map — `{ letters?, edges?, clusters?, stress?, finals?, syllables?, joining?, joins?, marks?, skeleton?, frames?, readPast?, mutations? }` | The letters the language writes and the rules its words keep. A language skill note only. |
472
+ | `data.soundChanges` | list of `{ name, from?, to?, takes?, steps }` | Named sets of ordered changes, each making one tongue's words from another's. A language skill note only. |
473
+ | `data.elements` | list of `{ form, variants?, position?, kind?, sense, examples?, word?, genitive? }` | The elements the language's names are built from. A language skill note only. |
474
+ | `data.roots` | list of `{ root, sense, field? }` | The consonantal roots the language's words are set from. A language skill note only. |
475
+ | `data.cognates` | list of `{ forms, proto?, gloss }` | Sets of forms one older word takes in several languages, stated on the eldest. A language skill note only. |
476
+ | `data.callingNames` | a map — `{ closings, minSyllables }` | The short name a long given name is called by. A language skill note only. |
477
+ | `data.older` | list of `{ form, gloss?, layer? }` | The words and names older than the language's rules, which no sound or name rule holds. A language skill note only. |
478
+ | `data.negation` | a map — `{ particle, mutation? }` | How a verb is negated. A language skill note only. |
479
+ | `data.naming` | a map — `{ shapes?, edges?, draws?, heldFrom?, nearName? }` | The shapes, edges and sources the language's names keep beyond its words' rules. A language skill note only. |
477
480
 
478
481
  **Inner keys** — a key a field's value does not declare is an error at its own line and column.
479
482
 
@@ -499,13 +502,18 @@ These fields are accepted by every note type. A field's value can still be irrel
499
502
  | `data.words[].tones` | a string | no | A tone pattern, such as `LHL`. |
500
503
  | `data.words[].root` | a string | no | A consonantal skeleton, such as `d-l-k`. |
501
504
  | `data.words[].frame` | a frame of the language's `sounds.frames` | no | The frame a root is set in, named in the language's `sounds.frames`. |
502
- | `data.affixes[].form` | the affix as written, such as `-rith` | yes | The affix as a word's `built` names it. |
505
+ | `data.affixes[].form` | the affix as written, such as `-rith` | no | The affix as a word's `built` names it; absent on a zero affix. |
506
+ | `data.affixes[].zero` | `true` | no | The affix is written as nothing, such as a case with no ending; it states no `form`, prints as `∅`, and no `built` piece names it. |
507
+ | `data.affixes[].kind` | one of case, derivation, tense, aspect, mood, voice, name, lineage | no | What the affix makes. |
508
+ | `data.affixes[].given` | one of man, woman, either | no | Who a given name closing on it belongs to; an affix of kind `name` only. |
509
+ | `data.affixes[].where` | a string, such as `before a stop` | no | Where this form stands, for an affix written two ways; everywhere where absent. |
503
510
  | `data.affixes[].gloss` | a string | yes | What the affix means. |
504
511
  | `data.affixes[].attaches` | a string | no | How it attaches, in words, such as `replaces a closing vowel`. |
505
512
  | `data.affixes[].replaces` | `vowel`, or the letters, such as `a` | no | What it takes the place of at the close of the stem: `vowel` for any closing vowel, or the letters it replaces; added to the whole stem where absent. |
506
513
  | `data.affixes[].lengthens` | `true` or `false` | no | Whether the stem's last vowel lengthens before the affix; `false` where unstated. An affix that `replaces` the stem's close does not also lengthen it. |
507
514
  | `data.affixes[].front` | the affix as written, such as `-ür` | no | Its form after front vowels, where the language keeps vowel harmony. |
508
515
  | `data.affixes[].examples` | a list of strings | no | The names the affix is read from. |
516
+ | `data.affixes[].barredAfter` | a list of sounds | no | The sounds a stem may not close on to take the affix. |
509
517
  | `data.affixes[].tones` | a tone pattern, such as `L` | no | The tone pattern the affix carries, written as a word's `tones` is. |
510
518
  | `data.tongues[].tag` | a tag such as `dunhari`, or `exonym:<tongue>` | yes | The tag a register row's `tongue` names: lowercase letters and digits, or a tag closing on `:<tongue>`, which declares one tag per tongue named after the colon. |
511
519
  | `data.tongues[].meaning` | a string | yes | What a name of this tongue is. |
@@ -514,6 +522,7 @@ These fields are accepted by every note type. A field's value can still be irrel
514
522
  | `data.register[].tongue` | a tag of `data.tongues`, or `translation` | no | The tongue the name is in, as a tag `data.tongues` declares, or `translation`. A row states a `tongue`, a `kind`, or both. |
515
523
  | `data.register[].kind` | one of byname, given, place | no | What kind of name it is, in a language that sorts its names by kind. |
516
524
  | `data.register[].built` | a list of pieces | no | The pieces the name is built from, in order, as a word's `built` writes them. |
525
+ | `data.register[].gloss` | a string | no | What the name means, where the pages give its sense. |
517
526
  | `data.register[].why` | a string | no | Why the name stands outside the pattern the language's names are built by. |
518
527
  | `data.retired[].form` | a string | yes | The form the setting does not write. |
519
528
  | `data.retired[].instead` | a string, or a list of them | no | What is written in its place, one form or several; absent where nothing is. |
@@ -529,13 +538,18 @@ These fields are accepted by every note type. A field's value can still be irrel
529
538
  | `data.sounds.letters` | list of `{ letters, kind, like, tongue? }` | no | The letters the language writes, and the sounds they stand for. |
530
539
  | `data.sounds.letters[].letters` | a list of letters | yes | The letters or digraphs the row describes, as written. |
531
540
  | `data.sounds.letters[].kind` | a string, such as `consonant` or `long vowel` | yes | What kind of sound they write. |
532
- | `data.sounds.letters[].like` | a string | yes | What they sound like. |
541
+ | `data.sounds.letters[].like` | a string | no | What they sound like; absent where the row states only their kind and place. |
542
+ | `data.sounds.letters[].place` | a string, such as `lips` or `tongue-tip` | no | Where in the mouth they are made. |
533
543
  | `data.sounds.letters[].tongue` | a tag, or a list of them | no | The tongues that write them, as tags `data.tongues` declares; every one where absent. |
534
544
  | `data.sounds.edges` | list of `{ edge, sounds }` | no | The sounds that may stand at each edge of a word, one entry per edge. |
535
545
  | `data.sounds.edges[].edge` | one of openConsonant, openCluster, closeConsonant, closeCluster, innerCluster, vowelPair, glide, syllabic | yes | Which edge of a word the sounds stand at. |
536
546
  | `data.sounds.edges[].sounds` | a list of sounds, or `any` | yes | The sounds, as written; `any` for every one, and an empty list for none. |
537
- | `data.sounds.clusters` | a map — `{ max }` | no | How many consonants may stand together. |
547
+ | `data.sounds.clusters` | a map — `{ max, groups? }` | no | How many consonants may stand together. |
538
548
  | `data.sounds.clusters.max` | a whole number | yes | The most consonants that stand together inside a word. |
549
+ | `data.sounds.clusters.groups` | list of `{ what, clusters, use }` | no | The clusters that may stand inside a word, in groups; a language stating them states no `innerCluster` edge. |
550
+ | `data.sounds.clusters.groups[].what` | a string, such as `a nasal and a fricative` | yes | What the clusters of the group are made of. |
551
+ | `data.sounds.clusters.groups[].clusters` | a list of clusters | yes | The clusters, as written; a digraph counts as one consonant. |
552
+ | `data.sounds.clusters.groups[].use` | one of common, sparing | yes | How often the group's clusters stand inside a word. |
539
553
  | `data.sounds.stress` | one of first syllable, penultimate or final, by weight, as marked | no | Where a word's stress falls. |
540
554
  | `data.sounds.finals` | list of `{ final, like }` | no | The syllable finals, for a language whose syllables are parsed by them. |
541
555
  | `data.sounds.finals[].final` | a final as written, such as `ang` | yes | The final. |
@@ -550,11 +564,12 @@ These fields are accepted by every note type. A field's value can still be irrel
550
564
  | `data.sounds.syllables.finalVowel` | `true` or `false` | no | Whether a word may close on a vowel; `true` where unstated. |
551
565
  | `data.sounds.syllables.neverInitial` | a list of letters | no | The letters that never open a word. |
552
566
  | `data.sounds.syllables.signature` | a list of letters | no | The letters that mark a word as the language's own. |
553
- | `data.sounds.syllables.harmony` | a map — `{ back, front, neutral?, neutralAs? }` | no | The vowel harmony a word keeps. |
567
+ | `data.sounds.syllables.harmony` | a map — `{ back, front, neutral?, neutralAs?, partners? }` | no | The vowel harmony a word keeps. |
554
568
  | `data.sounds.syllables.harmony.back` | a list of vowels | yes | The back vowels. |
555
569
  | `data.sounds.syllables.harmony.front` | a list of vowels | yes | The front vowels. |
556
570
  | `data.sounds.syllables.harmony.neutral` | a list of vowels | no | The vowels of neither set. |
557
571
  | `data.sounds.syllables.harmony.neutralAs` | one of back, front | no | The set a word of neutral vowels alone counts as. |
572
+ | `data.sounds.syllables.harmony.partners` | a map of vowels to front vowels | no | The front vowel each vowel turns to in an affix's front form, keyed by the vowel; every key is a vowel of the harmony and every value one of its `front` vowels. |
558
573
  | `data.sounds.joining` | list of `{ first, second, written, example? }` | no | How two parts are written where the first closes and the second opens on the given sounds. |
559
574
  | `data.sounds.joining[].first` | a list of sounds | yes | The sounds the first part closes on. |
560
575
  | `data.sounds.joining[].second` | a list of sounds | yes | The sounds the second part opens on. |
@@ -583,9 +598,15 @@ These fields are accepted by every note type. A field's value can still be irrel
583
598
  | `data.sounds.readPast[].written` | a string | yes | A mark or spelling the pages write. |
584
599
  | `data.sounds.readPast[].readAs` | a string | no | What a word is read with in its place; absent where it is read as nothing. |
585
600
  | `data.sounds.readPast[].what` | a string | yes | What the mark or spelling is. |
601
+ | `data.sounds.mutations` | list of `{ mutation, sound, becomes, example? }` | no | The sounds a word changes where a named mutation holds, one entry per sound, such as a stop worn to a fricative or a consonant voiced inside a compound. |
602
+ | `data.sounds.mutations[].mutation` | a string, such as `wearing` | yes | The mutation's name; its rows share it, and `data.negation` names it. |
603
+ | `data.sounds.mutations[].sound` | a sound as written, such as `p` | yes | The sound as it stands where the mutation does not hold; once per mutation. |
604
+ | `data.sounds.mutations[].becomes` | a sound as written, such as `v` | yes | What the sound becomes where the mutation holds. |
605
+ | `data.sounds.mutations[].example` | a string | no | A word that shows it. |
586
606
  | `data.soundChanges[].name` | a string, such as `From Helonic to Byzarian` | yes | The set's name, which no other set of the note shares. |
587
607
  | `data.soundChanges[].from` | a language skill Address | no | The language whose words the steps start from. |
588
608
  | `data.soundChanges[].to` | a language skill Address | no | The language whose words the steps make. |
609
+ | `data.soundChanges[].takes` | a case, such as `accusative` | no | The form of the older word the steps start from; its citation form where absent. |
589
610
  | `data.soundChanges[].steps` | list of `{ step, change, where? }` | yes | The steps, in the order they apply; no step label stands twice in a set. |
590
611
  | `data.soundChanges[].steps[].step` | a string, such as `A1` or `"1"` | yes | The step's label, in the order the changes apply. |
591
612
  | `data.soundChanges[].steps[].change` | a change in arrow notation, such as `` `ph` > `f`; `kh` > `h` `` | yes | The change, in arrow notation: one or more `` `sources` > `targets` `` rules separated by `;`, or the operation a step names in words. |
@@ -593,8 +614,11 @@ These fields are accepted by every note type. A field's value can still be irrel
593
614
  | `data.elements[].form` | the element as written, such as `arn-` | yes | The element a name is built from. |
594
615
  | `data.elements[].variants` | a list of forms | no | The element's other forms, such as `aldr-` beside `ald-`. |
595
616
  | `data.elements[].position` | one of opening, closing, either, link | no | Where in a name the element stands. |
617
+ | `data.elements[].kind` | one of byname, given, place | no | The kind of name the element builds, in a language that keeps one set of elements per kind; every kind where absent. |
596
618
  | `data.elements[].sense` | a string | yes | What the element means. |
597
619
  | `data.elements[].examples` | a list of names | no | The names the element is read from. |
620
+ | `data.elements[].word` | a form of the language's words | no | The word the element is the stem of, a form of `data.words` of the language or an ancestor. |
621
+ | `data.elements[].genitive` | the element as written, such as `-borgar` | no | The element in the genitive, where a name governing another word takes it. |
598
622
  | `data.roots[].root` | a consonantal skeleton, such as `d-l-k` | yes | The root. |
599
623
  | `data.roots[].sense` | a string | yes | What the root means. |
600
624
  | `data.roots[].field` | a string, such as `stone` | no | The field the root belongs to. |
@@ -606,6 +630,38 @@ These fields are accepted by every note type. A field's value can still be irrel
606
630
  | `data.callingNames.closings[].doubled` | `true` | no | The calling name doubles the first syllable rather than taking an ending. |
607
631
  | `data.callingNames.closings[].usual` | one of man, woman, either | yes | Whose name it usually makes. |
608
632
  | `data.callingNames.minSyllables` | a whole number | yes | The fewest syllables a given name has that the pages give a calling name. |
633
+ | `data.older[].form` | a form as the setting writes it | yes | A word or name whose shape the language's rules do not make. |
634
+ | `data.older[].gloss` | a string | no | What it means. |
635
+ | `data.older[].layer` | a string, such as `Primordial` | no | The historical layer it belongs to. |
636
+ | `data.negation.particle` | a form of the language's words | yes | The particle that stands before a verb to negate it, a form of `data.words` of the language or an ancestor. |
637
+ | `data.negation.mutation` | a mutation of `data.sounds.mutations` | no | The mutation the negated verb takes, named in `data.sounds.mutations`. |
638
+ | `data.naming.shapes` | list of `{ of, syllables?, perVowel? }` | no | How long each class of name runs. |
639
+ | `data.naming.shapes[].of` | one of name, given, clan, bound, compound | yes | The class of name the shape holds for; once per class. |
640
+ | `data.naming.shapes[].syllables` | a map — `{ min, max }` | no | How many syllables a name of the class runs to. |
641
+ | `data.naming.shapes[].syllables.min` | a whole number | yes | The fewest syllables a name of the class runs to. |
642
+ | `data.naming.shapes[].syllables.max` | a whole number | yes | The most syllables a name of the class runs to; never below `min`. |
643
+ | `data.naming.shapes[].perVowel` | a map — `{ min, max }` | no | How many consonants a name of the class carries to each vowel; a shape states this, `syllables`, or both. |
644
+ | `data.naming.shapes[].perVowel.min` | a number | yes | The fewest consonants a name of the class carries to each vowel. |
645
+ | `data.naming.shapes[].perVowel.max` | a number | yes | The most consonants a name of the class carries to each vowel; never below `min`. |
646
+ | `data.naming.edges` | list of `{ edge, sounds, vowel?, of? }` | no | The sounds a name may open and close on, where names keep edges ordinary words do not; an edge stands once for each class of name. |
647
+ | `data.naming.edges[].edge` | one of openConsonant, openCluster, closeConsonant, closeCluster, innerCluster, vowelPair, glide, syllabic | yes | Which edge of a name the sounds stand at. |
648
+ | `data.naming.edges[].sounds` | a list of sounds, or `any` | yes | The sounds, as written; `any` for every one, and an empty list for none. |
649
+ | `data.naming.edges[].vowel` | `true` or `false` | no | Whether a vowel may stand at the edge too; `false` where unstated. |
650
+ | `data.naming.edges[].of` | one of name, given, clan, bound, compound | no | The class of name the edge holds for; `name`, every name, where absent. |
651
+ | `data.naming.draws` | list of `{ kind, elements }` | no | The element kinds each kind of name is built from. |
652
+ | `data.naming.draws[].kind` | one of byname, given, place | yes | The kind of name; once per kind. |
653
+ | `data.naming.draws[].elements` | a list of element kinds | yes | The kinds of element, as `data.elements` states them, the name is built from. |
654
+ | `data.naming.heldFrom` | list of `{ source, takes, clip?, genitive?, elements? }` | no | What a place name takes first, by what the place is held from. |
655
+ | `data.naming.heldFrom[].source` | a string, such as `a founder` | yes | What the place is held from. |
656
+ | `data.naming.heldFrom[].takes` | one of stem, godName, givenName, element | yes | What stands first: a name-stem in any grade, a god's name, a founder's given name, or one of `elements`. |
657
+ | `data.naming.heldFrom[].clip` | one of first | no | How the name is clipped: `first`, to its first element; whole where absent. |
658
+ | `data.naming.heldFrom[].genitive` | a list of endings | no | The genitive endings, one of which stands at the seam after the name. |
659
+ | `data.naming.heldFrom[].elements` | a list of elements | no | The elements that may stand first, for a row that takes an `element`. |
660
+ | `data.naming.nearName` | a map — `{ minSyllables, examples? }` | no | The near name a long given name carries. |
661
+ | `data.naming.nearName.minSyllables` | a whole number | yes | The fewest syllables a given name has that carries a near name. |
662
+ | `data.naming.nearName.examples` | list of `{ near, given }` | no | Worked near names, each beside its given name. |
663
+ | `data.naming.nearName.examples[].near` | a near name, such as `Anlâ` | yes | The near name. |
664
+ | `data.naming.nearName.examples[].given` | a given name | yes | The given name it is the near name of. |
609
665
 
610
666
  **System mappings**
611
667
 
@@ -221,6 +221,9 @@ types:
221
221
  - roots
222
222
  - cognates
223
223
  - callingNames
224
+ - older
225
+ - negation
226
+ - naming
224
227
  subTypes:
225
228
  - social
226
229
  - nature
@@ -25,6 +25,11 @@
25
25
  * `data.tongues`, `translation` apart: `exonym:vylari` is declared by
26
26
  * `exonym:<tongue>`;
27
27
  * - an edge stands once in `data.sounds.edges`;
28
+ * - a sound stands once in each mutation of `data.sounds.mutations`;
29
+ * - a cluster stands in one group of `data.sounds.clusters.groups`, and a
30
+ * language stating groups states no `innerCluster` edge;
31
+ * - every vowel `data.sounds.syllables.harmony.partners` keys is a vowel of
32
+ * the harmony, and every partner one of its `front` vowels;
28
33
  * - every Address in `data.scope` resolves, and every type it names is a note
29
34
  * type;
30
35
  * - every language a cognate's `forms` names is a language note;
@@ -32,7 +37,15 @@
32
37
  * - a sound-change set's name stands once in the note, a step's label once
33
38
  * in its set, and its `from` and `to` name language notes;
34
39
  * - a frame's classes are bases of the master word classes;
35
- * - a calling-name closing states a `form` or `doubled: true`, not both.
40
+ * - a calling-name closing states a `form` or `doubled: true`, not both;
41
+ * - a form stands once in `data.older`;
42
+ * - the mutation `data.negation` names is one of `data.sounds.mutations`;
43
+ * - in `data.naming`, a class of name has one shape, which states its
44
+ * syllables, its consonants per vowel or both, each range's `min` no
45
+ * higher than its `max`; an edge stands once for each class of name; a kind
46
+ * of name has one set of draws; and a held-from row takes `elements` only
47
+ * where it takes an `element`, and is clipped or given a genitive only
48
+ * where it takes a name.
36
49
  *
37
50
  * @module
38
51
  */
@@ -43,6 +56,9 @@ import { addressKeyOf } from "./culture-reach.mjs";
43
56
  import { AddressEntries } from "./address-values.mjs";
44
57
  import { isAddressTuple, renderAddress } from "./address.mjs";
45
58
  import {
59
+ ANY_NAME,
60
+ HELD_NAMES,
61
+ HELD_TAKES,
46
62
  LANGUAGE_DATA_KEYS,
47
63
  RESERVED_TONGUE,
48
64
  SCOPE_KINDS,
@@ -287,6 +303,80 @@ export function checkLanguageData(note, { index, types } = {}) {
287
303
  else edgeAt.set(row.edge, i);
288
304
  });
289
305
 
306
+ if (isMap(sounds.clusters) && Array.isArray(sounds.clusters.groups))
307
+ listOf(sounds.edges).forEach((row, i) => {
308
+ if (!isMap(row) || row.edge !== "innerCluster") return;
309
+ findings.push(
310
+ finding(
311
+ ["data", "sounds", "edges", i, "edge"],
312
+ "error",
313
+ "`data.sounds.clusters.groups` states the clusters inside a word, so no " +
314
+ "`innerCluster` edge does",
315
+ ),
316
+ );
317
+ });
318
+ const clusterAt = new Map();
319
+ listOf(sounds.clusters?.groups).forEach((group, i) => {
320
+ if (!isMap(group)) return;
321
+ listOf(group.clusters).forEach((cluster, j) => {
322
+ if (typeof cluster !== "string") return;
323
+ if (clusterAt.has(cluster))
324
+ findings.push(
325
+ finding(
326
+ ["data", "sounds", "clusters", "groups", i, "clusters", j],
327
+ "error",
328
+ `the cluster \`${cluster}\` stands twice, first in ` +
329
+ `\`data.sounds.clusters.groups[${clusterAt.get(cluster)}]\``,
330
+ ),
331
+ );
332
+ else clusterAt.set(cluster, i);
333
+ });
334
+ });
335
+
336
+ const mutationAt = new Map();
337
+ listOf(sounds.mutations).forEach((row, i) => {
338
+ if (!isMap(row) || typeof row.mutation !== "string" || typeof row.sound !== "string")
339
+ return;
340
+ const key = JSON.stringify([row.mutation, row.sound]);
341
+ if (mutationAt.has(key))
342
+ findings.push(
343
+ finding(
344
+ ["data", "sounds", "mutations", i, "sound"],
345
+ "error",
346
+ `the sound \`${row.sound}\` stands twice in the mutation \`${row.mutation}\`, ` +
347
+ `first at \`data.sounds.mutations[${mutationAt.get(key)}]\``,
348
+ ),
349
+ );
350
+ else mutationAt.set(key, i);
351
+ });
352
+
353
+ const harmony = isMap(sounds.syllables?.harmony) ? sounds.syllables.harmony : {};
354
+ if (isMap(harmony.partners)) {
355
+ const front = new Set(listOf(harmony.front));
356
+ const vowels = new Set([...listOf(harmony.back), ...front, ...listOf(harmony.neutral)]);
357
+ for (const [vowel, partner] of Object.entries(harmony.partners)) {
358
+ const path = ["data", "sounds", "syllables", "harmony", "partners", vowel];
359
+ if (!vowels.has(vowel))
360
+ findings.push(
361
+ finding(
362
+ path,
363
+ "error",
364
+ `\`${vowel}\` is not a vowel of \`data.sounds.syllables.harmony\``,
365
+ true,
366
+ ),
367
+ );
368
+ else if (typeof partner === "string" && !front.has(partner))
369
+ findings.push(
370
+ finding(
371
+ path,
372
+ "error",
373
+ `the partner \`${partner}\` of \`${vowel}\` is not one of the harmony's ` +
374
+ "`front` vowels",
375
+ ),
376
+ );
377
+ }
378
+ }
379
+
290
380
  listOf(sounds.frames).forEach((row, i) => {
291
381
  if (!isMap(row)) return;
292
382
  listOf(row.class).forEach((base, j) => {
@@ -351,6 +441,151 @@ export function checkLanguageData(note, { index, types } = {}) {
351
441
  );
352
442
  });
353
443
 
444
+ // ── Older forms and negation ─────────────────────────────────────────────
445
+ const olderAt = new Map();
446
+ listOf(data.older).forEach((row, i) => {
447
+ if (!isMap(row) || typeof row.form !== "string") return;
448
+ if (olderAt.has(row.form))
449
+ findings.push(
450
+ finding(
451
+ ["data", "older", i, "form"],
452
+ "error",
453
+ `the form \`${row.form}\` stands twice, first at \`data.older[${olderAt.get(row.form)}]\``,
454
+ ),
455
+ );
456
+ else olderAt.set(row.form, i);
457
+ });
458
+ const negation = isMap(data.negation) ? data.negation : {};
459
+ if (typeof negation.mutation === "string") {
460
+ const named = new Set(
461
+ listOf(sounds.mutations)
462
+ .filter((row) => isMap(row) && typeof row.mutation === "string")
463
+ .map((row) => row.mutation),
464
+ );
465
+ if (!named.has(negation.mutation))
466
+ findings.push(
467
+ finding(
468
+ ["data", "negation", "mutation"],
469
+ "error",
470
+ `the mutation \`${negation.mutation}\` is not one of \`data.sounds.mutations\``,
471
+ ),
472
+ );
473
+ }
474
+
475
+ // ── Naming ───────────────────────────────────────────────────────────────
476
+ const naming = isMap(data.naming) ? data.naming : {};
477
+ /**
478
+ * A finding at the second of two entries sharing a key.
479
+ *
480
+ * @param {string} list - The list under `data.naming`.
481
+ * @param {(row: object) => string|undefined} keyOf - The entry's key.
482
+ * @param {(row: object, first: number) => [Array<string|number>, string]} report
483
+ * - Where the second sits, relative to its entry, and what is said.
484
+ */
485
+ const once = (list, keyOf, report) => {
486
+ const seen = new Map();
487
+ listOf(naming[list]).forEach((row, i) => {
488
+ if (!isMap(row)) return;
489
+ const key = keyOf(row);
490
+ if (key === undefined) return;
491
+ if (seen.has(key)) {
492
+ const [rest, message] = report(row, seen.get(key));
493
+ findings.push(finding(["data", "naming", list, i, ...rest], "error", message));
494
+ } else seen.set(key, i);
495
+ });
496
+ };
497
+ once(
498
+ "shapes",
499
+ (row) => (typeof row.of === "string" ? row.of : undefined),
500
+ (row, first) => [
501
+ ["of"],
502
+ `the class of name \`${row.of}\` has its shape stated twice, first at ` +
503
+ `\`data.naming.shapes[${first}]\``,
504
+ ],
505
+ );
506
+ once(
507
+ "edges",
508
+ (row) =>
509
+ typeof row.edge === "string" ?
510
+ JSON.stringify([row.edge, row.of ?? ANY_NAME])
511
+ : undefined,
512
+ (row, first) => [
513
+ ["edge"],
514
+ `the edge \`${row.edge}\` of \`${row.of ?? ANY_NAME}\` stands twice, first at ` +
515
+ `\`data.naming.edges[${first}]\``,
516
+ ],
517
+ );
518
+ once(
519
+ "draws",
520
+ (row) => (typeof row.kind === "string" ? row.kind : undefined),
521
+ (row, first) => [
522
+ ["kind"],
523
+ `the kind \`${row.kind}\` has its draws stated twice, first at ` +
524
+ `\`data.naming.draws[${first}]\``,
525
+ ],
526
+ );
527
+ listOf(naming.shapes).forEach((row, i) => {
528
+ if (!isMap(row)) return;
529
+ const at = (...rest) => ["data", "naming", "shapes", i, ...rest];
530
+ if (row.syllables == null && row.perVowel == null)
531
+ findings.push(
532
+ finding(
533
+ at(),
534
+ "error",
535
+ `\`data.naming.shapes[${i}]\` states neither \`syllables\` nor \`perVowel\``,
536
+ ),
537
+ );
538
+ for (const key of ["syllables", "perVowel"]) {
539
+ const range = row[key];
540
+ if (!isMap(range) || typeof range.min !== "number" || typeof range.max !== "number")
541
+ continue;
542
+ if (range.min > range.max)
543
+ findings.push(
544
+ finding(
545
+ at(key),
546
+ "error",
547
+ `\`data.naming.shapes[${i}].${key}\` runs from ${range.min} to ${range.max}; ` +
548
+ "its `min` is above its `max`",
549
+ ),
550
+ );
551
+ }
552
+ });
553
+ listOf(naming.heldFrom).forEach((row, i) => {
554
+ if (!isMap(row) || !HELD_TAKES.includes(row.takes)) return;
555
+ const at = (...rest) => ["data", "naming", "heldFrom", i, ...rest];
556
+ const label = `\`data.naming.heldFrom[${i}]\``;
557
+ if (row.takes === "element") {
558
+ if (!listOf(row.elements).length)
559
+ findings.push(
560
+ finding(
561
+ at(),
562
+ "error",
563
+ `${label} takes an \`element\` and names no \`elements\``,
564
+ ),
565
+ );
566
+ } else if (row.elements != null)
567
+ findings.push(
568
+ finding(
569
+ at("elements"),
570
+ "error",
571
+ `\`elements\` lists what a place takes first, and ${label} takes a ` +
572
+ `\`${row.takes}\``,
573
+ true,
574
+ ),
575
+ );
576
+ if (!HELD_NAMES.includes(row.takes))
577
+ for (const key of ["clip", "genitive"])
578
+ if (row[key] != null)
579
+ findings.push(
580
+ finding(
581
+ at(key),
582
+ "error",
583
+ `\`${key}\` shapes a name, and ${label} takes a \`${row.takes}\``,
584
+ true,
585
+ ),
586
+ );
587
+ });
588
+
354
589
  // ── Scope ────────────────────────────────────────────────────────────────
355
590
  const scope = isMap(data.scope) ? data.scope : {};
356
591
  for (const kind of SCOPE_KINDS) {