manifests: the consonant classes are inventories, not copies — 14 engines - #750
Merged
Conversation
…ines
The follow-up item assumed these were the vowel problem again. They are not: no two of the 33 are the
same list. "voiceless" is six different sets, "nasal" four, "velar" five — they share a class NAME and
nothing else, because each is that language's own inventory.
The narrowness is load-bearing, which is what settles it. totontepecmixe's NASAL is {m n} while the
engine DOES emit ŋ — post-nasal voicing fires after m/n only. occitan's VOICELESS_PH excludes ʃ while
the engine DOES emit it — intervocalic s→z depends on that. A universal class would change both.
So they go to their MANIFESTS (#746's rule), not to core/ipa.ts, each with the rule it serves and why
it is narrow. core/ipa.ts stays a vowel-class module. Irish and Scottish Gaelic are the clearest case:
their liquids carry the broad ⟨ˠ⟩ / slender ⟨ʲ⟩ marks, so "the liquids" is four phones in a way no
shared class could express.
Left alone: tibetan's Wylie classes (#741 kept the stack grammar with the parser), the VOICELESS sets
in bulgarian/maltese/macedonian (derived from each manifest's DEVOICE map, already single-sourced), and
icelandic (its files are in flight in #749).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
The premise of this PR is that the manifest comment becomes the authoritative explanation of the class, so a comment that is merely plausible is the defect, not a nitpick. All six were wrong on the facts: - santali "stops" was DEAD (STOP bound, never read) and its stated reason was false — "checked" maps ɡ→kʼ and k→kʼ identically, so a checked voiced stop does NOT surface differently. Dropped the key; voicedStops now says the real reason, that only a final VOICED stop is checked at all. - galician velars read backwards: the set is tested against the phone AFTER the nasal (cinco→siŋko), and word-final ⟨n⟩ velarizes through a separate no-following-phone branch. - albanian claimed the sibilant cluster was "the one place" a rising onset is not required; the nasal branch right beside it is a second exception, and it reads the very key being introduced. - scottishgaelic pointed at "the preceding rule" — the consumer is preaspiration. - dutch: the tables it referred to are below, not above, and its "must list exactly what the tables emit" invariant was already false (ʒ is emitted via voicedFinal and deliberately absent). Acting on the invariant would have changed seam degemination, so it now says NOT to. - and I orphaned 12 header comments from their tables again, the same way as in #746. Put back, and checked with the same script that verified that sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
christopherthompson81
force-pushed
the
core-ipa-consonants
branch
from
August 8, 2026 04:19
86b2f1a to
57e4307
Compare
christopherthompson81
added a commit
that referenced
this pull request
Aug 8, 2026
Same orphaning as #746 and #750 — my spellingVowels block landed between the digraph comment and the digraph table. Caught by the check-headers script, which I ran on the follow-up branch rather than this one; running it before pushing is the habit that keeps failing to stick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
christopherthompson81
added a commit
that referenced
this pull request
Aug 8, 2026
* manifests: the four orthographic sets #748 reverted They were named like IPA classes (VOWELS, R_VOWEL) but scan SPELLING, which is why pointing them at core/ipa.ts broke Quechua. #748 reverted them with a comment; this puts them where #746 says they belong, with the trap recorded beside the data: quechua spellingVowels — the -yuq/-niyuq suffix reads the last LETTER; ⟨y⟩ is absent on purpose, being a consonant letter in Quechua but the IPA vowel luo spellingVowels — the gi-/g- elision reads the first letter of the next number word balochi romanVowels — the ROMAN scan, distinct from the Arabic-script vowelLetters beside it fula latinVowels — the letters the Adlam lengthener doubles in adlamToLatin's output quechua gains a manifest.ts for the single parse (quechua.ts imports numbers.ts, so a back-import would cycle) — the abkhaz/georgian pattern. luo and fula already had one; balochi.ts already had DEF. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr * quechua: put the digraphs header back on its own table Same orphaning as #746 and #750 — my spellingVowels block landed between the digraph comment and the digraph table. Caught by the check-headers script, which I ran on the follow-up branch rather than this one; running it before pushing is the habit that keeps failing to stick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
christopherthompson81
added a commit
that referenced
this pull request
Aug 8, 2026
…ass vacuously Review caught the same orphaning as #746/#750/#751 — my three phone classes landed between the Context-free consonants comment and the consonants table. Fourth time. The check script DOES catch it. I ran it before committing, when git diff main...HEAD is empty, so it compared nothing and printed ok. A checker that reports success having checked nothing is the exact defect this session kept finding in the codebase, now in the tool meant to prevent it. Promoted from a scratchpad file to tools/check-manifest-headers.mjs, where comparing zero files EXITS 2 with an explanation instead of passing — the rule this repo already applies to its own gates (manifest-script's count assertion, cyrillic-confusables' expect(size).toBeGreaterThan(5)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr
christopherthompson81
added a commit
that referenced
this pull request
Aug 8, 2026
) * latin: the three function-local phone classes move to the manifest The last of #750's exclusions. VELAR, MUTA and LIQUID were declared INSIDE the scan, so they were also being rebuilt on every word — they are per-word constants, and Latin's own inventories besides: the labiovelars ⟨kʷ ɡʷ⟩ and the Greek-loan aspirates ⟨pʰ tʰ kʰ⟩ are members, so no shared class could stand in for them. The manifest now records what each is FOR, including the one that is not obvious: mutae is wider than velars because muta cum liquida is about obstruent+liquid, not place — it is what makes volucris antepenultimate ˈwɔɫʊkrɪs rather than *wɔˈɫukrɪs. Pure move: la referee identical at 41399/44907 (92.2%). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr * latin: put the consonants header back, and make the check refuse to pass vacuously Review caught the same orphaning as #746/#750/#751 — my three phone classes landed between the Context-free consonants comment and the consonants table. Fourth time. The check script DOES catch it. I ran it before committing, when git diff main...HEAD is empty, so it compared nothing and printed ok. A checker that reports success having checked nothing is the exact defect this session kept finding in the codebase, now in the tool meant to prevent it. Promoted from a scratchpad file to tools/check-manifest-headers.mjs, where comparing zero files EXITS 2 with an explanation instead of passing — the rule this repo already applies to its own gates (manifest-script's count assertion, cyrillic-confusables' expect(size).toBeGreaterThan(5)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
christopherthompson81
added a commit
that referenced
this pull request
Aug 26, 2026
Running the gate from the previous commit over a wider window turned up the exact defect it was written for, sitting in main. amharic.jsonc: the four-line "THE SHARED SYMBOL TIER's data" header ended up above the ORDINALS block. `ordinals` and `words` were inserted between that header and the `symbolTier` key it describes, so a reader following the header reached the ordinals table and `symbolTier` — 60 lines further down — carried no header at all. This is #746/#750/#751/#755 verbatim. The header is moved back onto its key; nothing else changed, and am stays 200/200. Two further hardenings the wide run exposed, both of the script's own stated "refuses to pass vacuously" invariant: - `git show` wrote "exists on disk, but not in <base>" to the terminal for every file when the base predates the data-tree move. Piped, so it reaches the catch instead of the console. - That catch means "no version at base", which is usually a new manifest but is EVERY file across a tree move — and the script then printed "ok" having compared nothing, which is precisely the vacuous pass its docstring says let #755 ship. Comparisons are now counted: an "ok" states how many files it actually compared, and zero-compared exits 2 like the empty-diff branch. Verified all three ways: passes clean on a normal base (35 changed manifests, 35 compared), exits 2 on a pre-move base, and still flags a synthetic key inserted between vowelSigns' header and its table. Full gate 79 languages / 15,800 rows / 0 differ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WuH4pRrB1PAEiG8CCLVR5S
christopherthompson81
added a commit
that referenced
this pull request
Aug 26, 2026
* bho: revive the manifest-header gate, and file the अ/ʌ split Follow-up to #1025, from its own review. ⚠ tools/check-manifest-headers.mjs HAS BEEN DEAD SINCE THE DATA MOVE. It globs `src/languages/**/*.jsonc`; all 184 manifests live under `data/languages/`. The glob matches nothing, so every run takes the "refuses to pass vacuously" branch and exits 2 — loud, so nobody was misled by a false pass, but the guard against a header detached from its key has had zero coverage since #769. That matters here specifically: #1025 was largely a rewrite of four top-level key headers in a manifest. Reviving it exposed a second defect in the same script. It fired on #1025's extended `independentVowels` header — a caveat APPENDED below the original line, still directly above its own key. The script's existing exemption only covers a header replaced outright ("old text gone from the file entirely"), so an extension looks exactly like an orphaning to the `normalized.includes` test. In a real orphaning the old text has moved to a DIFFERENT key, so `now` cannot contain it — that is the discriminator, and it is now the guard. Verified both ways: the gate passes the extended header, and still catches a synthetic insertion between `vowelSigns`' header and its table. FILED, NOT FIXED — ONE PHONEME, TWO SYMBOLS. ⟨अ⟩ ships [ʌ] while `inherentVowel` ships [ə], and the latter's own comment calls [ʌ] the same sound, "folded". अञ्जन reads ʌnd͡ʒən: both symbols, one word. `provenance` compounds it — the declared inventory /i e ɛ a ʌ ɔ o u/ contains ʌ and not ə, so neither value matches the file's own statement of the system. Not cosmetic: ʌ is in 99 of the 200 golden rows, ə in all 200. Every non-derived Devanagari sibling writes ə (hi/mr/awa/mai/hne); only magahi.jsonc, derived from this file, copies the ʌ. The direction is genuinely unobvious — folding ʌ→ə matches the inherent vowel and the siblings, folding ə→ʌ matches the declared inventory — and the referee that decides it is not in the repo. Also recorded: `provenance` ends "Single published source → ." — a generator dropped the confidence grade after the arrow, and magahi.jsonc has the same wound. Marked as truncated rather than reconstructed; guessing a confidence grade is worse than showing one is missing. csharp/STATUS.md is deliberately untouched — four ports are in flight and the brief reserves that file. Comment- and string-only: bho 200/200, full gate 79 languages / 15,800 rows / 0 differ, 5,397 vitest, 693 dotnet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WuH4pRrB1PAEiG8CCLVR5S * tools: the revived header gate finds a live orphaning in amharic.jsonc Running the gate from the previous commit over a wider window turned up the exact defect it was written for, sitting in main. amharic.jsonc: the four-line "THE SHARED SYMBOL TIER's data" header ended up above the ORDINALS block. `ordinals` and `words` were inserted between that header and the `symbolTier` key it describes, so a reader following the header reached the ordinals table and `symbolTier` — 60 lines further down — carried no header at all. This is #746/#750/#751/#755 verbatim. The header is moved back onto its key; nothing else changed, and am stays 200/200. Two further hardenings the wide run exposed, both of the script's own stated "refuses to pass vacuously" invariant: - `git show` wrote "exists on disk, but not in <base>" to the terminal for every file when the base predates the data-tree move. Piped, so it reaches the catch instead of the console. - That catch means "no version at base", which is usually a new manifest but is EVERY file across a tree move — and the script then printed "ok" having compared nothing, which is precisely the vacuous pass its docstring says let #755 ship. Comparisons are now counted: an "ok" states how many files it actually compared, and zero-compared exits 2 like the empty-diff branch. Verified all three ways: passes clean on a normal base (35 changed manifests, 35 compared), exits 2 on a pre-move base, and still flags a synthetic key inserted between vowelSigns' header and its table. Full gate 79 languages / 15,800 rows / 0 differ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WuH4pRrB1PAEiG8CCLVR5S * tools: decide the vacuous-pass case before anything prints "ok" Self-review of the previous commit: the zero-compared guard ran AFTER the console.log, so the pre-move-base run printed "ok — 184 changed manifest(s), 0 compared…" and only then errored. A gate that says ok on its way to exit 2 is worse than one that just says ok. Moved above the log, and the message now names the actual cause and the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WuH4pRrB1PAEiG8CCLVR5S --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The third residual from #748. The plan said "consolidate the 33 consonant classes into
core/ipa.ts". The triage says don't — and that's the main content of this PR.Why not
No two of them are the same list.
voicelessis six different sets,nasalfour,velarfive — they share a class name and nothing else:p t k f s ʃ t͡s t͡ʃ c x· icelandicp t k s θ f h c· luxembourgishp t k s ʃ f χ t͡s t͡ʃ· occitanp t k f s· romanian (obstruent, both voicings) · tibetan (Wylie tokens, not IPA)ɡ k kʰ ks· galiciank ɡ· latink ɡ kʷ ɡʷ kʰ· totontepecmixek ɡ ɣ· mindong (romanization finals)l r· irishɾˠ ɾʲ l̪ˠ lʲ· latinl ɫ r· scottishgaelicl̪ˠ lʲ rˠ ɾʲ· tibetan (Wylie)m n ɲ ŋ· chuvashn m ŋ j· totontepecmixem n· turkmenm n ŋThat contrast with the vowels is the whole point: 31 engines held near-identical copies of one universal alphabet, so sharing was obviously right. Here each list is the language's own inventory.
And the narrowness is load-bearing, which is what settles it rather than taste:
totontepecmixeNASAL is{m, n}— but the engine does emit ŋ. Post-nasal voicing fires after ⟨m n⟩ only.occitanVOICELESS_PH excludes ʃ — but the engine does emit it. Intervocalic ⟨s⟩→[z] depends on the exclusion.A universal class changes both. "The IPA vowel letters" is a fact about the notation; "the voiceless obstruents of this language" is a fact about the language.
What this PR does instead
Moves them to their manifests (#746's rule), each carrying the rule it serves and why it is narrow.
core/ipa.tsstays a vowel-class module.14 engines: albanian, ancientgreek, chuvash, dutch, galician, hungarian, irish, luxembourgish, occitan, romanian, santali, scottishgaelic, totontepecmixe, turkmen.
Irish and Scottish Gaelic are the clearest illustration — their liquids carry the broad ⟨ˠ⟩ / slender ⟨ʲ⟩ marks, so "the liquids" is four phones in a way no shared class could express. Galician already had
nasalsin its manifest; itsvelarsnow sits beside it.Left alone, deliberately
VOICELESS— derived from each manifest'sDEVOICEmap, so already single-sourced.Verification
Pure data relocation: 19 rewritten sets compared against
main, 0 differ (the 20th, Scottish Gaelic's, checked by hand — the script bindsDEF/MANIFEST, not that file'sM).npm run cigreen — 228 files / 3135 tests, typecheck, fence.docs/ipa_classes_investigation.mdRun 8 has the triage.🤖 Generated with Claude Code
https://claude.ai/code/session_012Lc3WnUgogC7okV7n53vjr