diff --git a/Machine.sln b/Machine.sln index 0d91d2dac..eb4ead14e 100644 --- a/Machine.sln +++ b/Machine.sln @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.Machine.Tokenization.Se EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.Machine.Tokenization.SentencePiece.Tests", "tests\SIL.Machine.Tokenization.SentencePiece.Tests\SIL.Machine.Tokenization.SentencePiece.Tests.csproj", "{AB5F75C1-64B7-4E0F-A4B5-B14EB16E6DDC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SIL.Machine.Morphology.HermitCrab.Conformance", "src\SIL.Machine.Morphology.HermitCrab.Conformance\SIL.Machine.Morphology.HermitCrab.Conformance.csproj", "{8E40C2C5-F4C5-4A41-B78A-10A43FCF0CE3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,10 @@ Global {AB5F75C1-64B7-4E0F-A4B5-B14EB16E6DDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB5F75C1-64B7-4E0F-A4B5-B14EB16E6DDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB5F75C1-64B7-4E0F-A4B5-B14EB16E6DDC}.Release|Any CPU.Build.0 = Release|Any CPU + {8E40C2C5-F4C5-4A41-B78A-10A43FCF0CE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E40C2C5-F4C5-4A41-B78A-10A43FCF0CE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E40C2C5-F4C5-4A41-B78A-10A43FCF0CE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E40C2C5-F4C5-4A41-B78A-10A43FCF0CE3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -116,6 +122,7 @@ Global {0BBEED18-3CC1-4BA6-8E11-603A5D2DEDC9} = {31B4F5EB-E148-4A51-84B5-B90BB8F38AD3} {09135223-6285-4268-BFA3-147BDB5B663B} = {31B4F5EB-E148-4A51-84B5-B90BB8F38AD3} {AB5F75C1-64B7-4E0F-A4B5-B14EB16E6DDC} = {66246A1C-8D45-40FB-A660-C58577122CA7} + {8E40C2C5-F4C5-4A41-B78A-10A43FCF0CE3} = {31B4F5EB-E148-4A51-84B5-B90BB8F38AD3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F18C25E-E140-43C3-B177-D562E1628370} diff --git a/conformance/PROTOCOL.md b/conformance/PROTOCOL.md new file mode 100644 index 000000000..0e6bd34a4 --- /dev/null +++ b/conformance/PROTOCOL.md @@ -0,0 +1,263 @@ +# The morphological parser conformance adapter protocol + +This document specifies, precisely enough that a third party should not need to read any engine's +source (or this repo's), what it takes for **any** morphological/phonological parser to become +testable against the fixtures under `conformance/`. The protocol is engine-agnostic: nothing here, +or in a fixture's own files, requires Machine's build, layout, or C# — only a way to run the engine +under test as a command and produce a plain TSV file. + +`SIL.Machine.Morphology.HermitCrab`'s C# implementation is this suite's **founding oracle**: every +fixture's ground truth was generated by running it, and its own `BatchCommand.cs` +(`src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs`) is where this contract's TSV format +and signature algorithm were first implemented — but HermitCrab is not a privileged consumer of the +protocol, just its first implementer (the Rust port's own CLI independently arrived at the same +3-arg `batch` contract). Any engine, for any language, implementing section 1 below is equally +conformant. + +## 1. The canonical adapter contract + +A conforming engine adapter is a single executable command: + +``` + batch [--start N] +``` + +- `` — the engine's grammar input for this fixture. For the reference representation this + is `grammar.xml` (`HermitCrabInput` XML); see section 6 for engines that cannot consume it + directly. +- `` — one word per line, blank lines ignored; shared verbatim across every engine under + test for a given fixture (never engine-specific). +- `` — the file the adapter must write its results to, in the format specified in + section 2. +- `--start N` (optional) — 0-based line index to resume at, for crash recovery on long word lists. + An adapter that doesn't need resumability may ignore this flag or not implement it; the harness + only relies on it for its own optional resume behavior, never as a correctness requirement. + +This is exactly what `hc-rs batch ` already does, so the Rust +port is adapter-conformant today with zero wrapper code. See section 7 for the one C#-specific +wrinkle. + +## 2. The output format: a 5-column TSV + +Every line of `` has exactly 5 tab-separated columns: + +``` +idx word ms status signature +``` + +- **`idx`** — 0-based index of the word in `words.txt` (matches C#'s `BatchCommand.RunSequential` + loop variable `i`). +- **`word`** — the literal word string being parsed (echoed back, so the row is self-describing + without needing to cross-reference `words.txt` by line number). +- **`ms`** — elapsed wall-clock milliseconds for parsing this one word. **Not compared** by the + harness (see section 3) — timing is informational only and is expected to vary run to run and + engine to engine. +- **`status`** — one of: + - `ok` — the parse attempt completed (regardless of whether it found any analyses — zero + analyses is a legitimate `ok` result, not a failure; see the signature column). + - `SKIPPED` — the engine could not attempt the parse at all (C#'s only case today: + `InvalidShapeException`, i.e. the word contains a character outside the grammar's character + definition table). A `SKIPPED` row's signature is always `-`. +- **`signature`** — the order-independent, sorted, semicolon-joined signature of every distinct + analysis found (see section 3 for the exact algorithm), or the literal `-` if zero analyses were + found (this is the normal, correct representation of "this word does not parse under this + grammar" — a fixture's `expect_fail: true` words). + +### The `STARTED` sentinel line + +C#'s `BatchCommand` writes a sentinel line immediately before attempting each word, so a crashed +process can be resumed from the last line written: + +``` +idx word STARTED +``` + +(3 columns, not 5.) This is a crash-recovery convenience for engines that might crash mid-word on a +long corpus; **it carries no comparison meaning** and adapters are not required to emit it (`hc-rs` +deliberately does not — it buffers results and writes the final file in one shot). The harness's TSV +parser and comparison logic (section 4) skip any line that doesn't have the 5-column `ok`/`SKIPPED` +shape, so `STARTED` lines are silently ignored wherever they appear. + +### The crash convention: `expect_crash` + +A handful of fixtures (e.g. `edge-cases/simultaneous-epenthesis-cascade`, whose founding oracle run +hit an infinite-loop detector mid-word) pin a **crash** as their ground truth, not a signature. Such +a fixture's `expected.tsv` contains only a `STARTED` sentinel line for the word that crashed the +oracle — there is no real result row, because the oracle itself died first — and its fixture +metadata (`words.yaml` front matter) carries `expect_crash: true` to make that explicit rather than +leaving the harness to infer it from an empty `expected.tsv`. + +The harness's contract for an `expect_crash` fixture: + +- The engine under test is expected to **also** crash while loading the grammar or parsing the + word (not necessarily the same exception type or message — just fail rather than complete). That + counts as a **PASS**: reproducing the crash IS conforming to the ground truth. +- If the engine crashes but `expect_crash` is **not** set, that is a **FAIL** (an unexpected engine + crash), with the engine's diagnostics shown. +- If `expect_crash` **is** set but the engine returns normally (produces output instead of + crashing), that is a **FAIL** ("expected crash, engine produced output") — the engine no longer + reproduces the bug the fixture pins. +- A harness-level problem (bad `--adapter` command, the adapter process failing to start, a + timeout, or an adapter exiting 0 without producing its output file) is never treated as a crash + match, `expect_crash` or not — it is always a plain failure/error. + +Because a crash aborts a fixture's whole word loop, crash fixtures should keep `words.txt` to a +single word: there is nothing after the crash for the harness to compare, so additional words would +never actually be exercised. + +## 3. The signature algorithm + +For a word that parses, each distinct result contributes one signature entry: + +``` +++...| +``` + +- The morpheme-ID chain is built by walking the word's allomorphs **in morph order** + (`Word.AllomorphsInMorphOrder`), taking each allomorph's owning morpheme's `Id`, joined with `+`. +- After the `|`, the surface shape is rendered via + `Shape.ToRegexString(word.Stratum.CharacterDefinitionTable, true)` — the same character-definition + table used to load the grammar, with boundary markers included (the trailing `true` argument). +- All of a word's distinct-analysis signature strings are then **sorted ordinally** + (`StringComparer.Ordinal`) and joined with `;`. This makes the signature **order-independent**: two + runs that find the same set of analyses in a different internal search order compare equal. It + does **not** dedupe — if an engine's search legitimately produces the same signature string twice + (a duplicate, not a distinct analysis), both occurrences appear; a divergence in duplicate *count* + for an otherwise-matching signature set is not automatically a pass, see section 4 for how the + harness handles this. +- If zero analyses are found, the signature is the literal string `-` (not an empty string). + +This is a direct transcription of `BatchCommand.BuildSignature` +(`src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs`); an adapter for a new engine +reproduces this string exactly, using that engine's own morpheme IDs and its own surface-shape +rendering, for its output to be diffable against `expected.tsv`. + +**Morpheme ID / gloss naming discipline**: because the signature format compares morpheme names +literally, any engine whose grammar representation assigns different IDs to the same morphemes +(e.g. a hand-authored non-XML grammar for a fixture, see section 6) must match `grammar.xml`'s IDs +exactly, or a real match will register as a false failure. + +**Multi-character segment representations render individually parenthesized.** A character +definition whose string representation is more than one character (e.g. a digraph segment like +`"ch"`) is wrapped in its own `(...)` in the shape half of a signature, unconditionally — this comes +straight from `HermitCrabExtensions.ToRegexString`'s `if (strRep.Length > 1) sb.Append("(")`, which +fires regardless of whether the segment is otherwise ambiguous. A word `"panucho"` in a grammar that +defines `"ch"` as one segment renders as `panu(ch)o`, not `panucho`. Easy to miss when hand-deriving +an expected signature — check a grammar's character definition table for multi-character +representations before trusting a hand-written signature (see +`conformance/languages/suffixing-quechua/words.yaml`'s authoring-discipline note for a case this +actually caught). + +**Guess-stem rendering (`Guesser`/`LexicalGuess`, pinned in G1).** A guessed root has no +distinguishing marker at all in the signature string: `Morpher.LexicalGuess` builds a throwaway +`LexEntry` whose `Id` (and `Gloss`) is set to the literal matched surface substring, so the guessed +root's morph-chain component in the signature is that substring itself, indistinguishable by string +alone from a real lexical entry that happened to share the same id (e.g. word `"foos"` with root +"foo" unknown but a lexical guess pattern `[Any]*` in scope, plus a real suffix rule PL="-s", yields +signature entries `foo+PL|foos` and — because the guess pattern is also tried against the +zero-affixes candidate — `foos|foos`; pinned executably in +`tests/SIL.Machine.Morphology.HermitCrab.Tests/GuesserSignatureTests.cs`). **Separately, and more +importantly for this suite: `BatchCommand` — the CLI `batch` subcommand every conformance adapter +and oracle invocation goes through — can never produce a guess-stem parse at all.** +`SignatureFormat.ParseOneWord` (what `BatchCommand.RunSequential` and the harness's own self-check +engine both call) invokes the 2-argument `Morpher.ParseWord(word, out trace)` overload, which +hardcodes `guessRoot: false`; no CLI command in `SIL.Machine.Morphology.HermitCrab.Tool` exposes the +3-argument, `guessRoot: true` overload at all. The harness's self-check mode works around this by +calling `Morpher.ParseWord` directly with `guessRoot: true` whenever a word declares a `guess: true` +parse (`src/SIL.Machine.Morphology.HermitCrab.Conformance/Runner.cs`) — but **adapter mode +cannot**: an external engine driven through the documented `batch` contract has no way to opt into +guessing, so a fixture with a `guess: true` word is self-check-only until `BatchCommand`/the adapter +contract gains an opt-in flag for it. Concretely: the harness's adapter-mode materialization **omits +any word carrying a `guess: true` parse** from the generated `words.txt`/`expected.tsv`, so such +words are asserted by self-check only and an adapter run neither sees them nor can falsely fail on +them. + +## 4. Comparison semantics (what "pass" means) + +The harness diffs an adapter's `` against a fixture's checked-in `expected.tsv` using +these rules: + +1. Only the `word`, `status`, and `signature` columns are compared. **`idx` and `ms` are ignored** + (row identity is keyed by `word`, not `idx`, so an adapter is free to reorder rows; `ms` is pure + timing noise). +2. Any line that isn't a well-formed 5-column `idx / word / ms / status / signature` row (in + particular, any `STARTED` sentinel line) is skipped entirely before comparison. +3. A signature is parsed into its `;`-separated set of `morph-chain|shape` entries and compared as a + **multiset** — reordering the entries within a signature string is not a failure (the algorithm + already sorts them, but a hand-edited or alternate-engine `expected.tsv` reordering is tolerated + defensively), but a genuine difference in the entries themselves, or in how many times a given + entry appears, **is** a failure. +4. A `status` mismatch (`ok` vs `SKIPPED`) is always a failure. +5. A missing or extra `word` row (present in one file, absent in the other) is a failure for that + word. + +Anything outside these rules — a real signature difference, a real status difference — is a +genuine conformance failure and is reported as such, per word, and rolled up per fixture. + +## 5. Capability profiles + +Not every engine implements every grammar-model construct HermitCrab supports (the motivating case +is XAmple, SIL's AMPLE-family morphotactic parser, which has no phonological-rule engine at all — +see `docs/archive/conformance-framework-plan.md` §4.6). Two mechanisms make the suite serve engines +of different scope without forking it: + +- **Engine capability declaration.** Whoever invokes the harness against an engine declares that + engine's supported capability set, e.g. `--capabilities phonology` for HermitCrab/`hc-rs`, or + `--capabilities ""` (empty) for a purely morphotactic engine. Self-check mode (running the live C# + oracle in-process) always implies full capability, `phonology` included. +- **Fixture capability requirement.** Every fixture's metadata (`words.yaml` front matter) carries a + `requires` array — today the only defined value is `phonology`, meaning "this fixture's + `grammar.xml` contains at least one `PhonologicalRule` or `MetathesisRule` element." An empty/absent + `requires` is pure morphotactics, runnable by any conforming engine. The harness mechanically + re-derives this list from `grammar.xml` on every run and FAILs the fixture (a metadata error, not + a conformance failure) if the checked-in `requires` disagrees, so this can never silently drift. + +The harness computes, for each fixture, whether the engine's declared capability set is a superset +of the fixture's `requires` list. If not, the fixture is **skipped** for that engine run — reported +as `SKIPPED`, distinct from both `PASS` and `FAIL`, so a capability-limited engine's run is never +scored as if it silently passed fixtures outside its scope. New capability values are added the same +way `phonology` was: name the vocabulary word, and mechanically derive it from the fixture's +`grammar.xml` (or other grammar representation) contents, never by hand-guessing from a directory +name. + +## 6. Per-engine grammar representation + +The adapter contract's first positional argument is *the engine's grammar input for the fixture*, +not necessarily `grammar.xml` itself. `grammar.xml` (`HermitCrabInput` XML) is the default and +reference representation — every fixture ships one, and it's what the C# oracle used to generate +`expected.tsv`. An engine that cannot consume that XML directly (XAmple's case: it consumes +AMPLE-style control/dictionary files) names an alternate, per-fixture grammar file or directory in +its own registration (e.g. a `grammar.xample/` directory living alongside `grammar.xml` in the +fixture — see `docs/archive/conformance-framework-plan.md`'s future XAmple-enablement phase, F6). +`words.txt` and `expected.tsv` are always shared across every engine tested against a fixture: one +word list, one ground truth, regardless of how many different grammar representations exist for it. + +## 7. Aside: C#'s own reference tool needs a wrapper to conform + +`SIL.Machine.Morphology.HermitCrab.Tool` (`hc.dll`) does **not** implement section 1's contract +directly, because of a pre-existing CLI shape it inherited from HermitCrab.NET: grammar loading is a +*global* `Program.Main` option (`-i `), separate from and prior to dispatching any +subcommand, and its `batch` subcommand only takes 2 positional arguments +(` `), not 3. The normal invocation is: + +``` +hc.dll -i grammar.xml -s script.txt +``` + +where `script.txt` contains one line: `batch words.txt expected.tsv` (paths relative to the fixture +directory the tool is run from). This is a real, working invocation shape — every fixture's +`expected.tsv` in this suite was generated exactly this way — it is just not literally section 1's +single 3-positional-arg command. + +Two consequences: + +- **The harness's self-check mode does not shell out to `hc.dll` at all.** It calls the `Morpher` + API in-process (loading `grammar.xml` via `XmlLanguageLoader`, building a `Morpher`, and computing + the same signature algorithm as `BatchCommand.BuildSignature`, see section 3) — this sidesteps the + CLI-shape mismatch entirely, since self-check mode never needs a CLI contract in the first place. +- If anyone ever wants to point the harness's `--adapter` flag at `hc.dll` itself (to test + adapter-mode plumbing against a known-good reference, independent of self-check mode), `hc.dll` + needs a **one-line wrapper script** that (1) writes a temporary `script.txt` containing + `batch ` with the two paths substituted in, then (2) invokes + `hc.dll -i -s `. Not required by any current acceptance criteria + (self-check plus `--adapter` against `hc-rs` suffices), but straightforward if ever needed. diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 000000000..711658589 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,103 @@ +# The morphological parser conformance suite + +An **all-language parser oracle**: a general-purpose, engine-agnostic conformance test suite for +what a morphological/phonological parser needs to get right, usable independently of this repo and +of any one engine. Passing this suite means an engine can correctly parse any language's morphology +— or will, as coverage grows. `PROTOCOL.md` specifies the adapter contract in engine-agnostic terms; +nothing in `conformance/` depends on Machine's build, layout, or C#. `SIL.Machine.Morphology.HermitCrab` +supplies the seed construct vocabulary (`constructs.txt`) and its C# engine is the **founding +oracle** that generated every fixture's ground truth, but it is not a hard boundary: any engine that +implements `PROTOCOL.md`'s contract can be validated here, declaring a partial **capability +profile** (e.g. morphotactics-only, no phonological rules) to be measured fairly against just the +subset it claims to support. + +## Shape + +``` +conformance/ + languages// grammar.xml + words.yaml -- 8 typologically-selected synthetic languages + edge-cases// grammar.xml + words.yaml -- 8 micro-grammars for things no shared grammar hosts + coverage.csv GENERATED: language x word x construct + rules.csv GENERATED: language x grammar rule id x exercising words +``` + +Exactly two files per fixture. `words.yaml` is the single ground truth: front matter (`language`, +`inspired_by`, `sources`, `requires`) plus one entry per word, each carrying a `note` and either +`parses` (`signature`/`gloss`/`rules`/`exercises`, one per distinct analysis), `expect_fail` + +`blocked_by` (well-formed input, zero valid analyses), or `expect_skip` (the oracle throws +`InvalidShapeException`, e.g. an undeclared segment — status "SKIPPED", not "ok"). Optional fields: +`provenance` (bug/PR reference), `guess` (a `LexicalGuess` parse), +`budget_ms`/`expect_crash` (edge-cases only). Signatures are authored-and-verified, never blindly +regenerated — self-check FAILs on mismatch; `--propose` prints a patch for a human to accept, never +writes. Full schema: `docs/conformance-language-suite-plan.md` §2.1. + +## The eight languages + +| Directory | Invented name | Simulates | +|---|---|---| +| `agglutinative-turkic` | Kirenti | Turkish/Finnish-style harmony, long suffix chains, gradation allomorphy | +| `templatic-semitic` | Zemreth | Arabic/Amharic root-and-pattern, stem names, OCP co-occurrence, simultaneous rewrite | +| `bantu-verbal` | Velamba | Swahili/Sena-style realizational rules, MPR groups, verbal reduplication, nasal mutation | +| `austronesian-phase` | Tavolan | Tagalog/Rotuman infix, circumfix, reduplication, metathesis, truncation | +| `polysynthetic-inuit` | Nuvattak | Yup'ik-style stratum ordering, incorporation-style compounding, guessed stems | +| `suffixing-quechua` | Chaqru | Cuzco Quechua, `requires: []` — the XAmple-eligible, phonology-free grammar | +| `prefixal-athabaskan` | Ketselan | Navajo-style left-edge position-class template, fused/discontinuous slots | +| `fusional-latin` | Vetruna | Latin/Russian inflection classes, syncretism, ablaut as sole exponent | + +Each is a dense, synthetic grammar (invented lexemes and segment inventories, no orthographic claim +about any real language) where ordinary words exercise several constructs at once, rather than a +one-mechanism probe. `edge-cases/` holds the eight things no shared grammar can host faithfully: +loader/XML-semantics probes, an `expect_crash` pin, a `budget_ms` pathological stress case, and +allomorphy/rewrite pins whose exact rule shape a naturalistic grammar wouldn't contain. + +## Coverage philosophy + +`constructs.txt` is the flat, cross-grammar checklist of grammar-model constructs every fixture's +`exercises:` values are drawn from — the coverage report cross-references against it and flags both +zero-coverage constructs and **dead rules** (a `grammar.xml` rule id no word exercises). `rules:` +per parse is a verified dimension, not aspirational: self-check runs the oracle with tracing on and +FAILs if the traced rule applications diverge from the declared list. As of this writing, 19/19 +in-scope constructs are covered (`Tracing (TraceType)` is out of scope — it was never in +`expected.tsv`'s domain). + +## Running it + +``` +# self-check: in-process C# oracle against every fixture +dotnet run --project src/SIL.Machine.Morphology.HermitCrab.Conformance -- --fixtures conformance --include-pathological + +# adapter mode: materializes words.txt/expected.tsv to temp, runs an external engine +dotnet run --project src/SIL.Machine.Morphology.HermitCrab.Conformance -- --fixtures conformance --adapter "" + +# coverage + dead-rule report: writes coverage.csv and rules.csv +dotnet run --project src/SIL.Machine.Morphology.HermitCrab.Conformance -- --fixtures conformance --coverage-report + +# migration parity proof (v1->v2 floors + the permanent absolute construct check) +python conformance/parity-check.py +``` + +## Growth policy: every bug fix lands with a conformance addition + +> **Every bug fix in a conforming engine lands together with a conformance addition** — a new word, +> a new grammar, or a new edge-case. + +The cheap path, in order of preference: + +1. **Add a word.** One `words.yaml` entry (word + `note` + `parses` + `provenance`) in the language + whose grammar already covers the territory. No harness or grammar changes. +2. **Extend a grammar** when no existing language's grammar can express the trigger but the + phenomenon is family-plausible for one of the eight (e.g. a new Turkic-style harmony + interaction belongs in `agglutinative-turkic`). +3. **Add an edge-case** when the pin is engine-internal or too specific to embed naturalistically + (an XML-loader quirk, a crash, a rule shape no real grammar would contain). +4. **Add a new `languages/` member** only for genuinely new typological territory not covered by any + of the eight (see the future-expansion tier in `docs/conformance-language-suite-plan.md` §3). + +## See also + +- `PROTOCOL.md` — the adapter contract: CLI shape, TSV format, signature algorithm, capability + profiles, per-engine grammar representation. +- `docs/conformance-migration-ledger.md` — the permanent provenance bridge from every v1 fixture to + its v2 destination. +- `docs/conformance-language-suite-plan.md` — the full design rationale, language-selection research, + and phased plan this suite was restructured from. diff --git a/conformance/adapters/hc-dotnet-wrapper.sh b/conformance/adapters/hc-dotnet-wrapper.sh new file mode 100644 index 000000000..a65996593 --- /dev/null +++ b/conformance/adapters/hc-dotnet-wrapper.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Adapter-protocol wrapper for SIL.Machine.Morphology.HermitCrab.Tool (hc.dll), per +# conformance/PROTOCOL.md section 7: hc.dll's own CLI shape ("-i grammar -s script.txt", where +# script.txt contains a "batch words.txt output.tsv" line) predates and doesn't match section 1's +# single 3-positional-arg contract, so this one-line-idea wrapper bridges the two: it writes a +# temporary script.txt and invokes hc.dll the normal way. +# +# Usage (matches the protocol's canonical shape, plus one extra arg for the tool path): +# hc-dotnet-wrapper.sh batch [path/to/hc.dll] +# +# Exists so --adapter mode (SIL.Machine.Morphology.HermitCrab.Conformance's external-engine path) +# can be exercised end-to-end against a real conforming engine even when no second engine (e.g. +# hc-rs, the Rust port) is present in this checkout -- this repo's own C# oracle, run as a +# subprocess through the documented adapter contract instead of self-check mode's in-process call, +# is a legitimate (if not very interesting) conformance-testable engine in its own right. +set -euo pipefail + +if [ "${1:-}" != "batch" ]; then + echo "usage: hc-dotnet-wrapper.sh batch [path/to/hc.dll]" >&2 + exit 2 +fi +grammar="$2" +words="$3" +output="$4" + +# Resolve the default hc.dll path relative to this script's own location (repo root), not the +# caller's cwd, so the wrapper works when invoked from anywhere. An explicit 5th arg always wins, +# used exactly as given (relative to the caller's cwd, same as before). +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../.." && pwd)" + +if [ -n "${5:-}" ]; then + hcdll="$5" +else + hcdll="" + for candidate in \ + "$repo_root/src/SIL.Machine.Morphology.HermitCrab.Tool/bin/Debug/net10.0/hc.dll" \ + "$repo_root/src/SIL.Machine.Morphology.HermitCrab.Tool/bin/Release/net10.0/hc.dll" + do + if [ -f "$candidate" ]; then + hcdll="$candidate" + break + fi + done + # Neither build exists: fall through with the Debug path so the error message below points at + # the expected default location instead of silently trying an empty path. + if [ -z "$hcdll" ]; then + hcdll="$repo_root/src/SIL.Machine.Morphology.HermitCrab.Tool/bin/Debug/net10.0/hc.dll" + fi +fi + +tmpscript="$(mktemp)" +trap 'rm -f "$tmpscript"' EXIT + +printf 'batch "%s" "%s"\n' "$words" "$output" > "$tmpscript" + +# hc.dll's stdout is load-progress/prompt chatter, not adapter-contract output -- but it also +# carries load errors, which AdapterEngine needs to see in its own captured stderr on a nonzero +# exit. Redirect (not discard) it, so it's still available for diagnostics without polluting the +# adapter protocol's stdout. Now that Program.Main's script mode actually returns a nonzero exit +# code on failure (rather than always 0), `set -e` above genuinely propagates a hc.dll failure. +dotnet "$hcdll" -i "$grammar" -s "$tmpscript" >&2 diff --git a/conformance/constructs.txt b/conformance/constructs.txt new file mode 100644 index 000000000..8bc32a51f --- /dev/null +++ b/conformance/constructs.txt @@ -0,0 +1,43 @@ +# HermitCrab grammar-model construct checklist +# +# This is the coverage-report key set: every conformance/languages//words.yaml and +# conformance/edge-cases//words.yaml's per-parse and per-word "exercises:" entries are +# expected to draw their values verbatim from this list. It is a data file, not compiled code, so a +# new construct can be added here the moment a fixture needs it, without touching the harness. +# +# Format: one construct per line. Blank lines and lines starting with "#" are ignored. A line's +# text (verbatim, including punctuation) is the key the coverage report (--coverage-report) +# cross-references against every word's/parse's "exercises:" entries, emitting coverage.csv and +# flagging both zero-coverage constructs and dead rules (grammar.xml rule ids no word exercises, +# emitted in rules.csv). This is a direct transcription of +# docs/archive/conformance-framework-plan.md section 6 ("Grammar-model construct checklist"); if +# that table grows, add the new row's construct text here too so the report picks it up. +# +# The harness treats an "exercises:" value that ISN'T in this file as a soft warning (a genuinely +# new construct, or a typo) -- never a hard error. + +Stratum (Linear/Unordered rule order) +AffixProcessRule: prefix/suffix/circumfix/infix +AffixProcessRule: reduplication (ReduplicationHint) +AffixProcessRule: subtraction/truncation +RealizationalAffixProcessRule +CompoundingRule +MorphologicalOutputAction: CopyFromInput/InsertSegments +MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext +RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +RewriteRule Simultaneous +MetathesisRule +Affix template slots (obligatory/disjunctive/ordering) +NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +Boundary markers (CharacterDefinitionTable) +MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +MPR features/groups +Guesser/LexicalGuess +Disjunctive allomorphs / free-fluctuation +Stem names +Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures) +Alpha-variable phonological environments (VariableFeature/AlphaVariables) +CharacterDefinitionTable pattern shapes: optional group / Kleene star +CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features) +Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Tracing (TraceType) diff --git a/conformance/coverage.csv b/conformance/coverage.csv new file mode 100644 index 000000000..12760cc2c --- /dev/null +++ b/conformance/coverage.csv @@ -0,0 +1,289 @@ +language,word,signature,construct +PathologicalDeepOptionalAffixNesting,k,K|k,Affix template slots (obligatory/disjunctive/ordering) +PathologicalDeepOptionalAffixNesting,xxxxxxxxxxxxk,P12+P11+P10+P9+P8+P7+P6+P5+P4+P3+P2+P1+K|xxxxxxxxxxxxk,Affix template slots (obligatory/disjunctive/ordering) +DisjunctiveRecheckProbe,wak,|wak,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,wok,,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,wakta,,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,wokta,+|wokta,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,pakza,+|pakza,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,pakda,,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,pitza,,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,pitda,+|pitda,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,gray,|gray,Disjunctive allomorphs / free-fluctuation +DisjunctiveRecheckProbe,grey,|grey,Disjunctive allomorphs / free-fluctuation +LatinDiacriticProbe,años,YEAR+PL|años,AffixProcessRule: prefix/suffix/circumfix/infix +LatinDiacriticProbe,años,YEAR+PL|años,MorphologicalOutputAction: CopyFromInput/InsertSegments +LatinDiacriticProbe,años,YEAR+PL|años,Affix template slots (obligatory/disjunctive/ordering) +LatinDiacriticProbe,cafés,COFFEE+PL|cafés,AffixProcessRule: prefix/suffix/circumfix/infix +LatinDiacriticProbe,cafés,COFFEE+PL|cafés,MorphologicalOutputAction: CopyFromInput/InsertSegments +LatinDiacriticProbe,cafés,COFFEE+PL|cafés,Affix template slots (obligatory/disjunctive/ordering) +LatinDiacriticProbe,göls,LAKE+PL|göls,AffixProcessRule: prefix/suffix/circumfix/infix +LatinDiacriticProbe,göls,LAKE+PL|göls,MorphologicalOutputAction: CopyFromInput/InsertSegments +LatinDiacriticProbe,göls,LAKE+PL|göls,Affix template slots (obligatory/disjunctive/ordering) +LatinDiacriticProbe,kelîs,WORD+PL|kelîs,AffixProcessRule: prefix/suffix/circumfix/infix +LatinDiacriticProbe,kelîs,WORD+PL|kelîs,MorphologicalOutputAction: CopyFromInput/InsertSegments +LatinDiacriticProbe,kelîs,WORD+PL|kelîs,Affix template slots (obligatory/disjunctive/ordering) +LatinDiacriticProbe,caféta,COFFEE+DIM|caféta,AffixProcessRule: prefix/suffix/circumfix/infix +LatinDiacriticProbe,caféta,COFFEE+DIM|caféta,MorphologicalOutputAction: CopyFromInput/InsertSegments +LatinDiacriticProbe,caféta,COFFEE+DIM|caféta,Affix template slots (obligatory/disjunctive/ordering) +LatinDiacriticProbe,caféta,COFFEE+DIM|caféta,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +LatinDiacriticProbe,kelîta,WORD+DIM|kelîta,AffixProcessRule: prefix/suffix/circumfix/infix +LatinDiacriticProbe,kelîta,WORD+DIM|kelîta,MorphologicalOutputAction: CopyFromInput/InsertSegments +LatinDiacriticProbe,kelîta,WORD+DIM|kelîta,Affix template slots (obligatory/disjunctive/ordering) +LatinDiacriticProbe,kelîta,WORD+DIM|kelîta,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +LatinDiacriticProbe,añota,,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +LatinDiacriticProbe,gölta,,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +LatinDiacriticProbe,gül,,Boundary markers (CharacterDefinitionTable) +N2-DefaultSymbol,bat,|bat,Boundary markers (CharacterDefinitionTable) +N2-DefaultSymbol,bdt,,Boundary markers (CharacterDefinitionTable) +N1-IsActive,kat,|kat,Boundary markers (CharacterDefinitionTable) +N1-IsActive,sod,|sod,Boundary markers (CharacterDefinitionTable) +N3-PatternShapes,bat,|b[ae]t,Boundary markers (CharacterDefinitionTable) +N3-PatternShapes,bet,|b[ae]t,Boundary markers (CharacterDefinitionTable) +N3-PatternShapes,bit,,Boundary markers (CharacterDefinitionTable) +N3-PatternShapes,bt,,CharacterDefinitionTable pattern shapes: optional group / Kleene star +StrRepIdentityProbe,pat,+|[(^0)∅]?+?pat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,pat,|pat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,mat,+|[(^0)∅]?+?mat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,mat,|mat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,mupat,+|mu+?pat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,mumat,+|mu+?mat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,mwpat,,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,mwuta,+|mw+?uta,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,muuta,,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,ndpat,++|[(^0)∅]?+?nd+?pat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,ndpat,++|nd+?[(^0)∅]?+?pat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,ndpat,+|nd+?pat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,ndmat,,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,imat,++|[(^0)∅]?+?i+?mat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,imat,++|i+?[(^0)∅]?+?mat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,imat,+|i+?mat,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +StrRepIdentityProbe,ipat,,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +TruncateRulesProbe,sag,|sag,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,sas,|sas,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,bubibi,|bubibi,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,sa,+|sa,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,ag,+|ag,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,as,+|as,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,gbubibi,+|gbubibi,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,gas,++|gas,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,gas,+|gas,AffixProcessRule: subtraction/truncation +TruncateRulesProbe,bag,,AffixProcessRule: subtraction/truncation +Kirenti,semitide,SEMIT+PAST|semitide,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,semitide,SEMIT+PAST|semitide,RewriteRule Simultaneous +Kirenti,semitile,SEMIT+PRES|semitile,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,semitile,SEMIT+PRES|semitile,RewriteRule Simultaneous +Kirenti,semitideler,SEMIT+PAST+PL|semitideler,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,kutagida,KUTAK+PAST|kutagida,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,kutagida,KUTAK+PAST|kutagida,RewriteRule Simultaneous +Kirenti,kutagida,KUTAK+PAST|kutagida,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,kutagida,KUTAK+PAST|kutagida,NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision +Kirenti,kutagida,KUTAK+PAST|kutagida,Boundary markers (CharacterDefinitionTable) +Kirenti,kutagila,KUTAK+PRES|kutagila,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,kutagila,KUTAK+PRES|kutagila,RewriteRule Simultaneous +Kirenti,kutagila,KUTAK+PRES|kutagila,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,kutagila,KUTAK+PRES|kutagila,Boundary markers (CharacterDefinitionTable) +Kirenti,kutak,,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,kutakler,,Affix template slots (obligatory/disjunctive/ordering) +Kirenti,unitide,UNT+PAST|unitide,RewriteRule Simultaneous +Kirenti,unitide,UNT+PAST|unitide,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,untda,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,guan,GAN+INTS|guan,AffixProcessRule: prefix/suffix/circumfix/infix +Kirenti,etemik,,AffixProcessRule: prefix/suffix/circumfix/infix +Kirenti,esalik,,AffixProcessRule: prefix/suffix/circumfix/infix +Kirenti,duii,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,duy,DUII|duy,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,sueeb,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,sueb,SUEEB|sueb,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Kirenti,satun,SAT+ALPHA|satun,Alpha-variable phonological environments (VariableFeature/AlphaVariables) +Kirenti,setin,SET+ALPHA|setin,Alpha-variable phonological environments (VariableFeature/AlphaVariables) +Tavolan,nui,NIU|nui,MetathesisRule +Tavolan,niu,,MetathesisRule +Tavolan,mu+i,MI+3SGU|mu+?i,MetathesisRule +Tavolan,mu+i,MI+3SGU|mu+?i,Boundary markers (CharacterDefinitionTable) +Tavolan,pui,PU+3SGI|pui,AffixProcessRule: prefix/suffix/circumfix/infix +Tavolan,sumulat,SULAT+AV|sumulat,AffixProcessRule: prefix/suffix/circumfix/infix +Tavolan,sumulat,SULAT+AV|sumulat,MorphologicalOutputAction: CopyFromInput/InsertSegments +Tavolan,keadilan,NMLZ+ADIL|keadilan,AffixProcessRule: prefix/suffix/circumfix/infix +Tavolan,keadilan,NMLZ+ADIL|keadilan,MorphologicalOutputAction: CopyFromInput/InsertSegments +Tavolan,tutula,RDP+TULA|tutula,AffixProcessRule: reduplication (ReduplicationHint) +Tavolan,tulatula,RDPL+TULA|tulatula,AffixProcessRule: reduplication (ReduplicationHint) +Tavolan,pur,PURE+INCP|pur,AffixProcessRule: subtraction/truncation +Tavolan,bahain,BAHA+LNK|bahain,AffixProcessRule: prefix/suffix/circumfix/infix +Tavolan,bahain,BAHA+LNK|bahain,MorphologicalOutputAction: CopyFromInput/InsertSegments +Tavolan,katibɯd,KTB+PERF|katibɯd,AffixProcessRule: prefix/suffix/circumfix/infix +Tavolan,katibɯd,KTB+PERF|katibɯd,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Tavolan,katabɯd,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Velamba,mba,PAV|mba,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Velamba,mpa,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Velamba,limaile,LIM+RPAST|limaile,RealizationalAffixProcessRule +Velamba,limape,LIM+TENSE|limape,RealizationalAffixProcessRule +Velamba,limapeile,,RealizationalAffixProcessRule +Velamba,xkib,EXTX+KIB|xkib,MPR features/groups +Velamba,yxkib,EXTY+EXTX+KIB|yxkib,MPR features/groups +Velamba,xkibz,EXTX+KIB+ENDZ|xkibz,MPR features/groups +Velamba,yxkibz,,MPR features/groups +Velamba,kibw,KIB+ENDW|kibw,MPR features/groups +Velamba,belw,,MPR features/groups +Velamba,nalw,,MPR features/groups +Velamba,kibh,,MPR features/groups +Velamba,belh,BEL+ENDH|belh,MPR features/groups +Velamba,nalh,,MPR features/groups +Velamba,kimbiakimbia,KIMB+RED|kimbiakimbia,AffixProcessRule: reduplication (ReduplicationHint) +Velamba,andika,AND+FV|andika,Affix template slots (obligatory/disjunctive/ordering) +Velamba,andik,,Affix template slots (obligatory/disjunctive/ordering) +Velamba,andikisha,AND+CAUS+FV|andikisha,Affix template slots (obligatory/disjunctive/ordering) +Velamba,andikila,AND+APPL+FV|andikila,Affix template slots (obligatory/disjunctive/ordering) +Velamba,andikishila,AND+CAUS+APPL+FV|andikishila,Affix template slots (obligatory/disjunctive/ordering) +Velamba,andikilisha,AND+APPL+CAUS+FV|andikilisha,Affix template slots (obligatory/disjunctive/ordering) +Velamba,dabep,DAB+NUMPL|dabep,Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures) +Velamba,dabepez,DAB+NUMPL+AGR|dabepez,Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures) +Velamba,dabez,DAB+AGR|dabez,Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures) +Velamba,kes,KES|kes,Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures) +Velamba,kesez,,Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures) +Velamba,siliz,SIL+RECURSUF|siliz,Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Velamba,siliziz,,Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Velamba,daleh,DAL+STEMREQ|daleh,Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Velamba,daheh,,Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Velamba,zimed,,Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Velamba,zamed,,Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable) +Vetruna,mun,STEM|mun,Stem names +Vetruna,muno,,Stem names +Vetruna,muns,,Stem names +Vetruna,munt,,Stem names +Vetruna,mano,STEM+P1|mano,Stem names +Vetruna,mans,STEM+P2|mans,Stem names +Vetruna,mant,,Stem names +Vetruna,man,,Stem names +Vetruna,mino,STEM+P1|mino,Stem names +Vetruna,mins,,Stem names +Vetruna,mint,STEM+P3|mint,Stem names +Vetruna,min,,Stem names +Vetruna,ferid,FER+RPAST|ferid,RealizationalAffixProcessRule +Vetruna,feres,FER+TENSE|feres,RealizationalAffixProcessRule +Vetruna,feresid,,RealizationalAffixProcessRule +Vetruna,tul,TUL|tul,RealizationalAffixProcessRule +Vetruna,ducit,,RealizationalAffixProcessRule +Vetruna,ducunt,DUC+PL2|ducunt,RealizationalAffixProcessRule +Vetruna,gelobt,GET+LOB|gelobt,AffixProcessRule: prefix/suffix/circumfix/infix +Vetruna,lobh,LOB+S3|lobh,AffixProcessRule: prefix/suffix/circumfix/infix +Vetruna,gelobth,GET+LOB+S3|gelobth,AffixProcessRule: prefix/suffix/circumfix/infix +Vetruna,gelobthh,,AffixProcessRule: prefix/suffix/circumfix/infix +Vetruna,vinc,VIC+PRESSTEM|vinc,AffixProcessRule: prefix/suffix/circumfix/infix +Vetruna,pannox,PAN+NOXN|pan+?nox,CompoundingRule +Vetruna,pannox,PAN+NOXV|pan+?nox,CompoundingRule +Vetruna,benox,BEPFX1+NOXV|benox,AffixProcessRule: prefix/suffix/circumfix/infix +Vetruna,panbenox,,CompoundingRule +Vetruna,pandex,,CompoundingRule +Vetruna,lexdom,LEX+DOMN|lex+?dom,CompoundingRule +Vetruna,lexdom,LEX+DOMV|lex+?dom,CompoundingRule +Vetruna,lexbedom,LEX+BEPFX2+DOMV|lex+?bedom,CompoundingRule +Vetruna,lexbex,,CompoundingRule +Vetruna,xped,THEMEX+PED|xped,MPR features/groups +Vetruna,yxped,THEMEY+THEMEX+PED|yxped,MPR features/groups +Vetruna,xpedz,THEMEX+PED+ENDZ|xpedz,MPR features/groups +Vetruna,yxpedz,,MPR features/groups +Vetruna,katw,KAT+ENDW|katw,MPR features/groups +Vetruna,sodw,,MPR features/groups +Vetruna,limw,,MPR features/groups +Vetruna,kath,,MPR features/groups +Vetruna,sodh,SOD+ENDH|sodh,MPR features/groups +Vetruna,limh,,MPR features/groups +Vetruna,sang,SING+ABLAUT|sang,MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext +Vetruna,mitlav,MIT+LAV|mit+?lav,CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features) +Vetruna,ficlav,FIC+LAV|fic+?lav,CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features) +Vetruna,seclav,,CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features) +Vetruna,genlav,,CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features) +Vetruna,mitlavlav,,CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features) +Nuvattak,nunaliq,NUNA+DERIV|nunaliq,AffixProcessRule: prefix/suffix/circumfix/infix +Nuvattak,nunaliqvuq,NUNA+DERIV+INFL|nunaliqvuq,Stratum (Linear/Unordered rule order) +Nuvattak,nunaliqvuq,NUNA+DERIV+INFL|nunaliqvuq,AffixProcessRule: prefix/suffix/circumfix/infix +Nuvattak,nunavuq,,Stratum (Linear/Unordered rule order) +Nuvattak,akutat,AKU+TATN|aku+?tat,CompoundingRule +Nuvattak,akutat,AKU+TATN|aku+?tat,Boundary markers (CharacterDefinitionTable) +Nuvattak,akutat,AKU+TATV|aku+?tat,CompoundingRule +Nuvattak,akutat,AKU+TATV|aku+?tat,Boundary markers (CharacterDefinitionTable) +Nuvattak,pitat,PIPFX1+TATV|pitat,AffixProcessRule: prefix/suffix/circumfix/infix +Nuvattak,akupitat,,CompoundingRule +Nuvattak,akutas,,CompoundingRule +Nuvattak,silanuk,SILA+NUKN|sila+?nuk,CompoundingRule +Nuvattak,silanuk,SILA+NUKN|sila+?nuk,Boundary markers (CharacterDefinitionTable) +Nuvattak,silanuk,SILA+NUKV|sila+?nuk,CompoundingRule +Nuvattak,silanuk,SILA+NUKV|sila+?nuk,Boundary markers (CharacterDefinitionTable) +Nuvattak,silamanuk,SILA+MAPFX2+NUKV|sila+?manuk,CompoundingRule +Nuvattak,silamak,,CompoundingRule +Nuvattak,takuvuq,taku+INFL|takuvuq,Guesser/LexicalGuess +Nuvattak,takuvuq,taku+INFL|takuvuq,Guesser/LexicalGuess +Nuvattak,takuvuq,takuvuq|takuvuq,Guesser/LexicalGuess +Nuvattak,takuvuq,takuvuq|takuvuq,Guesser/LexicalGuess +Nuvattak,bubu,DR1|bubu,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,bubu,DR2|bubu,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,bubu,DR3|bubu,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,buibu,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,kuuukuuu,MS|kuuukuuu,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,kuiikuii,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,buuubuuu,MDC|buuubuuu,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,buiibuii,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,mau,MDC2|mau,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Nuvattak,matu,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Ketselan,shiyidkal,SBJ+PRF+CLF+KAL|shiyidkal,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,niyidtan,SBJ+PRF+CLF+TAN|niyidtan,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,bishiyidkal,OBJ+SBJ+PRF+CLF+KAL|bishiyidkal,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,bishiwodkal,,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,yidkal,,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,kal,,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,shiwodkal,SBJ+IMPF+CLF+KAL|shiwodkal,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,gabishiyidkal,ADVA+OBJ+SBJ+PRF+CLF+KAL|gabishiyidkal,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,hogabishiyidkal,ADVB+ADVA+OBJ+SBJ+PRF+CLF+KAL|hogabishiyidkal,Affix template slots (obligatory/disjunctive/ordering) +Ketselan,gahobishiyidkal,,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,tupay,TUPA+1SG|tupay,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,tupay,TUPA2+1SG|tupay,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,sipuy,SIPU+1SG|sipuy,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,walakni,WALAK+1SG|walakni,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,walaknki,WALAK+2SG|walaknki,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,sipun,SIPU+3SG|sipun,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,walaknichik,WALAK+1SG+PL|walakni(ch)ik,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,sipuymi,SIPU+1SG+DIR|sipuymi,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,walaknkisi,WALAK+2SG+REP|walaknkisi,Disjunctive allomorphs / free-fluctuation +Chaqru,walaknkishi,WALAK+2SG+REP|walaknki(sh)i,Disjunctive allomorphs / free-fluctuation +Chaqru,sipuncha,SIPU+3SG+CONJ|sipun(ch)a,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,takiy,SING+1SG|takiy,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,kantay,SING+1SG|kantay,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,takincha,SING+3SG+CONJ|takin(ch)a,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,kantancha,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,walaknitan,WALAK+1SG+ASSUM|walaknitan,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,walaknichiktan,WALAK+1SG+PL+ASSUM|walakni(ch)iktan,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,walakntan,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,walakniwas,WALAK+1SG+MIR|walakniwas,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,walaknichikwas,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,siputukumi,SIPU+1PL.INCL+DIR|siputukumi,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,siputukuchikmi,SIPU+1PL.INCL+PL+DIR|siputuku(ch)ikmi,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,siputuku,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,sipulupami,SIPU+2PL+DIR|sipulupami,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,sipulupachikmi,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Chaqru,walaknkichikmi,WALAK+2SG+PL+DIR|walaknki(ch)ikmi,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,walakchik,,Affix template slots (obligatory/disjunctive/ordering) +Chaqru,walaknkicha,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Zemreth,katabit,KTB+P1SG|katabit,RewriteRule Simultaneous +Zemreth,katabit,KTB+P1SG|katabit,MorphologicalOutputAction: CopyFromInput/InsertSegments +Zemreth,katabt,,RewriteRule Simultaneous +Zemreth,kataba,KTB+P3SG|kataba,MorphologicalOutputAction: CopyFromInput/InsertSegments +Zemreth,sapr,SPR+FORMII|sapr,MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext +Zemreth,samm,,MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule +Zemreth,qil,QWL+PASS|qil,MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext +Zemreth,halak,HLK|halak,Stem names +Zemreth,halaku,,Stem names +Zemreth,hilaku,HLK+IMPF|hilaku,Stem names +Zemreth,hilaka,,Stem names +Zemreth,hilaki,HLK+PERF|hilaki,Stem names +Zemreth,hulaka,HLK+JUSS|hulaka,Stem names +Zemreth,hulaku,,Stem names +Zemreth,hulaki,HLK+PERF|hulaki,Stem names +Zemreth,gigugu,FEEDSIM|gigugu,RewriteRule Simultaneous +Zemreth,gigugi,FEEDITER|gigugi,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Zemreth,gigigi,,RewriteRule Simultaneous +Zemreth,gigigi,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Zemreth,iktub,KTUB|iktub,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Zemreth,ktub,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Zemreth,radda,RAD|radda,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) +Zemreth,rada,,RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge) diff --git a/conformance/edge-cases/deep-optional-affix-nesting/grammar.xml b/conformance/edge-cases/deep-optional-affix-nesting/grammar.xml new file mode 100644 index 000000000..b909b080e --- /dev/null +++ b/conformance/edge-cases/deep-optional-affix-nesting/grammar.xml @@ -0,0 +1,211 @@ + + + + + PathologicalDeepOptionalAffixNesting + + + v + + + Main + + x + k + + + + Any + + + + Main + + + p1 + + + + x + + + P1 + + + p2 + + + + x + + + P2 + + + p3 + + + + x + + + P3 + + + p4 + + + + x + + + P4 + + + p5 + + + + x + + + P5 + + + p6 + + + + x + + + P6 + + + p7 + + + + x + + + P7 + + + p8 + + + + x + + + P8 + + + p9 + + + + x + + + P9 + + + p10 + + + + x + + + P10 + + + p11 + + + + x + + + P11 + + + p12 + + + + x + + + P12 + + + + + deepOptionalTemplate + + slot1 + + + slot2 + + + slot3 + + + slot4 + + + slot5 + + + slot6 + + + slot7 + + + slot8 + + + slot9 + + + slot10 + + + slot11 + + + slot12 + + + + + + k + K + + + + + + diff --git a/conformance/edge-cases/deep-optional-affix-nesting/words.yaml b/conformance/edge-cases/deep-optional-affix-nesting/words.yaml new file mode 100644 index 000000000..8c118877a --- /dev/null +++ b/conformance/edge-cases/deep-optional-affix-nesting/words.yaml @@ -0,0 +1,1924 @@ +# Converted from conformance/pathological/deep-optional-affix-nesting (v1; kept in place, untouched, +# until phase G4 -- see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same +# file, unmodified: 12 independent, all-optional AffixTemplate prefix slots (P1..P12), each inserting +# the literal character "x" via InsertSegments+CopyFromInput, requiredPartsOfSpeech="posV" throughout, +# no PhonologicalRule/MetathesisRule element anywhere -- so RequiresDerivation mechanically derives +# requires: [] (this fixture is morphotactics-only, not phonology). +# +# What this pins (v1 manifest.json's provenance, carried forward verbatim below): per +# SynthesisAffixTemplateRule.ApplySlots, an all-optional slot chain explores the FULL powerset of +# "which slots fired" (2^12 = 4096 subsets) -- and because every slot inserts the identical character, +# any two subsets of the same size k produce byte-identical surface text but remain DISTINCT analyses +# (distinct MorphemeId chains, hence distinct signature strings). A word with k leading x's therefore +# has exactly C(12,k) equally-valid analyses. The three words below hit the two boundary cases (k=0, +# k=12, each exactly 1 analysis) and the combinatorial-blowup midpoint (k=6, C(12,6) = 924 analyses, +# oracle-confirmed) -- expected.tsv's own v1 ground truth for "xxxxxxk" is the full 924-entry +# semicolon-joined signature list, translated below into 924 individual `parses:` entries (one per +# distinct slot subset) rather than hand-derived from scratch, per the task's "expected parses come +# from the v1 expected.tsv (translate to signature form), verify against the oracle" instruction -- +# hand-deriving 924 combinatorial variants would be mechanical transcription of a well-understood +# combinatorial fact (which slots fired), not five hand-reasoned words, so this edge-case (unlike +# prefixal-athabaskan) is generated from the checked-in oracle output and then self-check-verified, +# not authored blind. Every declared (signature, rules) pair below was verified against the live +# oracle by the self-check gate (RunAllWords) before this file was committed. +# +# budget_ms below (15000, copied verbatim from v1's manifest.json budget.wallClockMs) is why this +# fixture is EXCLUDED from a default self-check run (RunnerV2.RunOneSelfCheck: `isPathological = +# fixture.Words.BudgetMs.HasValue`, skipped unless --include-pathological) and only exercised with +# that flag -- same isolation discipline as v1's own pathological category. + +language: PathologicalDeepOptionalAffixNesting +inspired_by: ["synthetic pathological-complexity probe (not modeling any real language)"] +sources: + - "docs/conformance-framework-plan.md section 4.5 (pathological category); F3 worked example" +requires: [] +budget_ms: 15000 +words: + - word: "k" + note: >- + The zero-prefix boundary case: no optional slot fires (the "skip everything" path through + ApplySlots), giving exactly ONE analysis, C(12,0) = 1. Confirms the template's all-optional + chain still permits the bare root. + provenance: "F3 worked example (pathological category): 12 independent all-optional affix-template prefix slots inserting an identical literal character, producing C(12,k) equally-valid analyses for a k-x-prefixed word (924 for k=6, confirmed against the live oracle: 3 words, ~2.0s total wall-clock on this branch's un-optimized plain-master engine)" + parses: + - signature: "K|k" + rules: [] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: "xxxxxxk" + note: >- + The combinatorial-blowup case: 6 leading x's admit C(12,6) = 924 distinct subsets of the 12 + slots that could have produced them (confirmed exactly against the live oracle: 924 + semicolon-joined entries in v1's own expected.tsv signature column for this row, translated + 1:1 into the 924 `parses:` entries below). Every one of the 924 analyses is equally + linguistically valid under this grammar -- there is no mechanism here to prefer one subset + over another, so a conforming engine MUST enumerate all of them. This is the fixture's actual + pathological content: not a crash or a hang, but a combinatorially large, fully tractable, + exactly-specified analysis SET that any conforming engine must reproduce in full within the + budget_ms wall-clock budget. + provenance: "F3 worked example (pathological category): 12 independent all-optional affix-template prefix slots inserting an identical literal character, producing C(12,k) equally-valid analyses for a k-x-prefixed word (924 for k=6, confirmed against the live oracle: 3 words, ~2.0s total wall-clock on this branch's un-optimized plain-master engine)" + parses: + - signature: "P10+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P10+P6+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP6, mrP4, mrP3, mrP2, mrP1] + - signature: "P10+P6+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP6, mrP5, mrP3, mrP2, mrP1] + - signature: "P10+P6+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP6, mrP5, mrP4, mrP2, mrP1] + - signature: "P10+P6+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP6, mrP5, mrP4, mrP3, mrP1] + - signature: "P10+P6+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP6, mrP5, mrP4, mrP3, mrP2] + - signature: "P10+P7+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP4, mrP3, mrP2, mrP1] + - signature: "P10+P7+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP5, mrP3, mrP2, mrP1] + - signature: "P10+P7+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP5, mrP4, mrP2, mrP1] + - signature: "P10+P7+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP5, mrP4, mrP3, mrP1] + - signature: "P10+P7+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP7, mrP5, mrP4, mrP3, mrP2] + - signature: "P10+P7+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP3, mrP2, mrP1] + - signature: "P10+P7+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP4, mrP2, mrP1] + - signature: "P10+P7+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP4, mrP3, mrP1] + - signature: "P10+P7+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP4, mrP3, mrP2] + - signature: "P10+P7+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP5, mrP2, mrP1] + - signature: "P10+P7+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP5, mrP3, mrP1] + - signature: "P10+P7+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP5, mrP3, mrP2] + - signature: "P10+P7+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP5, mrP4, mrP1] + - signature: "P10+P7+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP5, mrP4, mrP2] + - signature: "P10+P7+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP7, mrP6, mrP5, mrP4, mrP3] + - signature: "P10+P8+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP4, mrP3, mrP2, mrP1] + - signature: "P10+P8+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP5, mrP3, mrP2, mrP1] + - signature: "P10+P8+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP5, mrP4, mrP2, mrP1] + - signature: "P10+P8+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP5, mrP4, mrP3, mrP1] + - signature: "P10+P8+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP5, mrP4, mrP3, mrP2] + - signature: "P10+P8+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP3, mrP2, mrP1] + - signature: "P10+P8+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP4, mrP2, mrP1] + - signature: "P10+P8+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP4, mrP3, mrP1] + - signature: "P10+P8+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP4, mrP3, mrP2] + - signature: "P10+P8+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP5, mrP2, mrP1] + - signature: "P10+P8+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP5, mrP3, mrP1] + - signature: "P10+P8+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP5, mrP3, mrP2] + - signature: "P10+P8+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP5, mrP4, mrP1] + - signature: "P10+P8+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP5, mrP4, mrP2] + - signature: "P10+P8+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP8, mrP6, mrP5, mrP4, mrP3] + - signature: "P10+P8+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP3, mrP2, mrP1] + - signature: "P10+P8+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP4, mrP2, mrP1] + - signature: "P10+P8+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP4, mrP3, mrP1] + - signature: "P10+P8+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP4, mrP3, mrP2] + - signature: "P10+P8+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP5, mrP2, mrP1] + - signature: "P10+P8+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP5, mrP3, mrP1] + - signature: "P10+P8+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP5, mrP3, mrP2] + - signature: "P10+P8+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP5, mrP4, mrP1] + - signature: "P10+P8+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP5, mrP4, mrP2] + - signature: "P10+P8+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP5, mrP4, mrP3] + - signature: "P10+P8+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP2, mrP1] + - signature: "P10+P8+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP3, mrP1] + - signature: "P10+P8+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP3, mrP2] + - signature: "P10+P8+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP4, mrP1] + - signature: "P10+P8+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP4, mrP2] + - signature: "P10+P8+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP4, mrP3] + - signature: "P10+P8+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP5, mrP1] + - signature: "P10+P8+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP5, mrP2] + - signature: "P10+P8+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP5, mrP3] + - signature: "P10+P8+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP10, mrP8, mrP7, mrP6, mrP5, mrP4] + - signature: "P10+P9+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP4, mrP3, mrP2, mrP1] + - signature: "P10+P9+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP5, mrP3, mrP2, mrP1] + - signature: "P10+P9+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP5, mrP4, mrP2, mrP1] + - signature: "P10+P9+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP5, mrP4, mrP3, mrP1] + - signature: "P10+P9+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP5, mrP4, mrP3, mrP2] + - signature: "P10+P9+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP3, mrP2, mrP1] + - signature: "P10+P9+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP4, mrP2, mrP1] + - signature: "P10+P9+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP4, mrP3, mrP1] + - signature: "P10+P9+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP4, mrP3, mrP2] + - signature: "P10+P9+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP5, mrP2, mrP1] + - signature: "P10+P9+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP5, mrP3, mrP1] + - signature: "P10+P9+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP5, mrP3, mrP2] + - signature: "P10+P9+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP5, mrP4, mrP1] + - signature: "P10+P9+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP5, mrP4, mrP2] + - signature: "P10+P9+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP6, mrP5, mrP4, mrP3] + - signature: "P10+P9+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP3, mrP2, mrP1] + - signature: "P10+P9+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP4, mrP2, mrP1] + - signature: "P10+P9+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP4, mrP3, mrP1] + - signature: "P10+P9+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP4, mrP3, mrP2] + - signature: "P10+P9+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP5, mrP2, mrP1] + - signature: "P10+P9+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP5, mrP3, mrP1] + - signature: "P10+P9+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP5, mrP3, mrP2] + - signature: "P10+P9+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP5, mrP4, mrP1] + - signature: "P10+P9+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP5, mrP4, mrP2] + - signature: "P10+P9+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP5, mrP4, mrP3] + - signature: "P10+P9+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP2, mrP1] + - signature: "P10+P9+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP3, mrP1] + - signature: "P10+P9+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP3, mrP2] + - signature: "P10+P9+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP4, mrP1] + - signature: "P10+P9+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP4, mrP2] + - signature: "P10+P9+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP4, mrP3] + - signature: "P10+P9+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP5, mrP1] + - signature: "P10+P9+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP5, mrP2] + - signature: "P10+P9+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP5, mrP3] + - signature: "P10+P9+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP10, mrP9, mrP7, mrP6, mrP5, mrP4] + - signature: "P10+P9+P8+P3+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP3, mrP2, mrP1] + - signature: "P10+P9+P8+P4+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP4, mrP2, mrP1] + - signature: "P10+P9+P8+P4+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP4, mrP3, mrP1] + - signature: "P10+P9+P8+P4+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP4, mrP3, mrP2] + - signature: "P10+P9+P8+P5+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP5, mrP2, mrP1] + - signature: "P10+P9+P8+P5+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP5, mrP3, mrP1] + - signature: "P10+P9+P8+P5+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP5, mrP3, mrP2] + - signature: "P10+P9+P8+P5+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP5, mrP4, mrP1] + - signature: "P10+P9+P8+P5+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP5, mrP4, mrP2] + - signature: "P10+P9+P8+P5+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP5, mrP4, mrP3] + - signature: "P10+P9+P8+P6+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP2, mrP1] + - signature: "P10+P9+P8+P6+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP3, mrP1] + - signature: "P10+P9+P8+P6+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP3, mrP2] + - signature: "P10+P9+P8+P6+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP4, mrP1] + - signature: "P10+P9+P8+P6+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP4, mrP2] + - signature: "P10+P9+P8+P6+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP4, mrP3] + - signature: "P10+P9+P8+P6+P5+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP5, mrP1] + - signature: "P10+P9+P8+P6+P5+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP5, mrP2] + - signature: "P10+P9+P8+P6+P5+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP5, mrP3] + - signature: "P10+P9+P8+P6+P5+P4+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP6, mrP5, mrP4] + - signature: "P10+P9+P8+P7+P2+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP2, mrP1] + - signature: "P10+P9+P8+P7+P3+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP3, mrP1] + - signature: "P10+P9+P8+P7+P3+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP3, mrP2] + - signature: "P10+P9+P8+P7+P4+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP4, mrP1] + - signature: "P10+P9+P8+P7+P4+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP4, mrP2] + - signature: "P10+P9+P8+P7+P4+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP4, mrP3] + - signature: "P10+P9+P8+P7+P5+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP5, mrP1] + - signature: "P10+P9+P8+P7+P5+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP5, mrP2] + - signature: "P10+P9+P8+P7+P5+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP5, mrP3] + - signature: "P10+P9+P8+P7+P5+P4+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP5, mrP4] + - signature: "P10+P9+P8+P7+P6+P1+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP6, mrP1] + - signature: "P10+P9+P8+P7+P6+P2+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP6, mrP2] + - signature: "P10+P9+P8+P7+P6+P3+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP6, mrP3] + - signature: "P10+P9+P8+P7+P6+P4+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP6, mrP4] + - signature: "P10+P9+P8+P7+P6+P5+K|xxxxxxk" + rules: [mrP10, mrP9, mrP8, mrP7, mrP6, mrP5] + - signature: "P11+P10+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP4, mrP3, mrP2, mrP1] + - signature: "P11+P10+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP5, mrP3, mrP2, mrP1] + - signature: "P11+P10+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP5, mrP4, mrP2, mrP1] + - signature: "P11+P10+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP5, mrP4, mrP3, mrP1] + - signature: "P11+P10+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP5, mrP4, mrP3, mrP2] + - signature: "P11+P10+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP3, mrP2, mrP1] + - signature: "P11+P10+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP4, mrP2, mrP1] + - signature: "P11+P10+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP4, mrP3, mrP1] + - signature: "P11+P10+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP4, mrP3, mrP2] + - signature: "P11+P10+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP5, mrP2, mrP1] + - signature: "P11+P10+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP5, mrP3, mrP1] + - signature: "P11+P10+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP5, mrP3, mrP2] + - signature: "P11+P10+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP5, mrP4, mrP1] + - signature: "P11+P10+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP5, mrP4, mrP2] + - signature: "P11+P10+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP6, mrP5, mrP4, mrP3] + - signature: "P11+P10+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP3, mrP2, mrP1] + - signature: "P11+P10+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP4, mrP2, mrP1] + - signature: "P11+P10+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP4, mrP3, mrP1] + - signature: "P11+P10+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP4, mrP3, mrP2] + - signature: "P11+P10+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP5, mrP2, mrP1] + - signature: "P11+P10+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP5, mrP3, mrP1] + - signature: "P11+P10+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP5, mrP3, mrP2] + - signature: "P11+P10+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP5, mrP4, mrP1] + - signature: "P11+P10+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP5, mrP4, mrP2] + - signature: "P11+P10+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP5, mrP4, mrP3] + - signature: "P11+P10+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP2, mrP1] + - signature: "P11+P10+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP3, mrP1] + - signature: "P11+P10+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP3, mrP2] + - signature: "P11+P10+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP4, mrP1] + - signature: "P11+P10+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP4, mrP2] + - signature: "P11+P10+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP4, mrP3] + - signature: "P11+P10+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP5, mrP1] + - signature: "P11+P10+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP5, mrP2] + - signature: "P11+P10+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP5, mrP3] + - signature: "P11+P10+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP7, mrP6, mrP5, mrP4] + - signature: "P11+P10+P8+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP3, mrP2, mrP1] + - signature: "P11+P10+P8+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP4, mrP2, mrP1] + - signature: "P11+P10+P8+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP4, mrP3, mrP1] + - signature: "P11+P10+P8+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP4, mrP3, mrP2] + - signature: "P11+P10+P8+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP5, mrP2, mrP1] + - signature: "P11+P10+P8+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP5, mrP3, mrP1] + - signature: "P11+P10+P8+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP5, mrP3, mrP2] + - signature: "P11+P10+P8+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP5, mrP4, mrP1] + - signature: "P11+P10+P8+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP5, mrP4, mrP2] + - signature: "P11+P10+P8+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP5, mrP4, mrP3] + - signature: "P11+P10+P8+P6+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP2, mrP1] + - signature: "P11+P10+P8+P6+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP3, mrP1] + - signature: "P11+P10+P8+P6+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP3, mrP2] + - signature: "P11+P10+P8+P6+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP4, mrP1] + - signature: "P11+P10+P8+P6+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP4, mrP2] + - signature: "P11+P10+P8+P6+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP4, mrP3] + - signature: "P11+P10+P8+P6+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP5, mrP1] + - signature: "P11+P10+P8+P6+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP5, mrP2] + - signature: "P11+P10+P8+P6+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP5, mrP3] + - signature: "P11+P10+P8+P6+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP6, mrP5, mrP4] + - signature: "P11+P10+P8+P7+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP2, mrP1] + - signature: "P11+P10+P8+P7+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP3, mrP1] + - signature: "P11+P10+P8+P7+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP3, mrP2] + - signature: "P11+P10+P8+P7+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP4, mrP1] + - signature: "P11+P10+P8+P7+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP4, mrP2] + - signature: "P11+P10+P8+P7+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP4, mrP3] + - signature: "P11+P10+P8+P7+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP5, mrP1] + - signature: "P11+P10+P8+P7+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP5, mrP2] + - signature: "P11+P10+P8+P7+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP5, mrP3] + - signature: "P11+P10+P8+P7+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP5, mrP4] + - signature: "P11+P10+P8+P7+P6+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP6, mrP1] + - signature: "P11+P10+P8+P7+P6+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP6, mrP2] + - signature: "P11+P10+P8+P7+P6+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP6, mrP3] + - signature: "P11+P10+P8+P7+P6+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP6, mrP4] + - signature: "P11+P10+P8+P7+P6+P5+K|xxxxxxk" + rules: [mrP11, mrP10, mrP8, mrP7, mrP6, mrP5] + - signature: "P11+P10+P9+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP3, mrP2, mrP1] + - signature: "P11+P10+P9+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP4, mrP2, mrP1] + - signature: "P11+P10+P9+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP4, mrP3, mrP1] + - signature: "P11+P10+P9+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP4, mrP3, mrP2] + - signature: "P11+P10+P9+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP5, mrP2, mrP1] + - signature: "P11+P10+P9+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP5, mrP3, mrP1] + - signature: "P11+P10+P9+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP5, mrP3, mrP2] + - signature: "P11+P10+P9+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP5, mrP4, mrP1] + - signature: "P11+P10+P9+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP5, mrP4, mrP2] + - signature: "P11+P10+P9+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP5, mrP4, mrP3] + - signature: "P11+P10+P9+P6+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP2, mrP1] + - signature: "P11+P10+P9+P6+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP3, mrP1] + - signature: "P11+P10+P9+P6+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP3, mrP2] + - signature: "P11+P10+P9+P6+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP4, mrP1] + - signature: "P11+P10+P9+P6+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP4, mrP2] + - signature: "P11+P10+P9+P6+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP4, mrP3] + - signature: "P11+P10+P9+P6+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP5, mrP1] + - signature: "P11+P10+P9+P6+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP5, mrP2] + - signature: "P11+P10+P9+P6+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP5, mrP3] + - signature: "P11+P10+P9+P6+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP6, mrP5, mrP4] + - signature: "P11+P10+P9+P7+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP2, mrP1] + - signature: "P11+P10+P9+P7+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP3, mrP1] + - signature: "P11+P10+P9+P7+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP3, mrP2] + - signature: "P11+P10+P9+P7+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP4, mrP1] + - signature: "P11+P10+P9+P7+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP4, mrP2] + - signature: "P11+P10+P9+P7+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP4, mrP3] + - signature: "P11+P10+P9+P7+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP5, mrP1] + - signature: "P11+P10+P9+P7+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP5, mrP2] + - signature: "P11+P10+P9+P7+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP5, mrP3] + - signature: "P11+P10+P9+P7+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP5, mrP4] + - signature: "P11+P10+P9+P7+P6+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP6, mrP1] + - signature: "P11+P10+P9+P7+P6+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP6, mrP2] + - signature: "P11+P10+P9+P7+P6+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP6, mrP3] + - signature: "P11+P10+P9+P7+P6+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP6, mrP4] + - signature: "P11+P10+P9+P7+P6+P5+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP7, mrP6, mrP5] + - signature: "P11+P10+P9+P8+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP2, mrP1] + - signature: "P11+P10+P9+P8+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP3, mrP1] + - signature: "P11+P10+P9+P8+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP3, mrP2] + - signature: "P11+P10+P9+P8+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP4, mrP1] + - signature: "P11+P10+P9+P8+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP4, mrP2] + - signature: "P11+P10+P9+P8+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP4, mrP3] + - signature: "P11+P10+P9+P8+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP5, mrP1] + - signature: "P11+P10+P9+P8+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP5, mrP2] + - signature: "P11+P10+P9+P8+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP5, mrP3] + - signature: "P11+P10+P9+P8+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP5, mrP4] + - signature: "P11+P10+P9+P8+P6+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP6, mrP1] + - signature: "P11+P10+P9+P8+P6+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP6, mrP2] + - signature: "P11+P10+P9+P8+P6+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP6, mrP3] + - signature: "P11+P10+P9+P8+P6+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP6, mrP4] + - signature: "P11+P10+P9+P8+P6+P5+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP6, mrP5] + - signature: "P11+P10+P9+P8+P7+P1+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP7, mrP1] + - signature: "P11+P10+P9+P8+P7+P2+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP7, mrP2] + - signature: "P11+P10+P9+P8+P7+P3+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP7, mrP3] + - signature: "P11+P10+P9+P8+P7+P4+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP7, mrP4] + - signature: "P11+P10+P9+P8+P7+P5+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP7, mrP5] + - signature: "P11+P10+P9+P8+P7+P6+K|xxxxxxk" + rules: [mrP11, mrP10, mrP9, mrP8, mrP7, mrP6] + - signature: "P11+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P11+P6+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP6, mrP4, mrP3, mrP2, mrP1] + - signature: "P11+P6+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP6, mrP5, mrP3, mrP2, mrP1] + - signature: "P11+P6+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP6, mrP5, mrP4, mrP2, mrP1] + - signature: "P11+P6+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP6, mrP5, mrP4, mrP3, mrP1] + - signature: "P11+P6+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP6, mrP5, mrP4, mrP3, mrP2] + - signature: "P11+P7+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP4, mrP3, mrP2, mrP1] + - signature: "P11+P7+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP5, mrP3, mrP2, mrP1] + - signature: "P11+P7+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP5, mrP4, mrP2, mrP1] + - signature: "P11+P7+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP5, mrP4, mrP3, mrP1] + - signature: "P11+P7+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP7, mrP5, mrP4, mrP3, mrP2] + - signature: "P11+P7+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP3, mrP2, mrP1] + - signature: "P11+P7+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP4, mrP2, mrP1] + - signature: "P11+P7+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP4, mrP3, mrP1] + - signature: "P11+P7+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP4, mrP3, mrP2] + - signature: "P11+P7+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP5, mrP2, mrP1] + - signature: "P11+P7+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP5, mrP3, mrP1] + - signature: "P11+P7+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP5, mrP3, mrP2] + - signature: "P11+P7+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP5, mrP4, mrP1] + - signature: "P11+P7+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP5, mrP4, mrP2] + - signature: "P11+P7+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP7, mrP6, mrP5, mrP4, mrP3] + - signature: "P11+P8+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP4, mrP3, mrP2, mrP1] + - signature: "P11+P8+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP5, mrP3, mrP2, mrP1] + - signature: "P11+P8+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP5, mrP4, mrP2, mrP1] + - signature: "P11+P8+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP5, mrP4, mrP3, mrP1] + - signature: "P11+P8+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP5, mrP4, mrP3, mrP2] + - signature: "P11+P8+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP3, mrP2, mrP1] + - signature: "P11+P8+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP4, mrP2, mrP1] + - signature: "P11+P8+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP4, mrP3, mrP1] + - signature: "P11+P8+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP4, mrP3, mrP2] + - signature: "P11+P8+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP5, mrP2, mrP1] + - signature: "P11+P8+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP5, mrP3, mrP1] + - signature: "P11+P8+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP5, mrP3, mrP2] + - signature: "P11+P8+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP5, mrP4, mrP1] + - signature: "P11+P8+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP5, mrP4, mrP2] + - signature: "P11+P8+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP8, mrP6, mrP5, mrP4, mrP3] + - signature: "P11+P8+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP3, mrP2, mrP1] + - signature: "P11+P8+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP4, mrP2, mrP1] + - signature: "P11+P8+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP4, mrP3, mrP1] + - signature: "P11+P8+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP4, mrP3, mrP2] + - signature: "P11+P8+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP5, mrP2, mrP1] + - signature: "P11+P8+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP5, mrP3, mrP1] + - signature: "P11+P8+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP5, mrP3, mrP2] + - signature: "P11+P8+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP5, mrP4, mrP1] + - signature: "P11+P8+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP5, mrP4, mrP2] + - signature: "P11+P8+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP5, mrP4, mrP3] + - signature: "P11+P8+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP2, mrP1] + - signature: "P11+P8+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP3, mrP1] + - signature: "P11+P8+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP3, mrP2] + - signature: "P11+P8+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP4, mrP1] + - signature: "P11+P8+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP4, mrP2] + - signature: "P11+P8+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP4, mrP3] + - signature: "P11+P8+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP5, mrP1] + - signature: "P11+P8+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP5, mrP2] + - signature: "P11+P8+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP5, mrP3] + - signature: "P11+P8+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP8, mrP7, mrP6, mrP5, mrP4] + - signature: "P11+P9+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP4, mrP3, mrP2, mrP1] + - signature: "P11+P9+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP5, mrP3, mrP2, mrP1] + - signature: "P11+P9+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP5, mrP4, mrP2, mrP1] + - signature: "P11+P9+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP5, mrP4, mrP3, mrP1] + - signature: "P11+P9+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP5, mrP4, mrP3, mrP2] + - signature: "P11+P9+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP3, mrP2, mrP1] + - signature: "P11+P9+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP4, mrP2, mrP1] + - signature: "P11+P9+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP4, mrP3, mrP1] + - signature: "P11+P9+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP4, mrP3, mrP2] + - signature: "P11+P9+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP5, mrP2, mrP1] + - signature: "P11+P9+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP5, mrP3, mrP1] + - signature: "P11+P9+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP5, mrP3, mrP2] + - signature: "P11+P9+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP5, mrP4, mrP1] + - signature: "P11+P9+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP5, mrP4, mrP2] + - signature: "P11+P9+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP6, mrP5, mrP4, mrP3] + - signature: "P11+P9+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP3, mrP2, mrP1] + - signature: "P11+P9+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP4, mrP2, mrP1] + - signature: "P11+P9+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP4, mrP3, mrP1] + - signature: "P11+P9+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP4, mrP3, mrP2] + - signature: "P11+P9+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP5, mrP2, mrP1] + - signature: "P11+P9+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP5, mrP3, mrP1] + - signature: "P11+P9+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP5, mrP3, mrP2] + - signature: "P11+P9+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP5, mrP4, mrP1] + - signature: "P11+P9+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP5, mrP4, mrP2] + - signature: "P11+P9+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP5, mrP4, mrP3] + - signature: "P11+P9+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP2, mrP1] + - signature: "P11+P9+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP3, mrP1] + - signature: "P11+P9+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP3, mrP2] + - signature: "P11+P9+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP4, mrP1] + - signature: "P11+P9+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP4, mrP2] + - signature: "P11+P9+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP4, mrP3] + - signature: "P11+P9+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP5, mrP1] + - signature: "P11+P9+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP5, mrP2] + - signature: "P11+P9+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP5, mrP3] + - signature: "P11+P9+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP9, mrP7, mrP6, mrP5, mrP4] + - signature: "P11+P9+P8+P3+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP3, mrP2, mrP1] + - signature: "P11+P9+P8+P4+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP4, mrP2, mrP1] + - signature: "P11+P9+P8+P4+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP4, mrP3, mrP1] + - signature: "P11+P9+P8+P4+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP4, mrP3, mrP2] + - signature: "P11+P9+P8+P5+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP5, mrP2, mrP1] + - signature: "P11+P9+P8+P5+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP5, mrP3, mrP1] + - signature: "P11+P9+P8+P5+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP5, mrP3, mrP2] + - signature: "P11+P9+P8+P5+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP5, mrP4, mrP1] + - signature: "P11+P9+P8+P5+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP5, mrP4, mrP2] + - signature: "P11+P9+P8+P5+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP5, mrP4, mrP3] + - signature: "P11+P9+P8+P6+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP2, mrP1] + - signature: "P11+P9+P8+P6+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP3, mrP1] + - signature: "P11+P9+P8+P6+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP3, mrP2] + - signature: "P11+P9+P8+P6+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP4, mrP1] + - signature: "P11+P9+P8+P6+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP4, mrP2] + - signature: "P11+P9+P8+P6+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP4, mrP3] + - signature: "P11+P9+P8+P6+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP5, mrP1] + - signature: "P11+P9+P8+P6+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP5, mrP2] + - signature: "P11+P9+P8+P6+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP5, mrP3] + - signature: "P11+P9+P8+P6+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP6, mrP5, mrP4] + - signature: "P11+P9+P8+P7+P2+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP2, mrP1] + - signature: "P11+P9+P8+P7+P3+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP3, mrP1] + - signature: "P11+P9+P8+P7+P3+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP3, mrP2] + - signature: "P11+P9+P8+P7+P4+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP4, mrP1] + - signature: "P11+P9+P8+P7+P4+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP4, mrP2] + - signature: "P11+P9+P8+P7+P4+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP4, mrP3] + - signature: "P11+P9+P8+P7+P5+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP5, mrP1] + - signature: "P11+P9+P8+P7+P5+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP5, mrP2] + - signature: "P11+P9+P8+P7+P5+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP5, mrP3] + - signature: "P11+P9+P8+P7+P5+P4+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP5, mrP4] + - signature: "P11+P9+P8+P7+P6+P1+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP6, mrP1] + - signature: "P11+P9+P8+P7+P6+P2+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP6, mrP2] + - signature: "P11+P9+P8+P7+P6+P3+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP6, mrP3] + - signature: "P11+P9+P8+P7+P6+P4+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP6, mrP4] + - signature: "P11+P9+P8+P7+P6+P5+K|xxxxxxk" + rules: [mrP11, mrP9, mrP8, mrP7, mrP6, mrP5] + - signature: "P12+P10+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P10+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP5, mrP3, mrP2, mrP1] + - signature: "P12+P10+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP5, mrP4, mrP2, mrP1] + - signature: "P12+P10+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP5, mrP4, mrP3, mrP1] + - signature: "P12+P10+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP5, mrP4, mrP3, mrP2] + - signature: "P12+P10+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP3, mrP2, mrP1] + - signature: "P12+P10+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP4, mrP2, mrP1] + - signature: "P12+P10+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP4, mrP3, mrP1] + - signature: "P12+P10+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP4, mrP3, mrP2] + - signature: "P12+P10+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP5, mrP2, mrP1] + - signature: "P12+P10+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP5, mrP3, mrP1] + - signature: "P12+P10+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP5, mrP3, mrP2] + - signature: "P12+P10+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP5, mrP4, mrP1] + - signature: "P12+P10+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP5, mrP4, mrP2] + - signature: "P12+P10+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP6, mrP5, mrP4, mrP3] + - signature: "P12+P10+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP3, mrP2, mrP1] + - signature: "P12+P10+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP4, mrP2, mrP1] + - signature: "P12+P10+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP4, mrP3, mrP1] + - signature: "P12+P10+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP4, mrP3, mrP2] + - signature: "P12+P10+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP5, mrP2, mrP1] + - signature: "P12+P10+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP5, mrP3, mrP1] + - signature: "P12+P10+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP5, mrP3, mrP2] + - signature: "P12+P10+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP5, mrP4, mrP1] + - signature: "P12+P10+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP5, mrP4, mrP2] + - signature: "P12+P10+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP5, mrP4, mrP3] + - signature: "P12+P10+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP2, mrP1] + - signature: "P12+P10+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP3, mrP1] + - signature: "P12+P10+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP3, mrP2] + - signature: "P12+P10+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP4, mrP1] + - signature: "P12+P10+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP4, mrP2] + - signature: "P12+P10+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP4, mrP3] + - signature: "P12+P10+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP5, mrP1] + - signature: "P12+P10+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP5, mrP2] + - signature: "P12+P10+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP5, mrP3] + - signature: "P12+P10+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP7, mrP6, mrP5, mrP4] + - signature: "P12+P10+P8+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP3, mrP2, mrP1] + - signature: "P12+P10+P8+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP4, mrP2, mrP1] + - signature: "P12+P10+P8+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP4, mrP3, mrP1] + - signature: "P12+P10+P8+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP4, mrP3, mrP2] + - signature: "P12+P10+P8+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP5, mrP2, mrP1] + - signature: "P12+P10+P8+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP5, mrP3, mrP1] + - signature: "P12+P10+P8+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP5, mrP3, mrP2] + - signature: "P12+P10+P8+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP5, mrP4, mrP1] + - signature: "P12+P10+P8+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP5, mrP4, mrP2] + - signature: "P12+P10+P8+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP5, mrP4, mrP3] + - signature: "P12+P10+P8+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP2, mrP1] + - signature: "P12+P10+P8+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP3, mrP1] + - signature: "P12+P10+P8+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP3, mrP2] + - signature: "P12+P10+P8+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP4, mrP1] + - signature: "P12+P10+P8+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP4, mrP2] + - signature: "P12+P10+P8+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP4, mrP3] + - signature: "P12+P10+P8+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP5, mrP1] + - signature: "P12+P10+P8+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP5, mrP2] + - signature: "P12+P10+P8+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP5, mrP3] + - signature: "P12+P10+P8+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP6, mrP5, mrP4] + - signature: "P12+P10+P8+P7+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP2, mrP1] + - signature: "P12+P10+P8+P7+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP3, mrP1] + - signature: "P12+P10+P8+P7+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP3, mrP2] + - signature: "P12+P10+P8+P7+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP4, mrP1] + - signature: "P12+P10+P8+P7+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP4, mrP2] + - signature: "P12+P10+P8+P7+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP4, mrP3] + - signature: "P12+P10+P8+P7+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP5, mrP1] + - signature: "P12+P10+P8+P7+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP5, mrP2] + - signature: "P12+P10+P8+P7+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP5, mrP3] + - signature: "P12+P10+P8+P7+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP5, mrP4] + - signature: "P12+P10+P8+P7+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP6, mrP1] + - signature: "P12+P10+P8+P7+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP6, mrP2] + - signature: "P12+P10+P8+P7+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP6, mrP3] + - signature: "P12+P10+P8+P7+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP6, mrP4] + - signature: "P12+P10+P8+P7+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP10, mrP8, mrP7, mrP6, mrP5] + - signature: "P12+P10+P9+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP3, mrP2, mrP1] + - signature: "P12+P10+P9+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP4, mrP2, mrP1] + - signature: "P12+P10+P9+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP4, mrP3, mrP1] + - signature: "P12+P10+P9+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP4, mrP3, mrP2] + - signature: "P12+P10+P9+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP5, mrP2, mrP1] + - signature: "P12+P10+P9+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP5, mrP3, mrP1] + - signature: "P12+P10+P9+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP5, mrP3, mrP2] + - signature: "P12+P10+P9+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP5, mrP4, mrP1] + - signature: "P12+P10+P9+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP5, mrP4, mrP2] + - signature: "P12+P10+P9+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP5, mrP4, mrP3] + - signature: "P12+P10+P9+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP2, mrP1] + - signature: "P12+P10+P9+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP3, mrP1] + - signature: "P12+P10+P9+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP3, mrP2] + - signature: "P12+P10+P9+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP4, mrP1] + - signature: "P12+P10+P9+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP4, mrP2] + - signature: "P12+P10+P9+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP4, mrP3] + - signature: "P12+P10+P9+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP5, mrP1] + - signature: "P12+P10+P9+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP5, mrP2] + - signature: "P12+P10+P9+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP5, mrP3] + - signature: "P12+P10+P9+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP6, mrP5, mrP4] + - signature: "P12+P10+P9+P7+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP2, mrP1] + - signature: "P12+P10+P9+P7+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP3, mrP1] + - signature: "P12+P10+P9+P7+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP3, mrP2] + - signature: "P12+P10+P9+P7+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP4, mrP1] + - signature: "P12+P10+P9+P7+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP4, mrP2] + - signature: "P12+P10+P9+P7+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP4, mrP3] + - signature: "P12+P10+P9+P7+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP5, mrP1] + - signature: "P12+P10+P9+P7+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP5, mrP2] + - signature: "P12+P10+P9+P7+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP5, mrP3] + - signature: "P12+P10+P9+P7+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP5, mrP4] + - signature: "P12+P10+P9+P7+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP6, mrP1] + - signature: "P12+P10+P9+P7+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP6, mrP2] + - signature: "P12+P10+P9+P7+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP6, mrP3] + - signature: "P12+P10+P9+P7+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP6, mrP4] + - signature: "P12+P10+P9+P7+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP7, mrP6, mrP5] + - signature: "P12+P10+P9+P8+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP2, mrP1] + - signature: "P12+P10+P9+P8+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP3, mrP1] + - signature: "P12+P10+P9+P8+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP3, mrP2] + - signature: "P12+P10+P9+P8+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP4, mrP1] + - signature: "P12+P10+P9+P8+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP4, mrP2] + - signature: "P12+P10+P9+P8+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP4, mrP3] + - signature: "P12+P10+P9+P8+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP5, mrP1] + - signature: "P12+P10+P9+P8+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP5, mrP2] + - signature: "P12+P10+P9+P8+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP5, mrP3] + - signature: "P12+P10+P9+P8+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP5, mrP4] + - signature: "P12+P10+P9+P8+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP6, mrP1] + - signature: "P12+P10+P9+P8+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP6, mrP2] + - signature: "P12+P10+P9+P8+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP6, mrP3] + - signature: "P12+P10+P9+P8+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP6, mrP4] + - signature: "P12+P10+P9+P8+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP6, mrP5] + - signature: "P12+P10+P9+P8+P7+P1+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP7, mrP1] + - signature: "P12+P10+P9+P8+P7+P2+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP7, mrP2] + - signature: "P12+P10+P9+P8+P7+P3+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP7, mrP3] + - signature: "P12+P10+P9+P8+P7+P4+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP7, mrP4] + - signature: "P12+P10+P9+P8+P7+P5+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP7, mrP5] + - signature: "P12+P10+P9+P8+P7+P6+K|xxxxxxk" + rules: [mrP12, mrP10, mrP9, mrP8, mrP7, mrP6] + - signature: "P12+P11+P10+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP3, mrP2, mrP1] + - signature: "P12+P11+P10+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP4, mrP2, mrP1] + - signature: "P12+P11+P10+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP4, mrP3, mrP1] + - signature: "P12+P11+P10+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP4, mrP3, mrP2] + - signature: "P12+P11+P10+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP5, mrP2, mrP1] + - signature: "P12+P11+P10+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP5, mrP3, mrP1] + - signature: "P12+P11+P10+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP5, mrP3, mrP2] + - signature: "P12+P11+P10+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP5, mrP4, mrP1] + - signature: "P12+P11+P10+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP5, mrP4, mrP2] + - signature: "P12+P11+P10+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP5, mrP4, mrP3] + - signature: "P12+P11+P10+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP2, mrP1] + - signature: "P12+P11+P10+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP3, mrP1] + - signature: "P12+P11+P10+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP3, mrP2] + - signature: "P12+P11+P10+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP4, mrP1] + - signature: "P12+P11+P10+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP4, mrP2] + - signature: "P12+P11+P10+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP4, mrP3] + - signature: "P12+P11+P10+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP5, mrP1] + - signature: "P12+P11+P10+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP5, mrP2] + - signature: "P12+P11+P10+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP5, mrP3] + - signature: "P12+P11+P10+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP6, mrP5, mrP4] + - signature: "P12+P11+P10+P7+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP2, mrP1] + - signature: "P12+P11+P10+P7+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP3, mrP1] + - signature: "P12+P11+P10+P7+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP3, mrP2] + - signature: "P12+P11+P10+P7+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP4, mrP1] + - signature: "P12+P11+P10+P7+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP4, mrP2] + - signature: "P12+P11+P10+P7+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP4, mrP3] + - signature: "P12+P11+P10+P7+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP5, mrP1] + - signature: "P12+P11+P10+P7+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP5, mrP2] + - signature: "P12+P11+P10+P7+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP5, mrP3] + - signature: "P12+P11+P10+P7+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP5, mrP4] + - signature: "P12+P11+P10+P7+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP6, mrP1] + - signature: "P12+P11+P10+P7+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP6, mrP2] + - signature: "P12+P11+P10+P7+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP6, mrP3] + - signature: "P12+P11+P10+P7+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP6, mrP4] + - signature: "P12+P11+P10+P7+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP7, mrP6, mrP5] + - signature: "P12+P11+P10+P8+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP2, mrP1] + - signature: "P12+P11+P10+P8+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP3, mrP1] + - signature: "P12+P11+P10+P8+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP3, mrP2] + - signature: "P12+P11+P10+P8+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP4, mrP1] + - signature: "P12+P11+P10+P8+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP4, mrP2] + - signature: "P12+P11+P10+P8+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP4, mrP3] + - signature: "P12+P11+P10+P8+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP5, mrP1] + - signature: "P12+P11+P10+P8+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP5, mrP2] + - signature: "P12+P11+P10+P8+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP5, mrP3] + - signature: "P12+P11+P10+P8+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP5, mrP4] + - signature: "P12+P11+P10+P8+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP6, mrP1] + - signature: "P12+P11+P10+P8+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP6, mrP2] + - signature: "P12+P11+P10+P8+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP6, mrP3] + - signature: "P12+P11+P10+P8+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP6, mrP4] + - signature: "P12+P11+P10+P8+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP6, mrP5] + - signature: "P12+P11+P10+P8+P7+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP7, mrP1] + - signature: "P12+P11+P10+P8+P7+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP7, mrP2] + - signature: "P12+P11+P10+P8+P7+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP7, mrP3] + - signature: "P12+P11+P10+P8+P7+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP7, mrP4] + - signature: "P12+P11+P10+P8+P7+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP7, mrP5] + - signature: "P12+P11+P10+P8+P7+P6+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP8, mrP7, mrP6] + - signature: "P12+P11+P10+P9+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP2, mrP1] + - signature: "P12+P11+P10+P9+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP3, mrP1] + - signature: "P12+P11+P10+P9+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP3, mrP2] + - signature: "P12+P11+P10+P9+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP4, mrP1] + - signature: "P12+P11+P10+P9+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP4, mrP2] + - signature: "P12+P11+P10+P9+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP4, mrP3] + - signature: "P12+P11+P10+P9+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP5, mrP1] + - signature: "P12+P11+P10+P9+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP5, mrP2] + - signature: "P12+P11+P10+P9+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP5, mrP3] + - signature: "P12+P11+P10+P9+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP5, mrP4] + - signature: "P12+P11+P10+P9+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP6, mrP1] + - signature: "P12+P11+P10+P9+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP6, mrP2] + - signature: "P12+P11+P10+P9+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP6, mrP3] + - signature: "P12+P11+P10+P9+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP6, mrP4] + - signature: "P12+P11+P10+P9+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP6, mrP5] + - signature: "P12+P11+P10+P9+P7+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP7, mrP1] + - signature: "P12+P11+P10+P9+P7+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP7, mrP2] + - signature: "P12+P11+P10+P9+P7+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP7, mrP3] + - signature: "P12+P11+P10+P9+P7+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP7, mrP4] + - signature: "P12+P11+P10+P9+P7+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP7, mrP5] + - signature: "P12+P11+P10+P9+P7+P6+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP7, mrP6] + - signature: "P12+P11+P10+P9+P8+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP1] + - signature: "P12+P11+P10+P9+P8+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP2] + - signature: "P12+P11+P10+P9+P8+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP3] + - signature: "P12+P11+P10+P9+P8+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP4] + - signature: "P12+P11+P10+P9+P8+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP5] + - signature: "P12+P11+P10+P9+P8+P6+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP6] + - signature: "P12+P11+P10+P9+P8+P7+K|xxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP7] + - signature: "P12+P11+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P11+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP5, mrP3, mrP2, mrP1] + - signature: "P12+P11+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP5, mrP4, mrP2, mrP1] + - signature: "P12+P11+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP5, mrP4, mrP3, mrP1] + - signature: "P12+P11+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP5, mrP4, mrP3, mrP2] + - signature: "P12+P11+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP3, mrP2, mrP1] + - signature: "P12+P11+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP4, mrP2, mrP1] + - signature: "P12+P11+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP4, mrP3, mrP1] + - signature: "P12+P11+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP4, mrP3, mrP2] + - signature: "P12+P11+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP5, mrP2, mrP1] + - signature: "P12+P11+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP5, mrP3, mrP1] + - signature: "P12+P11+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP5, mrP3, mrP2] + - signature: "P12+P11+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP5, mrP4, mrP1] + - signature: "P12+P11+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP5, mrP4, mrP2] + - signature: "P12+P11+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP6, mrP5, mrP4, mrP3] + - signature: "P12+P11+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP3, mrP2, mrP1] + - signature: "P12+P11+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP4, mrP2, mrP1] + - signature: "P12+P11+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP4, mrP3, mrP1] + - signature: "P12+P11+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP4, mrP3, mrP2] + - signature: "P12+P11+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP5, mrP2, mrP1] + - signature: "P12+P11+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP5, mrP3, mrP1] + - signature: "P12+P11+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP5, mrP3, mrP2] + - signature: "P12+P11+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP5, mrP4, mrP1] + - signature: "P12+P11+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP5, mrP4, mrP2] + - signature: "P12+P11+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP5, mrP4, mrP3] + - signature: "P12+P11+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP2, mrP1] + - signature: "P12+P11+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP3, mrP1] + - signature: "P12+P11+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP3, mrP2] + - signature: "P12+P11+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP4, mrP1] + - signature: "P12+P11+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP4, mrP2] + - signature: "P12+P11+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP4, mrP3] + - signature: "P12+P11+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP5, mrP1] + - signature: "P12+P11+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP5, mrP2] + - signature: "P12+P11+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP5, mrP3] + - signature: "P12+P11+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP7, mrP6, mrP5, mrP4] + - signature: "P12+P11+P8+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP3, mrP2, mrP1] + - signature: "P12+P11+P8+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP4, mrP2, mrP1] + - signature: "P12+P11+P8+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP4, mrP3, mrP1] + - signature: "P12+P11+P8+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP4, mrP3, mrP2] + - signature: "P12+P11+P8+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP5, mrP2, mrP1] + - signature: "P12+P11+P8+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP5, mrP3, mrP1] + - signature: "P12+P11+P8+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP5, mrP3, mrP2] + - signature: "P12+P11+P8+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP5, mrP4, mrP1] + - signature: "P12+P11+P8+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP5, mrP4, mrP2] + - signature: "P12+P11+P8+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP5, mrP4, mrP3] + - signature: "P12+P11+P8+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP2, mrP1] + - signature: "P12+P11+P8+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP3, mrP1] + - signature: "P12+P11+P8+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP3, mrP2] + - signature: "P12+P11+P8+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP4, mrP1] + - signature: "P12+P11+P8+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP4, mrP2] + - signature: "P12+P11+P8+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP4, mrP3] + - signature: "P12+P11+P8+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP5, mrP1] + - signature: "P12+P11+P8+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP5, mrP2] + - signature: "P12+P11+P8+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP5, mrP3] + - signature: "P12+P11+P8+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP6, mrP5, mrP4] + - signature: "P12+P11+P8+P7+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP2, mrP1] + - signature: "P12+P11+P8+P7+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP3, mrP1] + - signature: "P12+P11+P8+P7+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP3, mrP2] + - signature: "P12+P11+P8+P7+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP4, mrP1] + - signature: "P12+P11+P8+P7+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP4, mrP2] + - signature: "P12+P11+P8+P7+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP4, mrP3] + - signature: "P12+P11+P8+P7+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP5, mrP1] + - signature: "P12+P11+P8+P7+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP5, mrP2] + - signature: "P12+P11+P8+P7+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP5, mrP3] + - signature: "P12+P11+P8+P7+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP5, mrP4] + - signature: "P12+P11+P8+P7+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP6, mrP1] + - signature: "P12+P11+P8+P7+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP6, mrP2] + - signature: "P12+P11+P8+P7+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP6, mrP3] + - signature: "P12+P11+P8+P7+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP6, mrP4] + - signature: "P12+P11+P8+P7+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP8, mrP7, mrP6, mrP5] + - signature: "P12+P11+P9+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP3, mrP2, mrP1] + - signature: "P12+P11+P9+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP4, mrP2, mrP1] + - signature: "P12+P11+P9+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP4, mrP3, mrP1] + - signature: "P12+P11+P9+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP4, mrP3, mrP2] + - signature: "P12+P11+P9+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP5, mrP2, mrP1] + - signature: "P12+P11+P9+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP5, mrP3, mrP1] + - signature: "P12+P11+P9+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP5, mrP3, mrP2] + - signature: "P12+P11+P9+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP5, mrP4, mrP1] + - signature: "P12+P11+P9+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP5, mrP4, mrP2] + - signature: "P12+P11+P9+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP5, mrP4, mrP3] + - signature: "P12+P11+P9+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP2, mrP1] + - signature: "P12+P11+P9+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP3, mrP1] + - signature: "P12+P11+P9+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP3, mrP2] + - signature: "P12+P11+P9+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP4, mrP1] + - signature: "P12+P11+P9+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP4, mrP2] + - signature: "P12+P11+P9+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP4, mrP3] + - signature: "P12+P11+P9+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP5, mrP1] + - signature: "P12+P11+P9+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP5, mrP2] + - signature: "P12+P11+P9+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP5, mrP3] + - signature: "P12+P11+P9+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP6, mrP5, mrP4] + - signature: "P12+P11+P9+P7+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP2, mrP1] + - signature: "P12+P11+P9+P7+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP3, mrP1] + - signature: "P12+P11+P9+P7+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP3, mrP2] + - signature: "P12+P11+P9+P7+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP4, mrP1] + - signature: "P12+P11+P9+P7+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP4, mrP2] + - signature: "P12+P11+P9+P7+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP4, mrP3] + - signature: "P12+P11+P9+P7+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP5, mrP1] + - signature: "P12+P11+P9+P7+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP5, mrP2] + - signature: "P12+P11+P9+P7+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP5, mrP3] + - signature: "P12+P11+P9+P7+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP5, mrP4] + - signature: "P12+P11+P9+P7+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP6, mrP1] + - signature: "P12+P11+P9+P7+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP6, mrP2] + - signature: "P12+P11+P9+P7+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP6, mrP3] + - signature: "P12+P11+P9+P7+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP6, mrP4] + - signature: "P12+P11+P9+P7+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP7, mrP6, mrP5] + - signature: "P12+P11+P9+P8+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP2, mrP1] + - signature: "P12+P11+P9+P8+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP3, mrP1] + - signature: "P12+P11+P9+P8+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP3, mrP2] + - signature: "P12+P11+P9+P8+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP4, mrP1] + - signature: "P12+P11+P9+P8+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP4, mrP2] + - signature: "P12+P11+P9+P8+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP4, mrP3] + - signature: "P12+P11+P9+P8+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP5, mrP1] + - signature: "P12+P11+P9+P8+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP5, mrP2] + - signature: "P12+P11+P9+P8+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP5, mrP3] + - signature: "P12+P11+P9+P8+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP5, mrP4] + - signature: "P12+P11+P9+P8+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP6, mrP1] + - signature: "P12+P11+P9+P8+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP6, mrP2] + - signature: "P12+P11+P9+P8+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP6, mrP3] + - signature: "P12+P11+P9+P8+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP6, mrP4] + - signature: "P12+P11+P9+P8+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP6, mrP5] + - signature: "P12+P11+P9+P8+P7+P1+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP7, mrP1] + - signature: "P12+P11+P9+P8+P7+P2+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP7, mrP2] + - signature: "P12+P11+P9+P8+P7+P3+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP7, mrP3] + - signature: "P12+P11+P9+P8+P7+P4+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP7, mrP4] + - signature: "P12+P11+P9+P8+P7+P5+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP7, mrP5] + - signature: "P12+P11+P9+P8+P7+P6+K|xxxxxxk" + rules: [mrP12, mrP11, mrP9, mrP8, mrP7, mrP6] + - signature: "P12+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P6+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP6, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P6+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP6, mrP5, mrP3, mrP2, mrP1] + - signature: "P12+P6+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP6, mrP5, mrP4, mrP2, mrP1] + - signature: "P12+P6+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP6, mrP5, mrP4, mrP3, mrP1] + - signature: "P12+P6+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP6, mrP5, mrP4, mrP3, mrP2] + - signature: "P12+P7+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P7+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP5, mrP3, mrP2, mrP1] + - signature: "P12+P7+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP5, mrP4, mrP2, mrP1] + - signature: "P12+P7+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP5, mrP4, mrP3, mrP1] + - signature: "P12+P7+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP7, mrP5, mrP4, mrP3, mrP2] + - signature: "P12+P7+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP3, mrP2, mrP1] + - signature: "P12+P7+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP4, mrP2, mrP1] + - signature: "P12+P7+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP4, mrP3, mrP1] + - signature: "P12+P7+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP4, mrP3, mrP2] + - signature: "P12+P7+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP5, mrP2, mrP1] + - signature: "P12+P7+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP5, mrP3, mrP1] + - signature: "P12+P7+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP5, mrP3, mrP2] + - signature: "P12+P7+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP5, mrP4, mrP1] + - signature: "P12+P7+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP5, mrP4, mrP2] + - signature: "P12+P7+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP7, mrP6, mrP5, mrP4, mrP3] + - signature: "P12+P8+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P8+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP5, mrP3, mrP2, mrP1] + - signature: "P12+P8+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP5, mrP4, mrP2, mrP1] + - signature: "P12+P8+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP5, mrP4, mrP3, mrP1] + - signature: "P12+P8+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP5, mrP4, mrP3, mrP2] + - signature: "P12+P8+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP3, mrP2, mrP1] + - signature: "P12+P8+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP4, mrP2, mrP1] + - signature: "P12+P8+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP4, mrP3, mrP1] + - signature: "P12+P8+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP4, mrP3, mrP2] + - signature: "P12+P8+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP5, mrP2, mrP1] + - signature: "P12+P8+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP5, mrP3, mrP1] + - signature: "P12+P8+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP5, mrP3, mrP2] + - signature: "P12+P8+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP5, mrP4, mrP1] + - signature: "P12+P8+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP5, mrP4, mrP2] + - signature: "P12+P8+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP8, mrP6, mrP5, mrP4, mrP3] + - signature: "P12+P8+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP3, mrP2, mrP1] + - signature: "P12+P8+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP4, mrP2, mrP1] + - signature: "P12+P8+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP4, mrP3, mrP1] + - signature: "P12+P8+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP4, mrP3, mrP2] + - signature: "P12+P8+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP5, mrP2, mrP1] + - signature: "P12+P8+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP5, mrP3, mrP1] + - signature: "P12+P8+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP5, mrP3, mrP2] + - signature: "P12+P8+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP5, mrP4, mrP1] + - signature: "P12+P8+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP5, mrP4, mrP2] + - signature: "P12+P8+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP5, mrP4, mrP3] + - signature: "P12+P8+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP2, mrP1] + - signature: "P12+P8+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP3, mrP1] + - signature: "P12+P8+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP3, mrP2] + - signature: "P12+P8+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP4, mrP1] + - signature: "P12+P8+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP4, mrP2] + - signature: "P12+P8+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP4, mrP3] + - signature: "P12+P8+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP5, mrP1] + - signature: "P12+P8+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP5, mrP2] + - signature: "P12+P8+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP5, mrP3] + - signature: "P12+P8+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP8, mrP7, mrP6, mrP5, mrP4] + - signature: "P12+P9+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP4, mrP3, mrP2, mrP1] + - signature: "P12+P9+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP5, mrP3, mrP2, mrP1] + - signature: "P12+P9+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP5, mrP4, mrP2, mrP1] + - signature: "P12+P9+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP5, mrP4, mrP3, mrP1] + - signature: "P12+P9+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP5, mrP4, mrP3, mrP2] + - signature: "P12+P9+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP3, mrP2, mrP1] + - signature: "P12+P9+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP4, mrP2, mrP1] + - signature: "P12+P9+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP4, mrP3, mrP1] + - signature: "P12+P9+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP4, mrP3, mrP2] + - signature: "P12+P9+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP5, mrP2, mrP1] + - signature: "P12+P9+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP5, mrP3, mrP1] + - signature: "P12+P9+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP5, mrP3, mrP2] + - signature: "P12+P9+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP5, mrP4, mrP1] + - signature: "P12+P9+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP5, mrP4, mrP2] + - signature: "P12+P9+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP6, mrP5, mrP4, mrP3] + - signature: "P12+P9+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP3, mrP2, mrP1] + - signature: "P12+P9+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP4, mrP2, mrP1] + - signature: "P12+P9+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP4, mrP3, mrP1] + - signature: "P12+P9+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP4, mrP3, mrP2] + - signature: "P12+P9+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP5, mrP2, mrP1] + - signature: "P12+P9+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP5, mrP3, mrP1] + - signature: "P12+P9+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP5, mrP3, mrP2] + - signature: "P12+P9+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP5, mrP4, mrP1] + - signature: "P12+P9+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP5, mrP4, mrP2] + - signature: "P12+P9+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP5, mrP4, mrP3] + - signature: "P12+P9+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP2, mrP1] + - signature: "P12+P9+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP3, mrP1] + - signature: "P12+P9+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP3, mrP2] + - signature: "P12+P9+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP4, mrP1] + - signature: "P12+P9+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP4, mrP2] + - signature: "P12+P9+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP4, mrP3] + - signature: "P12+P9+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP5, mrP1] + - signature: "P12+P9+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP5, mrP2] + - signature: "P12+P9+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP5, mrP3] + - signature: "P12+P9+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP9, mrP7, mrP6, mrP5, mrP4] + - signature: "P12+P9+P8+P3+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP3, mrP2, mrP1] + - signature: "P12+P9+P8+P4+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP4, mrP2, mrP1] + - signature: "P12+P9+P8+P4+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP4, mrP3, mrP1] + - signature: "P12+P9+P8+P4+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP4, mrP3, mrP2] + - signature: "P12+P9+P8+P5+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP5, mrP2, mrP1] + - signature: "P12+P9+P8+P5+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP5, mrP3, mrP1] + - signature: "P12+P9+P8+P5+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP5, mrP3, mrP2] + - signature: "P12+P9+P8+P5+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP5, mrP4, mrP1] + - signature: "P12+P9+P8+P5+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP5, mrP4, mrP2] + - signature: "P12+P9+P8+P5+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP5, mrP4, mrP3] + - signature: "P12+P9+P8+P6+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP2, mrP1] + - signature: "P12+P9+P8+P6+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP3, mrP1] + - signature: "P12+P9+P8+P6+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP3, mrP2] + - signature: "P12+P9+P8+P6+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP4, mrP1] + - signature: "P12+P9+P8+P6+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP4, mrP2] + - signature: "P12+P9+P8+P6+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP4, mrP3] + - signature: "P12+P9+P8+P6+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP5, mrP1] + - signature: "P12+P9+P8+P6+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP5, mrP2] + - signature: "P12+P9+P8+P6+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP5, mrP3] + - signature: "P12+P9+P8+P6+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP6, mrP5, mrP4] + - signature: "P12+P9+P8+P7+P2+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP2, mrP1] + - signature: "P12+P9+P8+P7+P3+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP3, mrP1] + - signature: "P12+P9+P8+P7+P3+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP3, mrP2] + - signature: "P12+P9+P8+P7+P4+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP4, mrP1] + - signature: "P12+P9+P8+P7+P4+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP4, mrP2] + - signature: "P12+P9+P8+P7+P4+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP4, mrP3] + - signature: "P12+P9+P8+P7+P5+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP5, mrP1] + - signature: "P12+P9+P8+P7+P5+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP5, mrP2] + - signature: "P12+P9+P8+P7+P5+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP5, mrP3] + - signature: "P12+P9+P8+P7+P5+P4+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP5, mrP4] + - signature: "P12+P9+P8+P7+P6+P1+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP6, mrP1] + - signature: "P12+P9+P8+P7+P6+P2+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP6, mrP2] + - signature: "P12+P9+P8+P7+P6+P3+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP6, mrP3] + - signature: "P12+P9+P8+P7+P6+P4+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP6, mrP4] + - signature: "P12+P9+P8+P7+P6+P5+K|xxxxxxk" + rules: [mrP12, mrP9, mrP8, mrP7, mrP6, mrP5] + - signature: "P6+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP6, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P7+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP7, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P7+P6+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP7, mrP6, mrP4, mrP3, mrP2, mrP1] + - signature: "P7+P6+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP7, mrP6, mrP5, mrP3, mrP2, mrP1] + - signature: "P7+P6+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP7, mrP6, mrP5, mrP4, mrP2, mrP1] + - signature: "P7+P6+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP7, mrP6, mrP5, mrP4, mrP3, mrP1] + - signature: "P7+P6+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP7, mrP6, mrP5, mrP4, mrP3, mrP2] + - signature: "P8+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P8+P6+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP6, mrP4, mrP3, mrP2, mrP1] + - signature: "P8+P6+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP6, mrP5, mrP3, mrP2, mrP1] + - signature: "P8+P6+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP6, mrP5, mrP4, mrP2, mrP1] + - signature: "P8+P6+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP8, mrP6, mrP5, mrP4, mrP3, mrP1] + - signature: "P8+P6+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP8, mrP6, mrP5, mrP4, mrP3, mrP2] + - signature: "P8+P7+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP4, mrP3, mrP2, mrP1] + - signature: "P8+P7+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP5, mrP3, mrP2, mrP1] + - signature: "P8+P7+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP5, mrP4, mrP2, mrP1] + - signature: "P8+P7+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP5, mrP4, mrP3, mrP1] + - signature: "P8+P7+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP8, mrP7, mrP5, mrP4, mrP3, mrP2] + - signature: "P8+P7+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP3, mrP2, mrP1] + - signature: "P8+P7+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP4, mrP2, mrP1] + - signature: "P8+P7+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP4, mrP3, mrP1] + - signature: "P8+P7+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP4, mrP3, mrP2] + - signature: "P8+P7+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP5, mrP2, mrP1] + - signature: "P8+P7+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP5, mrP3, mrP1] + - signature: "P8+P7+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP5, mrP3, mrP2] + - signature: "P8+P7+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP5, mrP4, mrP1] + - signature: "P8+P7+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP5, mrP4, mrP2] + - signature: "P8+P7+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP8, mrP7, mrP6, mrP5, mrP4, mrP3] + - signature: "P9+P5+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP5, mrP4, mrP3, mrP2, mrP1] + - signature: "P9+P6+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP6, mrP4, mrP3, mrP2, mrP1] + - signature: "P9+P6+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP6, mrP5, mrP3, mrP2, mrP1] + - signature: "P9+P6+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP6, mrP5, mrP4, mrP2, mrP1] + - signature: "P9+P6+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP6, mrP5, mrP4, mrP3, mrP1] + - signature: "P9+P6+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP6, mrP5, mrP4, mrP3, mrP2] + - signature: "P9+P7+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP4, mrP3, mrP2, mrP1] + - signature: "P9+P7+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP5, mrP3, mrP2, mrP1] + - signature: "P9+P7+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP5, mrP4, mrP2, mrP1] + - signature: "P9+P7+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP5, mrP4, mrP3, mrP1] + - signature: "P9+P7+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP7, mrP5, mrP4, mrP3, mrP2] + - signature: "P9+P7+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP3, mrP2, mrP1] + - signature: "P9+P7+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP4, mrP2, mrP1] + - signature: "P9+P7+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP4, mrP3, mrP1] + - signature: "P9+P7+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP4, mrP3, mrP2] + - signature: "P9+P7+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP5, mrP2, mrP1] + - signature: "P9+P7+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP5, mrP3, mrP1] + - signature: "P9+P7+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP5, mrP3, mrP2] + - signature: "P9+P7+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP5, mrP4, mrP1] + - signature: "P9+P7+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP5, mrP4, mrP2] + - signature: "P9+P7+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP9, mrP7, mrP6, mrP5, mrP4, mrP3] + - signature: "P9+P8+P4+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP4, mrP3, mrP2, mrP1] + - signature: "P9+P8+P5+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP5, mrP3, mrP2, mrP1] + - signature: "P9+P8+P5+P4+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP5, mrP4, mrP2, mrP1] + - signature: "P9+P8+P5+P4+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP5, mrP4, mrP3, mrP1] + - signature: "P9+P8+P5+P4+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP5, mrP4, mrP3, mrP2] + - signature: "P9+P8+P6+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP3, mrP2, mrP1] + - signature: "P9+P8+P6+P4+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP4, mrP2, mrP1] + - signature: "P9+P8+P6+P4+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP4, mrP3, mrP1] + - signature: "P9+P8+P6+P4+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP4, mrP3, mrP2] + - signature: "P9+P8+P6+P5+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP5, mrP2, mrP1] + - signature: "P9+P8+P6+P5+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP5, mrP3, mrP1] + - signature: "P9+P8+P6+P5+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP5, mrP3, mrP2] + - signature: "P9+P8+P6+P5+P4+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP5, mrP4, mrP1] + - signature: "P9+P8+P6+P5+P4+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP5, mrP4, mrP2] + - signature: "P9+P8+P6+P5+P4+P3+K|xxxxxxk" + rules: [mrP9, mrP8, mrP6, mrP5, mrP4, mrP3] + - signature: "P9+P8+P7+P3+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP3, mrP2, mrP1] + - signature: "P9+P8+P7+P4+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP4, mrP2, mrP1] + - signature: "P9+P8+P7+P4+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP4, mrP3, mrP1] + - signature: "P9+P8+P7+P4+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP4, mrP3, mrP2] + - signature: "P9+P8+P7+P5+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP5, mrP2, mrP1] + - signature: "P9+P8+P7+P5+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP5, mrP3, mrP1] + - signature: "P9+P8+P7+P5+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP5, mrP3, mrP2] + - signature: "P9+P8+P7+P5+P4+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP5, mrP4, mrP1] + - signature: "P9+P8+P7+P5+P4+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP5, mrP4, mrP2] + - signature: "P9+P8+P7+P5+P4+P3+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP5, mrP4, mrP3] + - signature: "P9+P8+P7+P6+P2+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP2, mrP1] + - signature: "P9+P8+P7+P6+P3+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP3, mrP1] + - signature: "P9+P8+P7+P6+P3+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP3, mrP2] + - signature: "P9+P8+P7+P6+P4+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP4, mrP1] + - signature: "P9+P8+P7+P6+P4+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP4, mrP2] + - signature: "P9+P8+P7+P6+P4+P3+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP4, mrP3] + - signature: "P9+P8+P7+P6+P5+P1+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP5, mrP1] + - signature: "P9+P8+P7+P6+P5+P2+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP5, mrP2] + - signature: "P9+P8+P7+P6+P5+P3+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP5, mrP3] + - signature: "P9+P8+P7+P6+P5+P4+K|xxxxxxk" + rules: [mrP9, mrP8, mrP7, mrP6, mrP5, mrP4] + + - word: "xxxxxxxxxxxxk" + note: >- + The all-slots-fired boundary case, the mirror image of "k": with all 12 x's present, every + single slot must have fired (no other subset of size 12 exists out of 12), so C(12,12) = 1 -- + back down to exactly one analysis, confirming the combinatorics collapse correctly at the + other extreme. Morph order is P12..P1..K because each later-firing slot's InsertSegments + prepends its "x" further to the left than every slot that fired before it (confirmed against + v1's own expected.tsv). + provenance: "F3 worked example (pathological category): 12 independent all-optional affix-template prefix slots inserting an identical literal character, producing C(12,k) equally-valid analyses for a k-x-prefixed word (924 for k=6, confirmed against the live oracle: 3 words, ~2.0s total wall-clock on this branch's un-optimized plain-master engine)" + parses: + - signature: "P12+P11+P10+P9+P8+P7+P6+P5+P4+P3+P2+P1+K|xxxxxxxxxxxxk" + rules: [mrP12, mrP11, mrP10, mrP9, mrP8, mrP7, mrP6, mrP5, mrP4, mrP3, mrP2, mrP1] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" diff --git a/conformance/edge-cases/disjunctive-recheck/grammar.xml b/conformance/edge-cases/disjunctive-recheck/grammar.xml new file mode 100644 index 000000000..c1b988a42 --- /dev/null +++ b/conformance/edge-cases/disjunctive-recheck/grammar.xml @@ -0,0 +1,203 @@ + + + + + DisjunctiveRecheckProbe + + + root + + + + + + ph + + w + a + o + k + t + p + i + z + d + g + r + e + y + + + + + Main + + w + a + o + k + t + p + i + z + d + g + r + e + y + + + + + Any + + + Tclass + + + + Kclass + + + + + + Main + + + + ta-suffix + + + + + + + + + + + + + ta + + + + + TA + + + + d-suffix + + + + + + + + + + + + + za + + + + + + + + + + + + + + + + + + + + + + + + + + da + + + + + D + + + + + + + wok + + + + + + + + + + + + + wak + + walk + + + + pak + + pak + + + + pit + + pit + + + + + gray + grey + + gray + + + + + + diff --git a/conformance/edge-cases/disjunctive-recheck/words.yaml b/conformance/edge-cases/disjunctive-recheck/words.yaml new file mode 100644 index 000000000..84d9d4085 --- /dev/null +++ b/conformance/edge-cases/disjunctive-recheck/words.yaml @@ -0,0 +1,180 @@ +# Converted from conformance/allomorphy/disjunctive-recheck (v1; kept in place, untouched, until +# phase G4 -- see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same file, +# unmodified: deliberately NO element on either MorphologicalRule (ruleT, ruleD) -- the +# v1 fixture pins signatures like "+|wokta" (an empty morph-chain component joined by "+"), which +# would change if a MorphemeId were added, so this fixture cannot gain one without breaking its own +# pin. This exposed a genuine GrammarRuleIndex gap while authoring this conversion (see the +# cross-cutting finding below); fixed in the same commit as this fixture's authoring. +# +# What this pins (from the v1 fixture's README.md/manifest.json, carried forward verbatim): C# +# Allomorph.IsWordValid's second loop (Allomorph.cs:127-152, introduced by history-matrix row 51 +# 987be2fd): a synthesized word is REJECTED when a passed-over disjunctive alternative -- an +# earlier-indexed allomorph of the same morpheme that does not free-fluctuate with the used one -- +# would ALSO have matched (its environments are absent or satisfied at the same morph position, and +# its other constraints hold). +# +# - Root disjunctivity (eWalk): allomorph 0 "wok" requires a following "t"; allomorph 1 "wak" is +# the elsewhere case. ruleT (unconstrained suffix "-ta") supplies the following "t". +# - Affix disjunctivity (ruleD): subrule 0 "-za" requires a preceding "k" (environment-constrained, +# so C#'s synthesis loop does not break after it and subrule 1 "-da" also fires, recording +# subrule 0 as passed-over). Roots pak/pit make the candidate's environment satisfied/unsatisfied. +# - Free-fluctuation escape (eGray): gray/grey, both unconstrained -- identical constraint sets +# free-fluctuate (Allomorph.FreeFluctuatesWith), so the later-indexed "grey" is NOT rejected. +# +# Every segment carries a unique featPh symbol specifically so a SegmentNaturalClass constraint does +# NOT degrade to "any segment" (the allomorphy/strrep-identity fixture is the deliberate opposite +# case) -- this fixture isolates the disjunctive re-check only. +# +# CROSS-CUTTING FINDING (GrammarRuleIndex, affects this fixture AND edge-cases/strrep-identity): +# both ruleT and ruleD genuinely fire during synthesis for several words below (e.g. ruleT fires for +# every "-ta"-suffixed word), but GrammarRuleIndex.ResolveMorphologicalRuleId could not attribute +# them at all before this commit -- it translates a runtime rule back to its grammar.xml id via +# Morpheme.Id (sourced from ), and neither rule declares one (see above). Self-check's +# traced-rules verification (RunnerV2.RunAllWords) therefore reported an empty actual set for every +# parse no matter what fired, which would force a dishonest "rules: []" on parses that visibly use +# ruleT/ruleD -- and separately made both rule ids permanently "dead" in rules.csv (CoverageReportV2 +# computes dead-rule status from declared "rules:"/"blocked_by", not the trace), which the ledger's +# floor forbids introducing. Fixed in GrammarRuleIndex.cs: when a MorphologicalRule/RealizationalRule +# has no at all, its text is registered in the SAME name-keyed fallback dict +# CompoundingRule/PhonologicalRule/MetathesisRule already use -- conservative because it only ever +# gains an entry in the no-MorphemeId case, so all 8 existing language grammars (which assign +# MorphemeId to every rule) are byte-for-byte unaffected. Confirmed empirically: before the fix, a +# draft "rules: [ruleT]" declaration on "wokta" FAILed self-check with "declared rules [ruleT] != +# traced []"; after the fix, the same declaration passes and the traced set is the honest, non-empty +# one used throughout this file. + +language: DisjunctiveRecheckProbe +inspired_by: ["synthetic allomorph-disjunctivity probe (not modeling any real language)"] +sources: + - "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" +requires: [] +words: + - word: wak + note: >- + eWalk's elsewhere allomorph "wak" bare (no suffix) -- allomorph 0 "wok"'s RightEnvironment + (following ncT) fails at word end, so no disjunctive rejection applies. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + parses: + - signature: "|wak" + gloss: walk + rules: [] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: wok + note: The used allomorph's OWN environment fails (no following "t" at all here) -- already-ported #5a own-environment gate, not the re-check this fixture adds. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + expect_fail: true + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: wakta + note: >- + THE distinguishing row (root disjunctivity): root allomorph "wak" (index 1) used with ruleT's + "-ta" suffix, but allomorph 0 "wok" (index 0)'s RightEnvironment (following ncT) is satisfied + at the root's own position (the "t" of "-ta" follows immediately) and the two allomorphs do + NOT free-fluctuate -- the disjunctive re-check REJECTS this synthesis. Zero parses, even + though "wak"+"-ta" concatenates cleanly and ruleT itself has no constraint against it. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + expect_fail: true + blocked_by: [ruleT] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: wokta + note: >- + The mirror-image acceptance: root allomorph "wok" (index 0) used, its OWN RightEnvironment + (following ncT) satisfied by ruleT's "-ta" -- no disjunctive rejection applies (index 0 has no + earlier-indexed alternative to be rejected in favor of). + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + parses: + - signature: "+|wokta" + gloss: walk-TA + rules: [ruleT] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: pak + note: PAK bare root, no suffix -- a plain, unrestricted control (no disjunctive allomorphs at all on this lexical entry). + parses: + - signature: "|pak" + gloss: pak + rules: [] + exercises: [] + + - word: pit + note: PIT bare root, no suffix -- the other plain control root used by the affix-disjunctivity rows below. + parses: + - signature: "|pit" + gloss: pit + rules: [] + exercises: [] + + - word: pakza + note: >- + PAK + ruleD subrule 0 ("-za", environment-constrained: preceding ncK) -- "pak" ends in "k", so + subrule 0's own environment is satisfied and it fires directly (no disjunctive-rejection + question for the USED subrule's own environment; that is the #5a gate, already correct). + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + parses: + - signature: "+|pakza" + gloss: pak-D + rules: [ruleD] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: pakda + note: >- + THE distinguishing row (affix disjunctivity): ruleD subrule 1 ("-da", elsewhere) used on + "pak", but subrule 0's environment (preceding ncK) is ALSO satisfied at the same morph + position (the stem still ends in "k") and subrule 0 was recorded as passed-over during + synthesis (it is environment-constrained, so C#'s loop does not break after it) -- the + disjunctive re-check REJECTS this synthesis. Zero parses. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + expect_fail: true + blocked_by: [ruleD] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: pitza + note: The used subrule's OWN environment fails ("pit" doesn't end in ncK) -- already-ported #5a own-environment gate, not the re-check this fixture adds. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + expect_fail: true + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: pitda + note: >- + The mirror-image acceptance: ruleD subrule 1 ("-da") used on "pit" -- subrule 0's candidate + environment (preceding ncK) fails ("pit" ends in "t", not "k"), so no disjunctive rejection + applies. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + parses: + - signature: "+|pitda" + gloss: pit-D + rules: [ruleD] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: gray + note: eGray's first allomorph "gray", unconstrained, no suffix -- establishes the free-fluctuation pair's baseline before "grey" below. + parses: + - signature: "|gray" + gloss: gray + rules: [] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: grey + note: >- + THE free-fluctuation escape: eGray's second allomorph "grey" -- allomorph 0 "gray" "would + also match" (both are totally unconstrained), but the two have IDENTICAL (empty) constraint + sets, so Allomorph.FreeFluctuatesWith recognizes them as free variants and short-circuits the + disjunctive rejection. "grey" is NOT rejected, unlike wakta/pakda above. + provenance: "W3.2, history row 51 (987be2fd) deferred disjunctive-allomorph re-check half" + parses: + - signature: "|grey" + gloss: gray + rules: [] + exercises: + - "Disjunctive allomorphs / free-fluctuation" diff --git a/conformance/edge-cases/latin-diacritic-segments/grammar.xml b/conformance/edge-cases/latin-diacritic-segments/grammar.xml new file mode 100644 index 000000000..7f8b88976 --- /dev/null +++ b/conformance/edge-cases/latin-diacritic-segments/grammar.xml @@ -0,0 +1,192 @@ + + + + + LatinDiacriticProbe + + + + root + + + + Main + + + a + c + e + f + g + k + l + o + s + t + + é + ö + ñ + î + + + + + + Any + + + + Diacritic + + + + + + + + + Main + + + + plural + + + + + + + + + + + + + s + + + + + PL + PL + + + + diminutive + + + + + + + + + + + + + ta + + + + + + + + + + + + + + + + DIM + DIM + + + + + suffixTemplate + + + suffix1 + + + + + + + + + año + + + YEAR + year + + + + + + café + + + COFFEE + coffee + + + + + + göl + + + LAKE + lake + + + + + + kelî + + + WORD + word + + + + + + diff --git a/conformance/edge-cases/latin-diacritic-segments/words.yaml b/conformance/edge-cases/latin-diacritic-segments/words.yaml new file mode 100644 index 000000000..789212307 --- /dev/null +++ b/conformance/edge-cases/latin-diacritic-segments/words.yaml @@ -0,0 +1,201 @@ +# What this pins: PanGloss's hc-foma engine (an alternative FST-based analyzer, not part of this +# repo) was found to return ZERO parses for any word containing a precomposed Latin diacritic (ñ, +# é, ö, î), while the default engine (ported from this repo's own C# HermitCrab) and Cyrillic text +# both parse those words fine. The bug is in hc-foma's own byte/token handling around Unicode +# NFD-decomposition, not in HermitCrab or in the grammar-model constructs any of this suite's 8 +# typological languages exercise (they use invented lexemes with plain ASCII segment inventories by +# design, so none of them can host this trigger naturalistically) -- per conformance/README.md's +# growth policy, this is therefore an edge-case (option 3: "engine-internal... a rule shape no real +# grammar would contain"), not a new word in an existing language. +# +# grammar.xml defines 4 roots, each carrying exactly one precomposed diacritic segment (é, ö, ñ, î +# -- the bug report's exact set) at a different position: +# - año (ñ MEDIAL, not stem-final) / göl (ö MEDIAL, not stem-final) +# - café (é stem-FINAL) / kelî (î stem-FINAL) +# Two optional, mutually-exclusive suffix rules exercise the affixed case (the actual bug affected +# both bare AND affixed diacritic-bearing words alike): +# - rPl ("-s"): unconditional, applies after any root regardless of its final segment. +# - rDim ("-ta"): GATED by a LeftEnvironment requiring the stem's final segment to be a member of +# ncDiacritic. This is the real regression pin, not just a copy-through check: café/kelî +# (diacritic-final) satisfy the gate and take "-ta"; año/göl (diacritic MEDIAL, not final) do +# not, so the SAME rule correctly fires on one pair and fails on the other. A byte/NFD-handling +# bug that mishandles diacritic segments could plausibly get one direction right and the other +# wrong (e.g. losing the segment's NaturalClass membership entirely -- the gate never fires even +# where it should -- or over-matching -- the gate wrongly fires where it shouldn't). +# One more word ("gül", built on "ü", a diacritic NOT declared anywhere in this grammar's +# CharacterDefinitionTable) pins the SKIPPED path: an undeclared diacritic must still surface as a +# clean InvalidShapeException/SKIPPED, not a silent zero-analysis "ok" or a crash. +# +# All signatures below are hand-derived (no multi-character segment representations exist in this +# grammar, so no HermitCrabExtensions.ToRegexString parenthesization applies -- see +# conformance/PROTOCOL.md section 3's addendum) and self-check-verified against the C# oracle before +# being checked in, per this suite's authoring discipline (signatures are authored-and-verified, +# never blindly regenerated). + +language: LatinDiacriticProbe +inspired_by: ["synthetic Latin-diacritic-segment probe (not modeling any real language)"] +sources: + - "PanGloss hc-foma engine: precomposed Latin diacritics (é/ö/ñ/î) yielding zero parses (NFD-decomposition / byte-handling bug in the alternative FST engine, not in HermitCrab)" +requires: [] +words: + - word: año + note: >- + Bare root, diacritic (ñ) MEDIAL -- not stem-final. Plain control: no affix rule fires (the + optional suffix slot is simply skipped), surface is the root unmutated. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "YEAR|año" + gloss: year + rules: [] + exercises: [] + + - word: café + note: >- + Bare root, diacritic (é) stem-FINAL. Plain control: no affix rule fires, surface is the root + unmutated. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "COFFEE|café" + gloss: coffee + rules: [] + exercises: [] + + - word: göl + note: >- + Bare root, diacritic (ö) MEDIAL -- not stem-final. Plain control: no affix rule fires. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "LAKE|göl" + gloss: lake + rules: [] + exercises: [] + + - word: kelî + note: >- + Bare root, diacritic (î) stem-FINAL. Plain control: no affix rule fires. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "WORD|kelî" + gloss: word + rules: [] + exercises: [] + + - word: años + note: >- + año + rPl ("-s"), the unconditional suffix -- exercises the affixed case on a + diacritic-MEDIAL root: rPl does not care about the stem's final segment, so it fires + regardless of where in the stem the diacritic sits. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "YEAR+PL|años" + gloss: year-PL + rules: [rPl] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: cafés + note: >- + café + rPl ("-s") -- the affixed case on a diacritic-FINAL root: the suffix attaches directly + after the diacritic segment itself, exercising CopyFromInput's preservation of a diacritic + segment immediately adjacent to an inserted affix boundary. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "COFFEE+PL|cafés" + gloss: coffee-PL + rules: [rPl] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: göls + note: göl + rPl ("-s"), the diacritic-MEDIAL affixed case, mirroring años. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "LAKE+PL|göls" + gloss: lake-PL + rules: [rPl] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: kelîs + note: kelî + rPl ("-s"), the diacritic-FINAL affixed case, mirroring cafés. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "WORD+PL|kelîs" + gloss: word-PL + rules: [rPl] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: caféta + note: >- + THE distinguishing acceptance row: café + rDim ("-ta"), whose LeftEnvironment gate requires + the stem's final segment to be a ncDiacritic member -- "café" ends in "é", satisfying the + gate, so rDim correctly fires. Pins that a diacritic segment is recognized as a genuine + NaturalClass member at the exact boundary position, not just copied through literally. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "COFFEE+DIM|caféta" + gloss: coffee-DIM + rules: [rDim] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + - "Affix template slots (obligatory/disjunctive/ordering)" + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: kelîta + note: >- + The mirror-image acceptance row: kelî + rDim ("-ta") -- "kelî" ends in "î", satisfying the + LeftEnvironment gate, same as caféta. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + parses: + - signature: "WORD+DIM|kelîta" + gloss: word-DIM + rules: [rDim] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + - "Affix template slots (obligatory/disjunctive/ordering)" + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: añota + note: >- + THE distinguishing rejection row: año + attempted rDim ("-ta") -- "año" ends in "o", NOT a + ncDiacritic member (the "ñ" is medial, not stem-final), so rDim's LeftEnvironment gate fails + and no valid derivation exists. Zero parses. An engine that over-matches (e.g. treats any + stem containing a diacritic ANYWHERE as gate-satisfying, rather than checking the exact + boundary-adjacent segment) would wrongly accept this. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + expect_fail: true + blocked_by: [rDim] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: gölta + note: >- + The mirror-image rejection row: göl + attempted rDim ("-ta") -- "göl" ends in "l", not a + ncDiacritic member ("ö" is medial), so the gate fails the same way as añota. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + expect_fail: true + blocked_by: [rDim] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: gül + note: >- + Built on "ü" (u-umlaut), a diacritic NOT declared anywhere in this grammar's + CharacterDefinitionTable (only é/ö/ñ/î are declared) -- the oracle's real status is SKIPPED + (InvalidShapeException), not a genuine zero-parse "ok". Pins that an undeclared diacritic + still surfaces as a clean SKIPPED rather than a silent "-" or a crash. + provenance: "PanGloss hc-foma Latin-diacritic zero-parse bug" + expect_skip: true + exercises: + - "Boundary markers (CharacterDefinitionTable)" diff --git a/conformance/edge-cases/loader-default-symbol/grammar.xml b/conformance/edge-cases/loader-default-symbol/grammar.xml new file mode 100644 index 000000000..d2bec7d77 --- /dev/null +++ b/conformance/edge-cases/loader-default-symbol/grammar.xml @@ -0,0 +1,188 @@ + + + + + N2-DefaultSymbol + + + Noun + + + + + + Nasal + + + + - + + + + Length + + - + + + + + + + SegId + + b + t + a + d + + + + + N2 Orthography + + + + + + b + + + + + + + + t + + + + + + + + + a + + + + + + + + d + + + + + + + + + aa + + + + + + + + Oral + + + + Long + + + + + + Lengthen oral segment + + + + + + + + + + + + + + + + + + + main + + + + + + bat + + + bat + + + + + + bdt + + + bdt + + + + + + diff --git a/conformance/edge-cases/loader-default-symbol/words.yaml b/conformance/edge-cases/loader-default-symbol/words.yaml new file mode 100644 index 000000000..9bcb260c3 --- /dev/null +++ b/conformance/edge-cases/loader-default-symbol/words.yaml @@ -0,0 +1,54 @@ +# Converted from conformance/loader/n2-default-symbol (v1; kept in place, untouched, until phase G4 +# -- see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same file, unmodified. +# +# What this pins (from the v1 fixture's README.md/manifest.json): C# XmlLanguageLoader.LoadFeature +# reads SymbolicFeature@defaultSymbol into SymbolicFeature.DefaultSymbolID, consumed only when +# useDefaults=true (the four phonological-rule matchers set this) -- for a pattern-pinned feature a +# data node leaves UNSET, substitute the feature's default value and check THAT, instead of treating +# "unset" as vacuously compatible with anything. Feature "nas" here declares +# defaultSymbol="symNasPlus" (+); segment "a" carries no explicit "nas" value; rule "pr1" rewrites +# ncOral (nas=-) targets to ncLong (len=+). +# +# "bat" is the UseDefaults signal: "a"'s effective nas is the default (+), which fails ncOral's +# nas=- pin, so pr1 must NOT fire during the confirming synthesis and the surface stays "bat". A +# defaults-unaware matcher (the pre-fix Rust bug: unconstrained lane overlaps any pin) would instead +# fire the rule, mutate "a" (len=long, char-def identity cleared -> renders as "aa", the only +# len=Long segment), produce surface "baat" != "bat", and report "-" (no parse) where the correct +# behavior is "|bat". +# +# "bdt" is the control (expected zero parses in BOTH engines): "d" is explicitly nas=- (an explicit +# value, not a default fallback), so pr1 legitimately fires on it during the confirming synthesis in +# both engines, and the resynthesized surface is no longer "bdt" -- pins that UseDefaults handling +# does not over-suppress a rule whose pin is satisfied by an explicit value, proving the "bat" +# divergence above is really about defaultSymbol, not some other loader/matcher bug. + +language: N2-DefaultSymbol +inspired_by: ["synthetic loader-semantics probe (not modeling any real language)"] +sources: + - "rust/parity-out/audit/phase2/C-loader-parity.md (Phase-2 audit C finding N2)" +requires: [phonology] +words: + - word: bat + note: >- + The UseDefaults signal: middle segment "a" carries no explicit "nas" value, so its effective + nas (the feature's declared default, +) fails ncOral's nas=- pin -- pr1 (Lengthen oral + segment) must NOT fire, and the surface stays "bat" unmutated. + provenance: "Phase-2 audit C finding N2 (SymbolicFeature@defaultSymbol / UseDefaults)" + parses: + - signature: "|bat" + gloss: bat + rules: [] + exercises: + - "Boundary markers (CharacterDefinitionTable)" + + - word: bdt + note: >- + Control (expected zero parses in both engines): middle segment "d" is EXPLICITLY nas=- (not a + default fallback), so pr1 legitimately fires during the confirming synthesis, mutating "d" so + the resynthesized surface is no longer "bdt" -- proves the bat/UseDefaults divergence is + really about defaultSymbol, not some other loader/matcher bug. + provenance: "Phase-2 audit C finding N2 (SymbolicFeature@defaultSymbol / UseDefaults)" + expect_fail: true + blocked_by: [pr1] + exercises: + - "Boundary markers (CharacterDefinitionTable)" diff --git a/conformance/edge-cases/loader-isactive/grammar.xml b/conformance/edge-cases/loader-isactive/grammar.xml new file mode 100644 index 000000000..0b58d74ca --- /dev/null +++ b/conformance/edge-cases/loader-isactive/grammar.xml @@ -0,0 +1,138 @@ + + + + + N1-IsActive + + + Noun + + + + + + Voice + + + + - + + + + + SegId + + a + o + t + d + k + s + + + + + + Draft + + x + + + + + N1 Orthography + + + + a + + + + + + o + + + + + + t + + + + + + + d + + + + + + + k + + + + + + s + + + + + + + + + + + + + + + + Voiced + + + + + + main + + + + + kat + + + kat + + + + + sod + + + sod + + + + + + diff --git a/conformance/edge-cases/loader-isactive/words.yaml b/conformance/edge-cases/loader-isactive/words.yaml new file mode 100644 index 000000000..f103e9877 --- /dev/null +++ b/conformance/edge-cases/loader-isactive/words.yaml @@ -0,0 +1,39 @@ +# Converted from conformance/loader/n1-isactive (v1; kept in place, untouched, until phase G4 -- see +# docs/conformance-language-suite-plan.md section 5, "the old fixture tree is deleted only when the +# ledger is complete and section 7's gates pass"). grammar.xml is the same file, unmodified. +# +# What this pins (from the v1 fixture's README.md/manifest.json): C# XmlLanguageLoader.LoadLanguage +# selects the phonological feature system with +# Elements("PhonologicalFeatureSystem").SingleOrDefault(IsActive) (XmlLanguageLoader.cs:239) -- the +# one *active* block among the two grammar.xml declares. A loader that ignores isActive and takes +# the LAST block encountered instead would select the wrong (inactive, "junk"-only) block, and the +# NaturalClasses' FeatureValue feature="voi" reference would fail to resolve, failing to load the +# whole grammar. Both this grammar loading successfully AND its two words parsing to plain literal +# signatures (no char-def-set bracket notation, thanks to the "seg6" disambiguating feature) is what +# this fixture pins. + +language: N1-IsActive +inspired_by: ["synthetic loader-semantics probe (not modeling any real language)"] +sources: + - "rust/parity-out/audit/phase2/C-loader-parity.md (Phase-2 audit C finding N1)" +requires: [] +words: + - word: kat + note: Plain lexical root, no affixes -- proves the grammar loaded at all (the active PhonologicalFeatureSystem block's "voi" feature resolved) and that its signature renders as a plain literal string. + provenance: "Phase-2 audit C finding N1 (PhonologicalFeatureSystem@isActive)" + parses: + - signature: "|kat" + gloss: kat + rules: [] + exercises: + - "Boundary markers (CharacterDefinitionTable)" + + - word: sod + note: Second plain lexical root -- the same isActive-selection proof as "kat", from the language's other lexical entry. + provenance: "Phase-2 audit C finding N1 (PhonologicalFeatureSystem@isActive)" + parses: + - signature: "|sod" + gloss: sod + rules: [] + exercises: + - "Boundary markers (CharacterDefinitionTable)" diff --git a/conformance/edge-cases/loader-pattern-shapes/grammar.xml b/conformance/edge-cases/loader-pattern-shapes/grammar.xml new file mode 100644 index 000000000..3148e22db --- /dev/null +++ b/conformance/edge-cases/loader-pattern-shapes/grammar.xml @@ -0,0 +1,80 @@ + + + + + N3-PatternShapes + + + Noun + + + + N3 Orthography + + + + b + + + + + t + + + + + a + + + + + e + + + + + + + + Vowel + + + + + + + main + + + + + + b[Vowel]t + + + bVt + + + + + + b([Vowel])t + + + bt + + + + + + diff --git a/conformance/edge-cases/loader-pattern-shapes/words.yaml b/conformance/edge-cases/loader-pattern-shapes/words.yaml new file mode 100644 index 000000000..556db42f8 --- /dev/null +++ b/conformance/edge-cases/loader-pattern-shapes/words.yaml @@ -0,0 +1,89 @@ +# Converted from conformance/loader/n3-pattern-shapes (v1; kept in place, untouched, until phase G4 +# -- see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same file, unmodified. +# +# What this pins (from the v1 fixture's README.md/manifest.json): C# +# CharacterDefinitionTable.GetShapeNodes(str, allowPattern, ...) has a pattern-language fallback +# active only when allowPattern=true: at any position where no literal character-definition +# substring matches, it additionally accepts "[NatClassName]" (bracketed natural-class reference, +# looked up by the class's text, not its XML id), "([NatClass])" (optional group), and +# "[NatClass]*" (Kleene star). Exactly one caller passes allowPattern=true: LoadRootAllomorph, i.e. +# plain lexicon root-allomorph shapes. This grammar's one lexical entry has PhoneticShape +# "b[Vowel]t": "b"/"t" match literally, "[Vowel]" doesn't match anything literally at that position, +# so it falls back to the Vowel natural class ({a, e}), giving signature "|b[ae]t" for any input that +# unifies against that pattern-derived shape node. +# +# Two-part status (carried from the v1 README, important context for any future Rust-adapter run +# against this fixture): the LOADER half (pinned here) is what N3 tests -- pre-fix, hc-grammar's +# load_root_allomorph had no pattern-language implementation at all and silently dropped the whole +# entry. The RUNTIME-MATCHING half (root_trie's CdSet-aware edge matching) is a documented, separate, +# NOT-yet-fixed follow-on gap in the Rust port (a pattern-derived trie edge born with NO_CHAR_DEF +# doesn't match a concrete query char_def) -- out of scope for this loader-semantics fixture, whose +# job is only to pin what the LOADER produces and what the ORACLE (C#) does with it. +# +# "bit": "i" is not declared anywhere in this grammar's CharacterDefinitionTable, so the C# oracle's +# own real status for this word is SKIPPED (InvalidShapeException), not "ok" with zero analyses -- +# v1's expected.tsv row is `2 bit 0 SKIPPED -`. This is the word's expect_skip: true below (as +# opposed to expect_fail, which is a genuine zero-parse "ok"). Both harness modes honour the +# distinction: self-check (RunnerV2.RunAllWords) confirms an InvalidShapeException actually fired, and +# adapter mode (FixtureMaterializer) materializes status "SKIPPED", so a conforming external adapter +# that reports SKIPPED for "bit" matches rather than registering a status mismatch. + +language: N3-PatternShapes +inspired_by: ["synthetic loader-semantics probe (not modeling any real language)"] +sources: + - "rust/parity-out/audit/phase2/C-loader-parity.md (Phase-2 audit C finding N3)" +requires: [] +words: + - word: bat + note: >- + "b[Vowel]t"'s pattern-derived middle position matches "a" -- the whole word unifies against + the one lexical entry's pattern-derived shape node, rendering as the char-def-set bracket + notation "[ae]" (both Vowel-class members), not the literal "a". + provenance: "Phase-2 audit C finding N3 (root-allomorph PhoneticShape pattern-language fallback)" + parses: + - signature: "|b[ae]t" + gloss: bVt + rules: [] + exercises: + - "Boundary markers (CharacterDefinitionTable)" + + - word: bet + note: >- + Same pattern-derived shape node as "bat", matched via its other Vowel-class member ("e") + instead -- proves the fallback is a genuine natural-class match, not a literal-string + coincidence limited to "a". + provenance: "Phase-2 audit C finding N3 (root-allomorph PhoneticShape pattern-language fallback)" + parses: + - signature: "|b[ae]t" + gloss: bVt + rules: [] + exercises: + - "Boundary markers (CharacterDefinitionTable)" + + - word: bit + note: >- + "i" is not declared anywhere in this grammar's CharacterDefinitionTable at all, so the oracle's + real status is SKIPPED (InvalidShapeException), not a genuine zero-parse "ok" -- hence + expect_skip, not expect_fail. Self-check confirms the InvalidShapeException actually fires; + adapter mode materializes status "SKIPPED" (v1's expected.tsv row was "2 bit 0 SKIPPED -"), so + an adapter that legitimately reports SKIPPED matches instead of registering a status mismatch. + provenance: "Phase-2 audit C finding N3 (root-allomorph PhoneticShape pattern-language fallback)" + expect_skip: true + exercises: + - "Boundary markers (CharacterDefinitionTable)" + + - word: bt + note: >- + G5 (code-coverage-driven): the second lexical entry's pattern "b([Vowel])t" makes the middle + position OPTIONAL in the LOADER's shape (the one CharacterDefinitionTable.GetShapeNodes + pattern-language branch N3 didn't already cover -- the loader itself runs this branch and + builds the optional-annotated shape node regardless of what happens next, so the loader code + is exercised either way). Reconciliation finding: at RUNTIME, root-allomorph analysis + matching does not actually honor the shape node's Optional annotation the way a phonological + environment match does -- "bt" gets zero parses rather than matching with the vowel position + skipped. Pinned as the real, verified oracle behavior, not the hand-derivation's original + guess (a genuine engine finding, not a fixture bug). + provenance: "G5 coverage sweep, CharacterDefinitionTable.GetShapeNodes optional-group '(...)' branch" + expect_fail: true + exercises: + - "CharacterDefinitionTable pattern shapes: optional group / Kleene star" diff --git a/conformance/edge-cases/simultaneous-epenthesis-cascade/grammar.xml b/conformance/edge-cases/simultaneous-epenthesis-cascade/grammar.xml new file mode 100644 index 000000000..692bf21b0 --- /dev/null +++ b/conformance/edge-cases/simultaneous-epenthesis-cascade/grammar.xml @@ -0,0 +1,143 @@ + + + + + IterativeEpenthesisCascadeProbe + + + n + + + + + cons + + + + - + + + + voc + + + + - + + + + high + + + + - + + + + back + + + + - + + + + round + + + + - + + + + + + Main + + + + b + + + + + + + + + + i + + + + + + + + + + u + + + + + + + + + + + + HighVowel + + + + + + HFUVowel + + + + + + + + + + + rule4 + + + + + + + + + + + + + + + + + + + + + + + + Main + + + + + bubu + + + 19 + + + + + + diff --git a/conformance/edge-cases/simultaneous-epenthesis-cascade/words.yaml b/conformance/edge-cases/simultaneous-epenthesis-cascade/words.yaml new file mode 100644 index 000000000..95b7f2b1d --- /dev/null +++ b/conformance/edge-cases/simultaneous-epenthesis-cascade/words.yaml @@ -0,0 +1,39 @@ +# Converted from conformance/rewrite/simultaneous-epenthesis-cascade (v1; kept in place, untouched, +# until phase G4 -- see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same +# file, unmodified. expect_crash semantics (front matter, below) must match v1's exactly: the engine +# under test is expected to ALSO crash (loading the grammar or parsing the word) for this fixture to +# PASS; a harness-level problem (bad adapter, process fails to start, timeout) is never treated as a +# crash match and always FAILs regardless of expect_crash -- see conformance/PROTOCOL.md's +# "expectCrash" section, unchanged by this conversion. +# +# What this pins (from the v1 fixture's README.md/manifest.json provenance, carried forward +# verbatim): P13 synthetic oracle -- a hand-designed probe (not derived from a C# unit test) of the +# OTHER direction of the Simultaneous-vs-Iterative divergence for epenthesis (LHS-empty) subrules: +# the rule inserts an HFU vowel ("i") after any HighVowel, no RightEnvironment, tagged Iterative (the +# grammar carries no multipleApplicationOrder="simultaneous" attribute). Because C#'s epenthesis +# substitute LHS pattern never adds a Modified=Clean filter, IterativePhonologicalPatternRule.Apply's +# freshly-inserted "i" node is itself a HighVowel and gets revisited by the very next iteration of +# the same outer loop, satisfying its own LeftEnvironment and re-triggering insertion -- a runaway +# self-feeding cascade that only stops when EpenthesisSynthesisRewriteSubruleSpec.ApplyRhs's own hard +# cap (Shape.Count == 256) throws InfiniteLoopException. BatchCommand.ParseOneWord only catches +# InvalidShapeException, so this kills the whole batch run rather than recording a per-word result -- +# expected.tsv's v1 ground truth was only the STARTED sentinel for word 0, never a completed row; +# this fixture's materialized expected.tsv (built by FixtureMaterializer for adapter mode) reproduces +# that same STARTED-only shape. Keep words.txt/this file to a single word: a crash aborts the whole +# word loop, so anything after the crashing word would never actually be exercised (same reasoning +# as v1 -- see PROTOCOL.md's "Because a crash aborts a fixture's whole word loop..." note). + +language: IterativeEpenthesisCascadeProbe +inspired_by: ["synthetic RewriteRule Iterative-vs-Simultaneous stress probe (not modeling any real language)"] +sources: + - "rust/docs/p13-simultaneous-design.md section 5" +requires: [phonology] +expect_crash: true +words: + - word: bubu + note: >- + Lexical root "bubu" alone (no affixes needed -- this only needs to exercise synthesis, i.e. + the forward phonological-rule direction). Parsing this word crashes the oracle with an + uncaught InfiniteLoopException partway through, per the fixture's front-matter expect_crash. + provenance: "P13 synthetic oracle: Iterative self-feeding epenthesis crashes; Simultaneous cannot" + expect_fail: true diff --git a/conformance/edge-cases/strrep-identity/grammar.xml b/conformance/edge-cases/strrep-identity/grammar.xml new file mode 100644 index 000000000..28b58b507 --- /dev/null +++ b/conformance/edge-cases/strrep-identity/grammar.xml @@ -0,0 +1,204 @@ + + + + + StrRepIdentityProbe + + + root + + + + + Main + + m + w + u + p + a + t + i + n + d + + + + + + + + + + + + ^0 + + + + + + + + Any + + + + Vclass + + + + + + Main + + + + class-prefix + + + + + + + + + + + + + mw+ + + + + + + + + + + + + + + + ^0+ + + + + + + + + + + + + + + + mu+ + + + + + + CL + + + + obj-prefix + + + + + + + + + + + + i+ + + + + + + + + + + m + + + + + + + + + + + + + + + + + + nd+ + + + + + + OBJ + + + + + + pat + + pat + + + + uta + + uta + + + + mat + + mat + + + + + + diff --git a/conformance/edge-cases/strrep-identity/words.yaml b/conformance/edge-cases/strrep-identity/words.yaml new file mode 100644 index 000000000..0e02ce2a2 --- /dev/null +++ b/conformance/edge-cases/strrep-identity/words.yaml @@ -0,0 +1,214 @@ +# Converted from conformance/allomorphy/strrep-identity (v1; kept in place, untouched, until phase +# G4 -- see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same file, +# unmodified: deliberately NO PhonologicalFeatureSystem at all (every char-def's feature bundle is +# empty) and NO element on either MorphologicalRule (rulePfx, ruleObj) -- like +# edge-cases/disjunctive-recheck, this fixture's own signatures ("+|mu+?pat" etc.) would change if a +# MorphemeId were added, so it cannot gain one. See disjunctive-recheck's words.yaml header for the +# GrammarRuleIndex cross-cutting finding this fixture also depends on (fixed in the same commit). +# +# What this pins (from the v1 fixture's README.md/manifest.json, carried forward verbatim): C# makes +# character identity a real matching dimension -- every char-def's feature struct carries a StrRep +# (the segment's own representation string), and SegmentNaturalClass/literal-segment +# constraints/environments consult it. On a grammar with NO phonological features at all (like Sena, +# and like this fixture), StrRep is the ONLY thing distinguishing one segment from another; an engine +# that drops this dimension degenerates every such constraint to "matches any segment" -- the exact +# gap behind Sena's null-allomorph coverage loss (this suite's single largest historical divergence +# class). +# +# - rulePfx (the Bantu mu-2/mu-3 class-prefix analog -- the DISJUNCTIVE-BREAK probe): subrule 0 +# ("mw+") requires a stem-initial ncV member ("u"); subrules 1 (the null "^0+") and 2 ("mu+") +# share identical LHS/constraints, so they free-fluctuate and BOTH fire once reached. On a +# consonant-initial stem C# skips subrule 0 (StrRep mismatch) and reaches the null subrule; an +# engine that lets subrule 0 match "any segment" breaks right after it and never realizes the +# zero allomorph. +# - ruleObj (the Bantu ndi- analog -- the ENVIRONMENT-RECHECK probe, P10 part 2): subrule 0 ("i+") +# carries a literal-segment RequiredEnvironment (following "m"): because it is +# environment-constrained, C#'s synthesis loop does not break after it, subrule 1 ("nd+") also +# fires, and subrule 0 lands in the nd-morph's passed-over set. At final validity +# (Allomorph.cs:127-152) an "nd+" parse is rejected only when subrule 0's environment is ALSO +# satisfied at the same morph position (stem starts with a real "m"). +# +# Both rules are on the same posRoot part of speech with morphologicalRuleOrder="unordered", so they +# can stack in either linear order -- ndpat/imat below each have 3 distinct valid analyses (rulePfx +# and ruleObj both applied, in either order; or ruleObj alone), all oracle-verified. + +language: StrRepIdentityProbe +inspired_by: ["synthetic StrRep-identity probe (not modeling any real language)"] +sources: + - "P10, StrRep identity dimension on a feature-less grammar" +requires: [] +words: + - word: pat + note: >- + PAT bare root -- ambiguous between two valid analyses: the null-prefixed one (rulePfx's + subrule 1, "^0+", a genuine zero allomorph that free-fluctuates with "mu+") and the plain bare + root. Both surface identically as "pat" but are distinct MorphemeId-chain analyses (the + shared, deliberate combinatorial-identity trick this whole fixture probes). + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "+|[(^0)∅]?+?pat" + gloss: pat-CL + rules: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - signature: "|pat" + gloss: pat + rules: [] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: uta + note: UTA bare root, no prefix -- a plain control; also the sole ncV ("Vclass") member, used by mwuta/muuta below. + parses: + - signature: "|uta" + gloss: uta + rules: [] + exercises: [] + + - word: mat + note: MAT bare root -- the same null-prefix/bare-root ambiguity as "pat", on the root ruleObj's own-environment rows below also use. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "+|[(^0)∅]?+?mat" + gloss: mat-CL + rules: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - signature: "|mat" + gloss: mat + rules: [] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: mupat + note: >- + PAT + rulePfx subrule 2 ("mu+", the free-fluctuation partner of the null subrule) -- "pat" is + consonant-initial, so subrule 0 ("mw+") is correctly skipped (StrRep mismatch on its + stem-initial ncV requirement) and subrule 2 is reached and fires. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "+|mu+?pat" + gloss: pat-CL + rules: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: mumat + note: The mu+ variant of the mat/rulePfx pair, mirroring mupat on the other root. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "+|mu+?mat" + gloss: mat-CL + rules: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: mwpat + note: >- + rulePfx subrule 0 ("mw+") requires the FOLLOWING stem to start with a ncV member ("u") -- "pat" + is consonant-initial, so subrule 0 cannot apply here at all, and no other subrule produces + surface "mw+pat" (subrules 1/2 insert "^0+"/"mu+", not "mw+"). Zero parses: this is the + StrRep-identity gate itself -- an engine that lets "mw+" match a consonant-initial stem (the + pre-fix Rust bug) would wrongly accept this. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + expect_fail: true + blocked_by: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: mwuta + note: >- + UTA + rulePfx subrule 0 ("mw+") -- "uta" starts with "u" (the sole ncV member), so subrule 0's + stem-initial requirement is satisfied and it fires; C#'s synthesis loop breaks immediately + after this ordinary (non-environment-constrained) match, so subrules 1/2 are never reached for + a vowel-initial stem (see muuta below). + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "+|mw+?uta" + gloss: uta-CL + rules: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: muuta + note: >- + The mirror-image gate of mwpat: "uta" is vowel-initial, so rulePfx subrule 0 ("mw+") matches + and the loop breaks right after it -- subrule 2 ("mu+") is never reached for this stem, so + surface "mu+uta" has no valid derivation. Zero parses. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + expect_fail: true + blocked_by: [rulePfx] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: ndpat + note: >- + PAT + ruleObj subrule 1 ("nd+"), optionally ALSO with rulePfx's null subrule stacked (the two + rules share posRoot with morphologicalRuleOrder="unordered", so both linear orders are valid, + plus the ruleObj-alone analysis) -- three distinct, equally-valid analyses, all surfacing + "ndpat" (the null prefix's "^0+"/"∅" boundary is invisible in the literal string). Also + pins C# GetSkippedOptionalNodes: a word-initial run of optional (boundary) nodes left of the + captured stem range is folded into the copy, so the medial zero allomorph survives ruleObj's + own stem-copy instead of being silently dropped. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "++|[(^0)∅]?+?nd+?pat" + rules: [rulePfx, ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - signature: "++|nd+?[(^0)∅]?+?pat" + rules: [rulePfx, ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - signature: "+|nd+?pat" + gloss: pat-OBJ + rules: [ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: ndmat + note: >- + THE distinguishing row (environment re-check): ruleObj subrule 1 ("nd+") used on "mat", but + subrule 0's RequiredEnvironment (following "m") is ALSO satisfied at the same morph position + (the stem is "mat", which starts with "m") and subrule 0 was recorded as passed-over -- the + disjunctive re-check REJECTS this synthesis (right answer for the C# oracle here, but for a + pre-fix engine that over-matches literal segments in environments, EVERY "nd+" parse would be + wrongly rejected this way -- Sena's "kundiphemba" gap). Zero parses. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + expect_fail: true + blocked_by: [ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: imat + note: >- + MAT + ruleObj subrule 0 ("i+", RequiredEnvironment following "m" is satisfied by the "m" of + "mat" itself) -- the mirror-image acceptance of ndmat, with the same rulePfx-stacking + ambiguity as ndpat (three analyses: subrule 0 alone, or stacked with rulePfx's null subrule in + either linear order). + provenance: "P10, StrRep identity dimension on a feature-less grammar" + parses: + - signature: "++|[(^0)∅]?+?i+?mat" + rules: [rulePfx, ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - signature: "++|i+?[(^0)∅]?+?mat" + rules: [rulePfx, ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - signature: "+|i+?mat" + gloss: mat-OBJ + rules: [ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + + - word: ipat + note: >- + ruleObj subrule 0 ("i+") requires a following "m" -- "pat" starts with "p", so subrule 0's own + environment fails outright (the already-ported #5a own-environment gate, not the re-check), + and no other subrule produces surface "i+pat" (subrule 1 inserts "nd+", not "i+"). Zero parses. + provenance: "P10, StrRep identity dimension on a feature-less grammar" + expect_fail: true + blocked_by: [ruleObj] + exercises: + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" diff --git a/conformance/edge-cases/truncate-morphotactic/grammar.xml b/conformance/edge-cases/truncate-morphotactic/grammar.xml new file mode 100644 index 000000000..6ece9e0ed --- /dev/null +++ b/conformance/edge-cases/truncate-morphotactic/grammar.xml @@ -0,0 +1,218 @@ + + + + + TruncateRulesProbe + + + v + + + + + cons + + + + - + + + + cont + + + + - + + + + place + + alveolar + velar + bilabial + + + + height + + low + high + + + + back + + + + - + + + + + + Main + + + + s + + + + + + + + g + + + + + + + + b + + + + + + + + a + + + + + + + u + + + + + + + + i + + + + + + + + + + Any + + + Fricative + + + + + + + Main + + + + truncate_trail_literal + + + + + + + + + + + + + + + + + + 3SGA + + + + truncate_lead_class + + + + + + + + + + + + + + + + + + 3SGB + + + + truncate_optional_insert + + + + + + + + + + + + + + + + + g + + + + + + 3SGC + + + + + + + sag + + + 32 + + + + + sas + + + 33 + + + + + bubibi + + + 42 + + + + + + diff --git a/conformance/edge-cases/truncate-morphotactic/words.yaml b/conformance/edge-cases/truncate-morphotactic/words.yaml new file mode 100644 index 000000000..0f43734f9 --- /dev/null +++ b/conformance/edge-cases/truncate-morphotactic/words.yaml @@ -0,0 +1,158 @@ +# Converted from conformance/affix-shapes/truncate (v1; kept in place, untouched, until phase G4 -- +# see docs/conformance-language-suite-plan.md section 5). grammar.xml is the same file, unmodified: +# it carries a PhonologicalFeatureSystem (feeding the NaturalClasses the morphotactic rules below +# pattern-match against) but declares NO PhonologicalRule/MetathesisRule element anywhere, so +# RequiresDerivation mechanically derives requires: [] -- truncation here is pure AffixProcessRule +# morphotactics, no phonological rule involved at all. +# +# THE XAMPLE FLOOR (why this fixture exists as its own edge-case rather than folding only into +# austronesian-phase): austronesian-phase's own truncate word (mrTrunc, "pure"->"pur", SAME +# provenance string as this fixture's own words below, carried deliberately -- both pin the identical +# W9.1 construct) lives in a requires:[phonology] grammar (austronesian-phase has real +# PhonologicalRule elements elsewhere), so it cannot serve as the suite's requires:[] home for +# truncation coverage. docs/conformance-migration-ledger.md's ledger row for affix-shapes/truncate +# names this gap explicitly ("XAmple floor: no natural requires-[] home -> edge-cases/ +# truncate-morphotactic duplicate") -- this fixture is that duplicate requires:[] home, so an +# XAmple-class (phonology-free) engine still gets truncation coverage. +# +# Also carries no on any MorphologicalRule (mruleTruncTrail/mruleTruncLead/ +# mruleTruncOptIns) or LexicalEntry (root32/root33/root42) -- same v1 authoring choice as +# edge-cases/disjunctive-recheck and edge-cases/strrep-identity (adding one would change the +# "+|sa"-style signatures this fixture pins), and it exercises the SAME GrammarRuleIndex Name-based +# fallback those two fixtures' commit introduced. +# +# What this pins (from the v1 fixture's README.md, carried forward verbatim): minimized from +# AffixProcessRuleTests.TruncateRules's 5 sub-cases down to 3 structurally distinct rules sharing one +# grammar -- +# - mruleTruncTrail (case 1/4 combined): a TRAILING segment matched by LITERAL identity +# (, not a natural class) is truncated -- root "sag" -> "sa". +# - mruleTruncLead (case 2/3 combined): a LEADING segment matched by a NATURAL CLASS (Fricative = +# cons+,cont+) is truncated -- root "sag" -> "ag", root "sas" -> "as". +# - mruleTruncOptIns (case 5): an OPTIONAL leading segment combined with an InsertSegments in the +# SAME Rhs -- root "sas" -> "gas" (optional "s" consumed, "g" inserted), root "bubibi" -> "gbubibi" +# (optional part empty, "g" inserted). +# Because morphologicalRuleOrder="unordered", the oracle finds an UNPLANNED but legitimate SECOND +# analysis for "gas": root "sas" --(mruleTruncLead, strips "s")--> "as" +# --(mruleTruncOptIns, optional part empty, inserts "g")--> "gas" -- two rule applications composing +# to the same surface form as the direct one-rule mruleTruncOptIns analysis. Both signatures are +# distinct (leading "+"-count differs: "++" for the 2-hop chain, "+" for the 1-hop direct analysis) +# and both are required; this pins the boundary-count marker surviving a pure-truncation hop with no +# new output material (LT-21939 / HISTORY-MATRIX row 11's "floating marker" fix, referenced in the +# v1 README for the Rust port's own parity history -- not itself a claim about this suite's oracle, +# which is C# throughout). + +language: TruncateRulesProbe +inspired_by: ["synthetic AffixProcessRule truncation probe (not modeling any real language)"] +sources: + - "W9.1 probe, AffixProcessRuleTests.TruncateRules" +requires: [] +words: + - word: sag + note: root32 ("sag") bare, no rule applied -- a plain control proving the root itself is valid before any truncation rule touches it. + parses: + - signature: "|sag" + gloss: "32" + rules: [] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: sas + note: root33 ("sas") bare, no rule applied -- the other plain control root used by the leading-truncation/optional-insertion rows below. + parses: + - signature: "|sas" + gloss: "33" + rules: [] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: bubibi + note: root42 ("bubibi") bare, no rule applied -- the plain control root used by the optional-insertion row (gbubibi) below. + parses: + - signature: "|bubibi" + gloss: "42" + rules: [] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: sa + note: >- + root32 ("sag") + mruleTruncTrail: the trailing segment matched by LITERAL identity + (, not a natural class) is truncated -- "sag" -> "sa". + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + parses: + - signature: "+|sa" + gloss: "32-3SGA" + rules: [mruleTruncTrail] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: ag + note: >- + root32 ("sag") + mruleTruncLead: the leading segment matched by the Fricative natural class + (cons+,cont+, matching "s") is truncated -- "sag" -> "ag". + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + parses: + - signature: "+|ag" + gloss: "32-3SGB" + rules: [mruleTruncLead] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: as + note: root33 ("sas") + mruleTruncLead, the same leading-fricative truncation as "ag" but on the other root -- "sas" -> "as". + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + parses: + - signature: "+|as" + gloss: "33-3SGB" + rules: [mruleTruncLead] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: gbubibi + note: >- + root42 ("bubibi") + mruleTruncOptIns: "bubibi" does not start with "s", so the rule's optional + leading segment matches EMPTY (0 occurrences) and the whole stem is copied, with "g" inserted + in front -- "bubibi" -> "gbubibi". + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + parses: + - signature: "+|gbubibi" + gloss: "42-3SGC" + rules: [mruleTruncOptIns] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: gas + note: >- + THE distinguishing row (unplanned second analysis via morphologicalRuleOrder="unordered"): + root33 ("sas") admits TWO independent, equally valid derivations of "gas" -- + (1) the DIRECT one-rule analysis: mruleTruncOptIns's optional leading segment matches the + stem-initial "s" itself (consumed, not copied) and inserts "g" in front of the remainder + "as" -> "gas"; and + (2) the CHAINED two-rule analysis: mruleTruncLead first strips the leading fricative "s" + ("sas" -> "as"), then mruleTruncOptIns applies to THAT result with its optional part empty + (since "as" doesn't start with "s") and inserts "g" -> "gas". + Both produce byte-identical surface text but are distinct MorphemeId-chain analyses (the + chained one carries one more empty-morpheme "+" boundary marker than the direct one) -- a + conforming engine must report BOTH. + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + parses: + - signature: "++|gas" + gloss: "33-3SGB-3SGC" + rules: [mruleTruncLead, mruleTruncOptIns] + exercises: + - "AffixProcessRule: subtraction/truncation" + - signature: "+|gas" + gloss: "33-3SGC" + rules: [mruleTruncOptIns] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: bag + note: >- + No lexical entry or rule combination produces "bag" at all -- none of the three roots + ("sag"/"sas"/"bubibi") begins with "ba", and no rule's output shape matches it either. A + plain, uncontrived zero-parse control (not itself a disjunctive/environment rejection). + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + expect_fail: true + exercises: + - "AffixProcessRule: subtraction/truncation" diff --git a/conformance/languages/agglutinative-turkic/grammar.xml b/conformance/languages/agglutinative-turkic/grammar.xml new file mode 100644 index 000000000..8ca73b697 --- /dev/null +++ b/conformance/languages/agglutinative-turkic/grammar.xml @@ -0,0 +1,570 @@ + + + + + Kirenti + + v + n + + vAlpha + + + + cons+- + voc+- + high+- + back+- + voice+- + + + place + + velar + palatal + dental + lateral + nasalN + nasalM + rhotic + sibilant + bilabial + vocalic + + + + + Main + + + a + + + + + e + + + + + i + + + + + u + + + + + + + k + + + + + t + + + + + d + + + + + l + + + + + n + + + + + m + + + + + r + + + + + g + + + + + y + + + + + s + + + + + b + + + + + + + + + Any + + Consonant + + + + BackVowel + + + + FrontE + + + + BackA + + + + FrontI + + + + BackU + + + + GlideY + + + StopK + + + + VoicedVelar + + + + + TargetT + + VowelAnyBack + + + HighVowelAnyBack + + + + + + + backnessHarmony + + + + + + + + + + + + + + + + + + + + + + vowelMerge + + + + + + + + + + + + + + + + + vowelMultiMerge + + + + + + + + + + + + + + gradation + + + + + + + + + + + + + + + + + + + + epenthesis + + + + + + + + + + + + + + + + + + alphaHighHarmony + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Main + + + + past + + + + de + + + PAST + + + + present + + + + le + + + PRES + + + + plural + + + + ler + + + PL + + + + collective + + + + + e + + ik + + + + + + + COLL + + + + intensive + + + + + + + + + u + + + + + INTS + + + + alphaSuf + + + + in + + + ALPHA + + + + + verbTemplate + + tense + + number + + + + + + semit + SEMIT + carry + + + + kutak + KUTAK + arrive + + + + unt + UNT + fall + + + + gan + GAN + roam + + + tem + TEM + rest + + + sal + SAL + wait + + + + duii + DUII + weave + + + sueeb + SUEEB + grind + + + + sat + SAT + sit + + + set + SET + stand + + + + + + diff --git a/conformance/languages/agglutinative-turkic/words.yaml b/conformance/languages/agglutinative-turkic/words.yaml new file mode 100644 index 000000000..b86993252 --- /dev/null +++ b/conformance/languages/agglutinative-turkic/words.yaml @@ -0,0 +1,326 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). Phonological rules apply, in this order, ONCE to +# the fully-concatenated underlying word (root+every suffix already joined) per the stratum's +# `phonologicalRules="prBacknessHarmony prVowelMerge prVowelMultiMerge prGradation prEpenthesis"` +# order -- NOT incrementally per suffix. That ordering is itself load-bearing: prBacknessHarmony +# must see the pre-epenthesis consonant cluster, or the transparency span would be broken by an +# epenthetic vowel the harmony rule can't see past (its span only elides consonants, not vowels). +# +# 1. semit+PAST -> underlying "semit"+"de" = "semitde". Harmony: target "e" (final); scanning +# left, the nearest vowel within <=2 intervening consonants is "i" (position 4) -- FRONT, not +# back -- so the environment does not match; rule does not fire. Epenthesis: consonant seam +# t-d (positions 5-6) gets a linking "i" -> "semitide". +# -> SEMIT+PAST|semitide, rules: [mrPast, prEpenthesis] +# 2. semit+PRES -> underlying "semit"+"le" = "semitle". Same reasoning (nearest vowel to target +# "e" is front "i"): harmony doesn't fire. Epenthesis at seam t-l -> "semitile". +# -> SEMIT+PRES|semitile, rules: [mrPres, prEpenthesis] +# 3. kutak+PAST -> underlying "kutak"+"de" = "kutakde". Harmony: target "e" (final); scanning +# left past 2 consonants (d, k) reaches "a" (position 4) -- BACK -- environment matches, rule +# fires: e->a, giving "kutakda". Gradation: the word-initial "k" (position 1) is caught by the +# anchored subrule (initialBoundaryCondition) and stays "k"; the second "k" (position 5) is NOT +# word-initial, so the catch-all subrule weakens it to "g" -> "kutagda". Epenthesis: consonant +# seam g-d (positions 5-6) gets a linking "i" -> "kutagida". +# -> KUTAK+PAST|kutagida, rules: [mrPast, prBacknessHarmony, prGradation, prEpenthesis] +# 4. kutak+PRES -> underlying "kutak"+"le" = "kutakle". Same harmony/gradation reasoning as #3 +# (target "e" backed by root-internal "a" across k+l; second "k" lenites) -> "kutagla" -> +# epenthesis at the g-l seam -> "kutagila". +# -> KUTAK+PRES|kutagila, rules: [mrPres, prBacknessHarmony, prGradation, prEpenthesis] +# 5. unt+PAST -> underlying "unt"+"de" = "untde". Harmony: target "e" (final); scanning left, the +# first 2 consonants are d, t -- that already exhausts the max=2 transparency span before any +# vowel is reached, so the back vowel "u" (position 1) is OUT OF REACH -- environment does not +# match, rule does not fire (the quantifier's upper bound is the whole point of this word). +# Epenthesis (simultaneous, so every C_C seam in the 3-consonant run n-t-d gets its own "i" in +# one pass): n-t seam and t-d seam both qualify -> "u-n-i-t-i-d-e" = "unitide". +# -> UNT+PAST|unitide, rules: [mrPast, prEpenthesis] (prBacknessHarmony correctly absent) +# +# The remaining words either reuse these same five rules on independent lexical material (the +# discontinuous-circumfix/infix pair, and the two vowel-coalescence pins) or are expect_fail words +# whose signature is simply the empty set -- see PROTOCOL.md section 3 for the `-`/empty-signature +# convention, unchanged here. +# +# Reconciliation against the oracle (self-check): the SURFACE signatures above (5/5 hand-derived +# words) matched the oracle exactly once two authoring bugs were found and fixed -- see below; every +# other word's signature (14 more) also matched on the first run after those two fixes, with no +# further disagreement. +# - Bug 1 (grammar-authoring, not a hand-derivation error): a phonological subrule that CHANGES a +# segment's identity (prBacknessHarmony's e->a, prGradation's k->g, prVowelMerge/ +# prVowelMultiMerge's literal outputs) is analysis-unreachable in self-check unless its target +# (Lhs) AND output (Rhs) are both expressed as a FeatureNaturalClass over a real, FULLY-SPECIFIED +# PhonologicalFeatureSystem (every segment given an explicit value for every feature, and every +# segment's feature bundle unique in the table -- confirmed by isolated probing: a bare +# SegmentNaturalClass/literal-Segment target or output for a CHANGING subrule always finds ZERO +# parses, regardless of anchoring or an explicit Environment). The grammar originally had no +# PhonologicalFeatureSystem at all (mirroring suffixing-quechua's phonology-free precedent, which +# doesn't need one); this grammar's 4 changing phonological rules needed one added. This is a +# genuine, previously-undocumented-outside-this-fixture engine/authoring constraint, not a hand +# derivation mistake -- the hand-derived SURFACE FORMS above were all correct from the first +# draft; only the grammar's own feature-system scaffolding needed fixing for the oracle to find +# them at all. +# - Bug 2 (a genuine hand-derivation miss, the LT-22613-style lesson this discipline exists to +# catch): prBacknessHarmony's transparency span was first authored as `min="0" max="2"` +# (0-2 intervening consonants). With min="0", the rule also fires on a back vowel IMMEDIATELY +# adjacent to a front "e" (zero consonants between) -- which spuriously matched inside the SUEEB +# lexical entry's own "u-e-e" trigger pattern (the "u" is directly followed by "e"), backing the +# first "e" to "a" BEFORE prVowelMultiMerge ever got a chance to see the intact "u-e-e" pattern, +# permanently losing the word's only valid analysis. Fixed by matching v1 rewrite/quantifier's own +# bound exactly (`min="1" max="2"`), which does not change any of the derivations above (every +# harmony-eligible word here has >=1 intervening consonant already) but removes the false-positive +# zero-consonant case. The oracle was right to reject "sueb" under the min="0" version; the +# hand-derivation's IMPLICIT assumption (that harmony only interacts with the words it was +# designed for) was naive, exactly as suffixing-quechua's own header warns. +# - Bug/finding 3 (a harness-tracing bug, found while authoring this grammar and fixed in the same +# stage): self-check's traced "rules:" verification (TraceRuleAttributor.cs) originally counted a +# phonological rule as "applied" to a word whenever its synthesis pass RAN, regardless of whether +# it matched anything -- with >=2 phonological rules sharing a stratum (this grammar's five), +# every declared phonological rule id showed up in every word's traced set unconditionally +# (confirmed by an isolated 2-rule probe grammar where a word containing NEITHER rule's target +# segment traced both as applied). Root cause: the engine's TraceManager records both +# PhonologicalRuleApplied (FailureReason.None) and PhonologicalRuleNotApplied (FailureReason set) +# as PhonologicalRuleSynthesis trace nodes, and the attributor counted both kinds. Fixed by +# filtering to FailureReason.None nodes, after which every traced set below matched this header's +# hand derivations exactly (semitide traces [mrPast, prEpenthesis]; unitide correctly lacks +# prBacknessHarmony; bare roots trace []). Per-word phonological-rule attribution is therefore +# real: a parse's `rules:` list names exactly the rules that fired with effect on that word. + +language: Kirenti +inspired_by: ["Turkish (Turkic)", "Finnish (Uralic)"] +sources: + - "WALS ch. 20/21 - Suffixing vs. Prefixing in Inflectional Morphology" + - "WALS - Vowel Harmony chapters (Turkic backness harmony typology)" + - "Karlsson, Finnish: An Essential Grammar (consonant gradation)" +requires: [phonology] +words: + - word: semitide + note: >- + SEMIT (front-voweled root, "carry") + PAST. Harmony never fires (root/suffix material is all + front), so this word isolates simultaneous epenthesis breaking up the t-d seam from the + obligatory disjunctive tense slot -- the same pairing as the F3 cross-cutting worked example. + provenance: "F3 worked example (cross-cutting category): a disjunctive/obligatory affix-template slot combined with Simultaneous-mode epenthesis, chosen per docs/conformance-framework-implementation-notes.md's recommendation (both halves already have a proven single-feature fixture to crib from: negative/obligatory-tense-slot and rewrite/simultaneous-epenthesis)" + parses: + - signature: "SEMIT+PAST|semitide" + gloss: carry-PAST + rules: [mrPast, prEpenthesis] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + - "RewriteRule Simultaneous" + + - word: semitile + note: SEMIT + PRES, the disjunctive slot-1 sibling of semitide -- same epenthesis reasoning, present tense suffix instead. + parses: + - signature: "SEMIT+PRES|semitile" + gloss: carry-PRES + rules: [mrPres, prEpenthesis] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + - "RewriteRule Simultaneous" + + - word: semitideler + note: SEMIT + PAST(slot 1, obligatory) + PL(slot 2, optional) -- proves the optional number slot composes once the obligatory tense slot is filled, same mechanism as suffixing-quechua's walaknichik. + parses: + - signature: "SEMIT+PAST+PL|semitideler" + gloss: carry-PAST-PL + rules: [mrPast, mrPlural, prEpenthesis] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: kutagida + note: >- + KUTAK ("arrive", back-voweled root containing "k" both word-initially and again word-medially) + + PAST: the flagship word exercising the obligatory/disjunctive tense slot, iterative + long-distance backness harmony (reaching past the k+d consonant seam), consonant-gradation + allomorphy (initial k resists, medial k lenites to g), and simultaneous epenthesis all at once. + provenance: "W9.2 probe, RewriteRuleTests.LongDistanceRules" + parses: + - signature: "KUTAK+PAST|kutagida" + gloss: arrive-PAST + rules: [mrPast, prBacknessHarmony, prGradation, prEpenthesis] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + - "RewriteRule Simultaneous" + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + - "NaturalClass: Segments vs FeatureNaturalClass/SegmentNaturalClass precision" + - "Boundary markers (CharacterDefinitionTable)" + + - word: kutagila + note: KUTAK + PRES, the disjunctive slot-1 sibling of kutagida -- same harmony/gradation/epenthesis reasoning, present tense suffix instead. Also the rewrite/disjunctive pin (consonant-gradation's two ordered, word-initial-anchored subrules). + provenance: "W9.2 probe, RewriteRuleTests.DisjunctiveRules" + parses: + - signature: "KUTAK+PRES|kutagila" + gloss: arrive-PRES + rules: [mrPres, prBacknessHarmony, prGradation, prEpenthesis] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + - "RewriteRule Simultaneous" + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + - "Boundary markers (CharacterDefinitionTable)" + + - word: kutak + note: KUTAK bare, no tense suffix at all -- slot 1 (tense) is obligatory and unfilled, so the AffixTemplate can never produce this output at all (mirrors suffixing-quechua's walakchik / negative/obligatory-tense-slot's "tun") -- zero parses. + expect_fail: true + provenance: "F3 worked example (negative category), hand-authored to close the zero-coverage 'Affix template slots' construct flagged in docs/conformance-framework-plan.md section 6" + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: kutakler + note: KUTAK + PL only, no tense -- slot 2 is unreachable without slot 1 being filled first, so this fails for the same structural reason as bare "kutak" (proving the failure is specifically about the missing obligatory tense slot, not merely "no suffix at all"). + expect_fail: true + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: unitide + note: >- + UNT ("fall") + PAST: the root's back vowel "u" sits 3 consonants away from the suffix's target + "e" (n, t, then the suffix's own "d") -- one past the harmony rule's max=2 transparency span, + so harmony correctly does NOT fire (the quantified-environment upper bound holding, the mirror + image of kutagida's in-bound case). Simultaneous epenthesis still fires at both C_C seams in + the resulting 3-consonant run in one pass. + provenance: "W9.2 probe, RewriteRuleTests.QuantifierRules" + parses: + - signature: "UNT+PAST|unitide" + gloss: fall-PAST + rules: [mrPast, prEpenthesis] + exercises: + - "RewriteRule Simultaneous" + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: untda + note: The hypothetical "harmony reached 3 consonants" surface form -- never producible, since prBacknessHarmony's correct (bounded) behavior is exactly what forbids it; see unitide's note for the in-bound/out-of-bound contrast. + expect_fail: true + blocked_by: [prBacknessHarmony] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: gan + note: GAN ("roam") bare root, posN -- no obligatory template applies to posN, so (unlike kutak) a bare root is a complete, valid word on its own. + parses: + - signature: "GAN|gan" + gloss: roam + rules: [] + exercises: [] + + - word: guan + note: GAN infixed with the intensive marker "-u-" after the first segment -- the discontinuous-morph positive/success control (no RequiredEnvironments at all, always succeeds), paired with etemik/esalik's negative circumfix demonstration below. + parses: + - signature: "GAN+INTS|guan" + gloss: roam-INTS + rules: [ruleInfixDemo] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: tem + note: TEM ("rest") bare root, posN -- one of the two roots used in the circumfix per-occurrence-environment demonstration below (its initial "t" matches the circumfix's required right-context class). + parses: + - signature: "TEM|tem" + gloss: rest + rules: [] + exercises: [] + + - word: sal + note: SAL ("wait") bare root, posN -- the other circumfix-demonstration root; its initial "s" does NOT match the circumfix's required right-context class. + parses: + - signature: "SAL|sal" + gloss: wait + rules: [] + exercises: [] + + - word: etemik + note: >- + TEM circumfixed ("e-tem-ik"): the PREFIX piece's own local right-context is "t" (TEM's own + initial segment), which matches the required class -- but the SUFFIX piece is word-final, so + its own local right-context can never match "followed by t". Because HC checks the SAME + required environment independently at EACH contiguous piece of a discontinuous morph (see + grammar.xml's ruleCollective comment), the word is rejected even though one piece's local check + passed -- both must pass, not just one. + expect_fail: true + blocked_by: [ruleCollective] + provenance: "W3.3, history row 51 (987be2fd) per-occurrence environment checking half" + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: esalik + note: SAL circumfixed ("e-sal-ik") -- the minimal-pair partner of etemik, but here the PREFIX piece's own local right-context ("s") already fails to match the required class, so this word fails even more directly (both pieces fail, rather than just one) -- same overall rejection, different reason, proving the per-piece check isn't merely "satisfied somewhere in the word". + expect_fail: true + blocked_by: [ruleCollective] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: duii + note: The DUII lexical entry's own underlying shape, fed back in as a literal surface string -- it can never surface as itself, since prVowelMerge obligatorily collapses "u"+"i"+"i" to "u"+"y" (see duy below) whenever the environment matches, which it always does here. + expect_fail: true + blocked_by: [prVowelMerge] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: duy + note: DUII's actual surface realization -- two adjacent "i" segments preceded by "u" coalesce to the glide "y" (vowel coalescence at an agglutination-plausible seam; history row 1 provenance). + provenance: "W9.2 probe, RewriteRuleTests.MergeRules; history row 1 (812aa48e/#403)" + parses: + - signature: "DUII|duy" + gloss: weave + rules: [prVowelMerge] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: sueeb + note: The SUEEB lexical entry's own underlying shape, fed back in as a literal surface string -- like duii, it can never surface as itself, since prVowelMultiMerge obligatorily reduces "u"+"e"+"e" to "u"+"e" (see sueb below). + expect_fail: true + blocked_by: [prVowelMultiMerge] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: sueb + note: SUEEB's actual surface realization -- a three-segment pattern (u+e+e, trigger folded directly into the Lhs, no separate Environment element, unlike duy's prVowelMerge) reduces to two segments (u+e); the structural counterpart to duy's explicit-environment merge. + provenance: "W9.2 probe, RewriteRuleTests.MultipleMergeRules; history row 1 (812aa48e/#403)" + parses: + - signature: "SUEEB|sueb" + gloss: grind + rules: [prVowelMultiMerge] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: sat + note: SAT ("sit", posVAlpha) bare root, no suffix -- control for satun below. + provenance: "G5 coverage sweep, VariableFeature/AlphaVariables (prAlphaHighHarmony)" + parses: + - signature: "SAT|sat" + gloss: sit + rules: [] + exercises: [] + + - word: set + note: SET ("stand", posVAlpha) bare root, no suffix -- control for setin below. + provenance: "G5 coverage sweep, VariableFeature/AlphaVariables (prAlphaHighHarmony)" + parses: + - signature: "SET|set" + gloss: stand + rules: [] + exercises: [] + + - word: satun + note: >- + SAT + mrAlphaSuf ("-in" underlyingly) -- the root's own last vowel "a" is back, so + prAlphaHighHarmony's alpha variable captures back+ from the LeftEnvironment and imposes it on + the suffix's high vowel, turning "i" into "u". Previously zero coverage in the whole suite: + no other grammar declares a PhonologicalRule's own VariableFeatures/AlphaVariables at all. + provenance: "G5 coverage sweep, VariableFeature/AlphaVariables (prAlphaHighHarmony)" + parses: + - signature: "SAT+ALPHA|satun" + gloss: sit-ALPHA + rules: [mrAlphaSuf, prAlphaHighHarmony] + exercises: + - "Alpha-variable phonological environments (VariableFeature/AlphaVariables)" + + - word: setin + note: >- + SET + mrAlphaSuf -- the root's own last vowel "e" is front, so the captured alpha value is + back-, leaving the suffix's high vowel "i" unchanged (an identity case, output == input, the + same house style as prGradation's own anchored subrule) -- proves the rule is a genuine + agreement, not a one-way rewrite that always backs the vowel. + provenance: "G5 coverage sweep, VariableFeature/AlphaVariables (prAlphaHighHarmony)" + parses: + - signature: "SET+ALPHA|setin" + gloss: stand-ALPHA + rules: [mrAlphaSuf, prAlphaHighHarmony] + exercises: + - "Alpha-variable phonological environments (VariableFeature/AlphaVariables)" diff --git a/conformance/languages/austronesian-phase/grammar.xml b/conformance/languages/austronesian-phase/grammar.xml new file mode 100644 index 000000000..69a5162cc --- /dev/null +++ b/conformance/languages/austronesian-phase/grammar.xml @@ -0,0 +1,467 @@ + + + + + Tavolan + + + nComplexMeta + nSimpleMeta + nNotUnapplied + vInfix + vCircum + vRedup + vTrunc + nLnk + vNonContig + + + + cons+- + voc+- + high+- + low+- + back+- + round+- + voice+- + + place + + bilabial + alveolar + velar + glottal + sibilant + lateral + rhotic + nasalLab + nasalAlv + vocalic + + + + + + Main + + + a + + + + + + e + + + + + + i + + + + + + u + + + + + + ɯ + + + + + + + p + + + + + + b + + + + + + m + + + + + + t + + + + + + d + + + + + + n + + + + + + s + + + + + + l + + + + + + r + + + + + + k + + + + + + h + + + + + + + + + + + + + + Any + Consonant + + + Vowel + + + + LowVowel + + + VowelI + + + + + VoicedCons + + + + I + U + + + + + nonContigRaising + + + + + + + + + + + + + metathesisSimple + + + + + + + + + + + + metathesisComplex + + + + + + + + + + + + + + + ComplexMeta + + + uSuffix + + + + +u + + + 3SGU + + + + + mi + MI + come + + + + + + SimpleMeta + + + iSuffix + + + + i + + + 3SGI + + + + + + niu + NIU + coconut + + + pu + PU + fire + + + + + + Morphology + + + + infixUm + + + + + + + + + um + + + + + AV + + + + circumKeAn + + + + + ke + + an + + + + NMLZ + + + + redupCV + + + + + + + + + + + + + + RDP + + + + redupFull + + + + + + + + + + RDPL + + + + truncateFinalE + + + + + + + + + + INCP + + + + linker + + + + in + + + LNK + + + + perfNonContig + + + + + + + + + + a + + a + + ɯd + + + + PERF + + + + + sulat + SULAT + write + + + adil + ADIL + just + + + tula + TULA + help + + + pure + PURE + finish + + + baha + BAHA + flood + + + ktb + KTB + arrange + + + + + + diff --git a/conformance/languages/austronesian-phase/words.yaml b/conformance/languages/austronesian-phase/words.yaml new file mode 100644 index 000000000..feac07671 --- /dev/null +++ b/conformance/languages/austronesian-phase/words.yaml @@ -0,0 +1,235 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). +# +# 1. nui -> NIU ("coconut", posSimpleMeta) underlying "niu": mrSimpleMeta (adjacent i-then-u -> +# u-then-i swap) fires obligatorily -> "nui". The underlying "niu" can never itself surface. +# -> NIU|nui, rules: [mrSimpleMeta] +# 2. niu -> the raw, un-metathesized underlying shape -- mrSimpleMeta is obligatory wherever an +# i-then-u adjacency holds, so "niu" can never be a valid surface (mirrors v1 simple_rule row 1). +# -> expect_fail, blocked_by: [mrSimpleMeta] +# 3. mu+i -> MI ("come", posComplexMeta) + 3SGU ("+u"): underlying "mi"+"+u" = "mi+u", then +# mrComplexMeta (i +BND u -> u +BND i, final-anchored) swaps across the boundary -> "mu+i", +# rendered with the visible boundary as "mu+?i". +# -> MI+3SGU|mu+?i, rules: [mrUSuffix, mrComplexMeta] +# 4. pui -> PU ("fire", posNotUnapplied) + 3SGI ("i"): underlying "pu"+"i" = "pui". The surface is +# u-then-i, NOT the i-then-u adjacency mrSimpleMeta requires, so metathesis correctly never fires +# (the non-unapplication pin) -- rules lists only the suffix. +# -> PU+3SGI|pui, rules: [mrISuffix] +# 5. sumulat -> SULAT ("write", posInfix) + AV (-um- infixed after the first consonant): "s"+"um"+ +# "ulat" = "sumulat". +# -> SULAT+AV|sumulat, rules: [mrInfixUm] +# +# The remaining words reuse these mechanisms on independent lexical material -- circumfix ke-...-an, +# CV- and full reduplication (ReduplicationHint), literal-final truncation, the isolated +# CopyFromInput/InsertSegments linker, and the noncontiguous perf + allophonic a->i raising -- or are +# expect_fail words whose signature is the empty set. See PROTOCOL.md section 3 for the `-`/empty- +# signature convention and the multi-character-segment rendering note. +# +# Reconciliation against the oracle (self-check): documented per word where the oracle disagreed with +# the hand derivation; the trace is authoritative per the task discipline. + +language: Tavolan +inspired_by: ["Tagalog (Austronesian)", "Indonesian (Austronesian)", "Rotuman (Oceanic)", "Leti (Austronesian)"] +sources: + - "WALS - infixation, reduplication, and metathesis typology chapters" + - "Blust, The Austronesian Languages (2013) - infix -um-, ke-...-an circumfix, CV- reduplication" + - "McCarthy, Rotuman consonant metathesis and the phase distinction; O'odham subtractive perfective" +requires: [phonology] +words: + - word: nui + note: >- + NIU ("coconut", posSimpleMeta) underlying "niu" -- mrSimpleMeta swaps the adjacent i-then-u to + u-then-i (leftSwitch=u, rightSwitch=i), the metathesis/simple_rule mechanism. Obligatory, so + the underlying "niu" never itself surfaces (see next word). + provenance: "MetathesisRuleTests.SimpleRule" + parses: + - signature: "NIU|nui" + gloss: coconut + rules: [mrSimpleMeta] + exercises: + - "MetathesisRule" + + - word: niu + note: The raw, un-metathesized underlying shape -- mrSimpleMeta is obligatory wherever an i-then-u adjacency holds, so this surface form can never be a valid analysis (mirrors v1 simple_rule's own negative row). + expect_fail: true + blocked_by: [mrSimpleMeta] + exercises: + - "MetathesisRule" + + - word: mu+i + note: >- + MI ("come", posComplexMeta) + 3SGU ("+u") -- underlying "mi"+"+u" = "mi+u", then mrComplexMeta + (i +BND u -> u +BND i, final-anchored, with a non-switch BoundaryMarker between the switch + roles) swaps across the boundary. The visible "+" boundary renders as "+?" in the signature + shape (metathesis/complex_rule mechanism). + provenance: "MetathesisRuleTests.ComplexRule" + parses: + - signature: "MI+3SGU|mu+?i" + gloss: come-3SG + rules: [mrUSuffix, mrComplexMeta] + exercises: + - "MetathesisRule" + - "Boundary markers (CharacterDefinitionTable)" + + - word: mi + note: MI ("come", posComplexMeta) bare root, no suffix -- mrComplexMeta has no i +BND u site, so nothing fires. + parses: + - signature: "MI|mi" + gloss: come + rules: [] + exercises: [] + + - word: pui + note: >- + PU ("fire", posNotUnapplied) + 3SGI ("i") -- underlying "pu"+"i" = "pui". The surface is + u-then-i, NOT the i-then-u adjacency mrSimpleMeta requires, so the shared metathesis rule + correctly never fires (metathesis/not_unapplied's non-unapplication pin); rules names only the + suffix. + provenance: "MetathesisRuleTests.SimpleRuleNotUnapplied" + parses: + - signature: "PU+3SGI|pui" + gloss: fire-3SG + rules: [mrISuffix] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: sumulat + note: SULAT ("write", posInfix) + AV -- Tagalog actor-voice -um- infixed after the first consonant, "s"+"um"+"ulat" = "sumulat". + provenance: "W9.1 probe, AffixProcessRuleTests.InfixRules" + parses: + - signature: "SULAT+AV|sumulat" + gloss: write-AV + rules: [mrInfixUm] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + + - word: sulat + note: SULAT bare root, no infix -- a plain control. + parses: + - signature: "SULAT|sulat" + gloss: write + rules: [] + exercises: [] + + - word: keadilan + note: ADIL ("just", posCircum) + NMLZ -- Indonesian ke-...-an nominalizer wraps the stem, "ke"+"adil"+"an" = "keadilan". + provenance: "W9.1 probe, AffixProcessRuleTests.CircumfixRules" + parses: + - signature: "NMLZ+ADIL|keadilan" + gloss: NMLZ-just + rules: [mrCircum] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + + - word: adil + note: ADIL bare root, no circumfix -- a plain control. + parses: + - signature: "ADIL|adil" + gloss: just + rules: [] + exercises: [] + + - word: tutula + note: >- + TULA ("help", posRedup) with CV- reduplication (ReduplicationHint prefix): the first C-V ("tu") + is copied as the reduplicant prefix, then the whole stem -- "tu"+"tula" = "tutula". + parses: + - signature: "RDP+TULA|tutula" + gloss: RED-help + rules: [mrRedupCV] + exercises: + - "AffixProcessRule: reduplication (ReduplicationHint)" + + - word: tulatula + note: >- + TULA with full reduplication (ReduplicationHint prefix): the whole stem is copied twice, + "tula"+"tula" = "tulatula". + parses: + - signature: "RDPL+TULA|tulatula" + gloss: RED.FULL-help + rules: [mrRedupFull] + exercises: + - "AffixProcessRule: reduplication (ReduplicationHint)" + + - word: tula + note: TULA bare root, no reduplication -- a plain control. + parses: + - signature: "TULA|tula" + gloss: help + rules: [] + exercises: [] + + - word: pur + note: >- + PURE ("finish", posTrunc) + INCP -- subtractive truncation deletes the literal final "e" + (Rotuman deletion/incomplete phase; O'odham subtractive perfective cited in provenance), + "pure" -> "pur". + provenance: "W9.1 probe, AffixProcessRuleTests.TruncateRules" + parses: + - signature: "PURE+INCP|pur" + gloss: finish-INCP + rules: [mrTrunc] + exercises: + - "AffixProcessRule: subtraction/truncation" + + - word: pure + note: PURE bare root, no truncation -- a plain control. + parses: + - signature: "PURE|pure" + gloss: finish + rules: [] + exercises: [] + + - word: bahain + note: >- + BAHA ("flood", posLnk) + LNK -- the isolated CopyFromInput/InsertSegments demo: a plain linker + suffix "-in" with no phonology and no infixation, so the two output actions are exercised on + their own, "baha"+"in" = "bahain". + parses: + - signature: "BAHA+LNK|bahain" + gloss: flood-LNK + rules: [mrLnk] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + + - word: baha + note: BAHA bare root, no linker -- a plain control. + parses: + - signature: "BAHA|baha" + gloss: flood + rules: [] + exercises: [] + + - word: katibɯd + note: >- + KTB ("arrange", posNonContig) + PERF -- the triconsonantal root gets an "a" inserted after C1 + and C2 plus a trailing "ɯd" (k+a+t+a+b+ɯd = katabɯd), then prNonContig raises the "a" before the + voiced "b" to "i" -> "katibɯd" (the first "a", before voiceless "t", is left unchanged). The + noncontiguous affix + allophonic-rewrite interplay. + provenance: "W9.1 probe, AffixProcessRuleTests.NonContiguousRules" + parses: + - signature: "KTB+PERF|katibɯd" + gloss: arrange-PERF + rules: [mrPerf, prNonContig] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: katabɯd + note: The raw, un-raised underlying shape of the perf form -- prNonContig obligatorily raises the "a" before a voiced consonant, so this surface form can never itself be a valid analysis (see katibɯd). + expect_fail: true + blocked_by: [prNonContig] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: ktb + note: KTB bare triconsonantal root, no perf affix -- a plain control. + parses: + - signature: "KTB|ktb" + gloss: arrange + rules: [] + exercises: [] diff --git a/conformance/languages/bantu-verbal/grammar.xml b/conformance/languages/bantu-verbal/grammar.xml new file mode 100644 index 000000000..06d9d0532 --- /dev/null +++ b/conformance/languages/bantu-verbal/grammar.xml @@ -0,0 +1,617 @@ + + + + + Velamba + + + vNasalMutation + vRealizational + vMprGroups + vExtension + vReduplication + vAgreement + + vSecondSweep + + + + cons+- + voc+- + high+- + back+- + voice+- + nasal+- + + place + + bilabial + dental + sibilant + nasalM + nasalN + lateral + glottal + velar + labiovelar + palatal + uvular + vocalic + + + + + + + tense + + pres + past + + + + + num + + sg + pl + + + + + g5 + + g5a + g5b + + + + + + + stress + + stressed + unstressed + + + + + + ExtA + ExtB + ConcA + ConcB + ConcC + ConcD + + RRealTest + ExtGroup + AllConc + AnyConc + + + + + G5Stem + + + + + + + Main + + + a + + + + + + e + + + + + + i + + + + + + + b + + + + + + p + + + + + + d + + + + + + s + + + + + + z + + + + + + m + + + + + + n + + + + + + l + + + + + + h + + + + + + k + + + + + + w + + + + + + y + + + + + + x + + + + + + + + + Any + + Nasal + + + + VoicelessBilabial + + + + VoicedBilabial + + + + + + + FamRRealBlock + + + + + nasalMutation + + + + + + + + + + + + + + Main + + + + tense + + + + pe + + + + TENSE + + + realizPast + + + + ile + + + + RPAST + + + + extX + + + + x + + + EXTX + + + extY + + + + y + + + EXTY + + + endZ + + + + z + + + ENDZ + + + + endW + + + + w + + + ENDW + + + endH + + + + h + + + ENDH + + + + redup + + + + + + + RED + + + + causative + + + + ish + + + CAUS + + + applicative + + + + il + + + APPL + + + finalVowel + + + + a + + + FV + + + + numPl + + + + ep + + + + + NUMPL + + + agr + + + + ez + + + + + AGR + + + + recurSuf + + + + iz + + + RECURSUF + + + + stemReq + + + + eh + + + STEMREQ + + + + rRealTest + + + + ed + + + + RREALTEST + + + + + extensionTemplate + causative + applicative + finalVowel + + + + + + mpa + PAV + hit + + + + lima + LIM + cultivate + + + + kib + KIB + root.kib + + + bel + BEL + root.bel + + + nal + NAL + root.nal + + + + andik + AND + write + + + + kimbia + KIMB + run + + + + dab + DAB + cook + + + + kes + + + KES + sing + + + + sil + SIL + plant + + + + dal + DAL + dig + + + dah + DAH + climb + + + + zim + + ZIM + fly + + + zam + + ZAM + swim + + + wep + + WEP + dive + + + + + + diff --git a/conformance/languages/bantu-verbal/words.yaml b/conformance/languages/bantu-verbal/words.yaml new file mode 100644 index 000000000..89e49f450 --- /dev/null +++ b/conformance/languages/bantu-verbal/words.yaml @@ -0,0 +1,458 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). +# +# 1. mba -> PAV ("hit", posV) bare root, underlying "mpa": prNasalMutation (posV-scoped subrule) +# obligatorily voices the "p" to "b" following the nasal "m" -> "mba". The raw "mpa" itself can +# never surface for this candidate (see mpa below). +# -> PAV|mba, rules: [prNasalMutation] +# 2. mpa -> the SAME underlying shape "mpa", but analyzed as the (nonexistent) posN-equivalent -- +# no such rule ever gates a posV-only subrule off, so instead this is eMpaV's own OWN candidate, +# analyzed as unmutated: impossible, since prNasalMutation is obligatory for posV. Zero parses +# would follow IF eMpaV were the only lexical entry sharing shape "mpa" -- but there is no +# second (posV-immune) lexical entry sharing "mpa" in this word list, so "mpa" is simply +# expect_fail on its own terms (prNasalMutation's obligatory synthesis-confirm never lets the +# raw "mpa" survive as PAV's own surface). +# -> expect_fail, blocked_by: [prNasalMutation] +# 3. lima -> LIM ("cultivate", posV2) bare root, no rule applied. +# -> LIM|lima, rules: [] +# 4. limaile -> LIM + RPAST (a RealizationalRule, no AffixTemplate needed): the root's own FS has +# no tense value at all, so IsBlocked's presence check finds nothing to collide with. +# -> LIM+RPAST|limaile, rules: [rrPast] +# 5. limapeile -> LIM + TENSE ("-pe", sets tense=pres) attempted together with RPAST: IsBlocked +# sees tense is ALREADY present (pres, not past) and blocks anyway (presence-only, not +# value-equality) -> zero parses. +# -> expect_fail, blocked_by: [rrPast] +# +# The remaining words reuse these same mechanisms on independent lexical/rule material -- MPR +# features/groups (Overwrite + All/Any gating, mirroring fusional-latin's own proof exactly), +# verbal reduplication (ReduplicationHint, zero coverage today -- this grammar's home), and +# extension-slot ordering via AffixTemplate (Causative before Applicative, both before the +# obligatory Final Vowel) -- or are expect_fail words whose signature is the empty set. +# +# Reconciliation against the oracle (self-check): all 5 words above matched the hand derivation on +# the first self-check run. Two genuine reconciliations were needed elsewhere in the file: +# 1. Reduplication (mrRedup's redupMorphType="suffix"): the hand derivation assumed morph order +# ROOT+RED (the "existing"/first copy keeping the root's own morpheme identity, the "new"/ +# second copy carrying RED), verified correct by an isolated probe before this word was added +# to the file (see grammar.xml's mrRedup comment) -- no --propose correction was needed once +# that probe's finding was applied. +# 2. Extension-slot ordering ("andikilisha", the intended wrong-order negative control): hand- +# derived as expect_fail on the assumption that the AffixTemplate's declared slot sequence +# (causative before applicative) forbids the reverse surface order -- the oracle instead +# ACCEPTS it (AND+APPL+CAUS+FV). Confirmed by an isolated 2-slot probe (see that word's own +# note for the full mechanism): the template's slot order is a hard SYNTHESIS constraint and +# constrains a single analysis-side unwind the same way, but with two OPTIONAL, generically- +# shaped slots, the overall stratum analysis retries the rule set (including the template) to a +# fixpoint, so the two affixes can be peeled off across separate template applications in +# either order. Corrected in place rather than discarded, since it is a genuine and useful +# engine finding for any future template-heavy grammar (prefixal-athabaskan especially). +# Every other word in the file matched on the first run. + +language: Velamba +inspired_by: ["Swahili (Bantu)", "Ndebele (Bantu)", "Sena (Bantu)"] +sources: + - "WALS - Reduplication chapters; Bantu noun-class and verbal-template surveys" + - "Hyman, Suffix Ordering in Bantu: A Morphocentric Analysis (CARP hierarchy)" + - "Katamba, Bantu nominal morphology (in Nurse & Philippson, The Bantu Languages) - post-nasal mutation" +requires: [phonology] +words: + - word: mba + note: >- + PAV ("hit", posV) bare root, underlying "mpa" -- prNasalMutation obligatorily voices the "p" + to "b" following the nasal "m", a genuine Bantu post-nasal-voicing effect (modeled here as + already fused into the root's own stored shape, mirroring v1 rewrite/required-pos-subrule's + own bare-shape minimalism). + parses: + - signature: "PAV|mba" + gloss: hit + rules: [prNasalMutation] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: mpa + note: The raw, un-mutated underlying shape -- prNasalMutation is obligatory for posV wherever its nasal-left-environment holds, so this surface form can never itself be a valid analysis of PAV. + expect_fail: true + blocked_by: [prNasalMutation] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: lima + note: LIM ("cultivate", posV2), bare root, no lexical head features at all -- trivially valid, no rule touches it. + parses: + - signature: "LIM|lima" + gloss: cultivate + rules: [] + exercises: [] + + - word: limaile + note: LIM + RPAST (a RealizationalRule, no AffixTemplate/Slot wrapper needed) -- the root's own FS has no tense value at all, so RPAST's IsBlocked presence check finds nothing to collide with. + provenance: "W5, RealizationalAffixProcessRule core mechanics + IsBlocked" + parses: + - signature: "LIM+RPAST|limaile" + gloss: cultivate-PST.REALIZ + rules: [rrPast] + exercises: + - "RealizationalAffixProcessRule" + + - word: limape + note: LIM + TENSE ("-pe", ordinary affix, sets tense=pres) -- pre-loads a tense value so the realizational rule's blocking check below has something to trip on. + parses: + - signature: "LIM+TENSE|limape" + gloss: cultivate-PRES + rules: [mrTense] + exercises: + - "RealizationalAffixProcessRule" + + - word: limapeile + note: >- + LIM + TENSE + RPAST attempted together -- mrTense sets tense=pres first, then RPAST's + IsBlocked check sees tense is ALREADY present (even though it's pres, not past) and blocks -- + a value-equality mis-implementation would wrongly accept this; presence alone is what matters. + expect_fail: true + blocked_by: [rrPast] + exercises: + - "RealizationalAffixProcessRule" + + - word: kib + note: KIB ("root.kib", posV3), bare -- ruleFeatures preset both AllConc members (mprConcA, mprConcB) directly on the lexical entry. + parses: + - signature: "KIB|kib" + gloss: root.kib + rules: [] + exercises: [] + + - word: bel + note: BEL ("root.bel", posV3), bare -- ruleFeatures preset mprConcA (half of AllConc) and mprConcC (half of AnyConc). + parses: + - signature: "BEL|bel" + gloss: root.bel + rules: [] + exercises: [] + + - word: nal + note: NAL ("root.nal", posV3), bare -- no ruleFeatures preset at all. + parses: + - signature: "NAL|nal" + gloss: root.nal + rules: [] + exercises: [] + + - word: xkib + note: KIB prefixed by mrExtX ("x-", sets MPR feature mprExtA). + provenance: "W3.1, MprFeatureGroupOutput.Overwrite in MPR output accumulation" + parses: + - signature: "EXTX+KIB|xkib" + gloss: EXT.A-root.kib + rules: [mrExtX] + exercises: + - "MPR features/groups" + + - word: yxkib + note: KIB prefixed by mrExtX then mrExtY ("y-", sets mprExtB) -- ExtGroup's outputType="overwrite" means mrExtY's own output DROPS mprExtA rather than accumulating it alongside mprExtB. + parses: + - signature: "EXTY+EXTX+KIB|yxkib" + gloss: EXT.B-EXT.A-root.kib + rules: [mrExtX, mrExtY] + exercises: + - "MPR features/groups" + + - word: xkibz + note: KIB prefixed by mrExtX then suffixed by mrEndZ (requiredMPRFeatures="mprExtA") -- mprExtA is still present (only mrExtX applied), so mrEndZ's requirement is satisfied. + parses: + - signature: "EXTX+KIB+ENDZ|xkibz" + gloss: EXT.A-root.kib-END + rules: [mrExtX, mrEndZ] + exercises: + - "MPR features/groups" + + - word: yxkibz + note: >- + KIB prefixed by mrExtX then mrExtY then suffixed by mrEndZ -- the DISTINGUISHING row: after + mrExtY, ExtGroup's Overwrite semantics have already dropped mprExtA, so mrEndZ's + requiredMPRFeatures="mprExtA" fails. Zero parses. A pre-fix "flat union" mis-implementation + would wrongly accept this. + expect_fail: true + blocked_by: [mrEndZ] + exercises: + - "MPR features/groups" + + - word: kibw + note: KIB + mrEndW (requiredMPRFeatures="mprConcA mprConcB", matchType="all") -- KIB has both AllConc members, so the ALL requirement is satisfied. + provenance: "W3.1, MprFeatureGroupMatchType.All/Any in required-MPR gating" + parses: + - signature: "KIB+ENDW|kibw" + gloss: root.kib-ALL.END + rules: [mrEndW] + exercises: + - "MPR features/groups" + + - word: belw + note: BEL + mrEndW -- BEL has only mprConcA, not mprConcB, so the ALL requirement is NOT satisfied. Zero parses -- the pre-fix flat-overlap bug would have wrongly accepted this. + expect_fail: true + blocked_by: [mrEndW] + exercises: + - "MPR features/groups" + + - word: nalw + note: NAL + mrEndW -- NAL has neither AllConc member, so the ALL requirement fails outright. + expect_fail: true + exercises: + - "MPR features/groups" + + - word: kibh + note: KIB + mrEndH (requiredMPRFeatures="mprConcC mprConcD", matchType="any") -- KIB has NEITHER AnyConc member, so the ANY requirement fails. + expect_fail: true + exercises: + - "MPR features/groups" + + - word: belh + note: BEL + mrEndH -- BEL has mprConcC (one AnyConc member), so the ANY requirement (at least one) is satisfied. + parses: + - signature: "BEL+ENDH|belh" + gloss: root.bel-ANY.END + rules: [mrEndH] + exercises: + - "MPR features/groups" + + - word: nalh + note: NAL + mrEndH -- NAL has neither AnyConc member, so the ANY requirement fails. + expect_fail: true + exercises: + - "MPR features/groups" + + - word: kimbia + note: KIMB ("run/flee", posV5), bare root, no reduplication applied -- a plain, unrestricted control. + parses: + - signature: "KIMB|kimbia" + gloss: run + rules: [] + exercises: [] + + - word: kimbiakimbia + note: >- + KIMB fully reduplicated (intensive/repetitive aspect, Swahili kimbia/kimbiakimbia + "run"/"run about repeatedly"-plausible): mrRedup copies the whole stem twice. + redupMorphType="suffix" treats the FIRST copy as the root's own continuing morph and the + SECOND as the rule's own new morph, so the morph chain reads root-then-RED (confirmed by an + isolated probe before this word was authored -- see grammar.xml's mrRedup comment). This is + the zero-coverage-today construct's home: no other v2 grammar carries ReduplicationHint. + parses: + - signature: "KIMB+RED|kimbiakimbia" + gloss: run-RED + rules: [mrRedup] + exercises: + - "AffixProcessRule: reduplication (ReduplicationHint)" + + - word: andika + note: AND ("write", posV4) + FV only ("-a") -- the obligatory Final Vowel slot filled, both optional extension slots (causative, applicative) skipped entirely. + parses: + - signature: "AND+FV|andika" + gloss: write-FV + rules: [mrFV] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: andik + note: AND bare, no Final Vowel at all -- slot 3 (finalVowel) is obligatory and unfilled, so the AffixTemplate can never produce this output (mirrors agglutinative-turkic's kutak / suffixing-quechua's walakchik). + expect_fail: true + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: andikisha + note: AND + CAUS (slot 1, "-ish") + FV (slot 3, "-a") -- the optional applicative slot (slot 2) skipped, proving slot 1 and slot 3 compose independently of slot 2. + provenance: "plan item 2 (wave-3), requiredPartsOfSpeech on a phonological subrule" + parses: + - signature: "AND+CAUS+FV|andikisha" + gloss: write-CAUS-FV + rules: [mrCaus, mrFV] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: andikila + note: AND + APPL (slot 2, "-il") + FV (slot 3, "-a") -- causative (slot 1) skipped, the mirror-image partial fill of andikisha. + parses: + - signature: "AND+APPL+FV|andikila" + gloss: write-APPL-FV + rules: [mrAppl, mrFV] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: andikishila + note: >- + AND + CAUS (slot 1) + APPL (slot 2) + FV (slot 3), ALL THREE slots filled in the template's + own fixed order -- the extension-slot-ordering construct's flagship word, Bantu CARP-style + (Causative before Applicative, both before the obligatory Final Vowel). + parses: + - signature: "AND+CAUS+APPL+FV|andikishila" + gloss: write-CAUS-APPL-FV + rules: [mrCaus, mrAppl, mrFV] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: andikilisha + note: >- + The textual mirror-image of andikishila (applicative-then-causative instead of causative- + then-applicative) -- hand-derived as expect_fail (assumed the template's declared slot + sequence would forbid this order structurally), but the ORACLE ACCEPTS it. Reconciliation + finding, confirmed by an isolated 2-slot probe before correcting this word: AffixTemplate's + own slot sequence governs SYNTHESIS strictly (generation always produces causative-then- + applicative) and constrains a SINGLE analysis-side unwind of the template the same way, but + Slot 1/2 are both optional and their MorphologicalInput stems are both unconstrained + ("any segment, 1+"), so the overall stratum analysis (which retries its rule set, including + this template, to a fixpoint) can peel the affixes off across TWO separate template + applications: first CAUS strips from the outside ("andikilish" -> "andikil", since + applicative's own "-il" pattern doesn't match at that point), then the template is retried on + the result and APPL strips from ITS outside ("andikil" -> "andik"). Neither slot's rule ever + violated its own pattern; the "wrong order" is simply a different, also-valid decomposition of + the same string once repeated application is accounted for. A grammar wanting true analysis- + side order enforcement would need an MPR-feature gate between the two slots (this grammar's + own mpr-groups section demonstrates that mechanism); this template alone does not provide it. + parses: + - signature: "AND+APPL+CAUS+FV|andikilisha" + gloss: write-APPL-CAUS-FV + rules: [mrAppl, mrCaus, mrFV] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: dabep + note: >- + DAB ("cook", posV6) + mrNumPl ("-op") -- assigns featNum=pl (OutputHeadFeatures) and + featStress=stressed (OutputFootFeatures) to the resulting word. G5 (code-coverage-driven): + no other grammar in the suite exercises RequiredHeadFeatures/OutputHeadFeatures/ + RequiredFootFeatures/OutputFootFeatures at all before this pair of words. + provenance: "G5 coverage sweep, SyntacticFeatureSystem/XmlLanguageLoader head+foot feature paths" + parses: + - signature: "DAB+NUMPL|dabep" + gloss: cook-PL + rules: [mrNumPl] + exercises: + - "Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures)" + + - word: dabepez + note: >- + DAB + mrNumPl + mrAgr ("-ez") -- mrAgr's RequiredHeadFeatures (featNum=pl) and + RequiredFootFeatures (featStress=stressed) are both satisfied because mrNumPl already + assigned them, so the agreement suffix can attach on top. + provenance: "G5 coverage sweep, SyntacticFeatureSystem/XmlLanguageLoader head+foot feature paths" + parses: + - signature: "DAB+NUMPL+AGR|dabepez" + gloss: cook-PL-AGR + rules: [mrNumPl, mrAgr] + exercises: + - "Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures)" + + - word: dabez + note: >- + DAB + mrAgr directly, WITHOUT the intervening mrNumPl -- reconciliation finding: a bare root + carries no featNum/featStress value at all, and an UNSET feature unifies freely against + mrAgr's RequiredHeadFeatures/RequiredFootFeatures (unification, not presence-checking), so + this analysis succeeds vacuously rather than failing. Contrast with kesez below, whose root + carries an explicit CONFLICTING value via AssignedHeadFeatures/AssignedFootFeatures -- that + is what actually trips the requirement. + provenance: "G5 coverage sweep, SyntacticFeatureSystem/XmlLanguageLoader head+foot feature paths" + parses: + - signature: "DAB+AGR|dabez" + gloss: cook-AGR + rules: [mrAgr] + exercises: + - "Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures)" + + - word: kes + note: >- + KES ("sing", posV6) bare root -- carries AssignedHeadFeatures featNum=sg and + AssignedFootFeatures featStress=unstressed directly on the lexical entry (both previously at + zero coverage: AssignedFootFeatures had no user anywhere in the suite). A bare-root parse is + unaffected by either assignment. + provenance: "G5 coverage sweep, LexicalEntry.AssignedHeadFeatures/AssignedFootFeatures" + parses: + - signature: "KES|kes" + gloss: sing + rules: [] + exercises: + - "Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures)" + + - word: kesez + note: >- + KES + mrAgr ("-ez") -- KES's own AssignedHeadFeatures (featNum=sg) genuinely CONFLICTS with + mrAgr's RequiredHeadFeatures (featNum=pl), and likewise AssignedFootFeatures (unstressed) + conflicts with RequiredFootFeatures (stressed): unification fails on both, so this analysis + has zero parses -- the real rejection dabez (above) does NOT demonstrate. + provenance: "G5 coverage sweep, LexicalEntry.AssignedHeadFeatures/AssignedFootFeatures" + expect_fail: true + blocked_by: [mrAgr] + exercises: + - "Syntactic feature agreement (RequiredHeadFeatures/OutputHeadFeatures/RequiredFootFeatures/OutputFootFeatures)" + + - word: siliz + note: >- + SIL ("plant", posV7) + mrRecurSuf ("-iz"), one application -- a plain control establishing + the suffix works at all before silizizi (below) attempts a second application. + provenance: "G5 coverage sweep (second sweep), SynthesisAffixProcessRule.MaxApplicationCount" + parses: + - signature: "SIL+RECURSUF|siliz" + gloss: plant-RECURSUF + rules: [mrRecurSuf] + exercises: + - "Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable)" + + - word: siliziz + note: >- + SIL+RECURSUF (siliz, above) with "-iz" attached a SECOND time -- the DTD's own default + multipleApplication="1" caps mrRecurSuf to exactly one application per derivation, so this + has zero parses. + provenance: "G5 coverage sweep (second sweep), SynthesisAffixProcessRule.MaxApplicationCount" + expect_fail: true + blocked_by: [mrRecurSuf] + exercises: + - "Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable)" + + - word: daleh + note: >- + DAL ("dig", posV7) + mrStemReq ("-eh") -- DAL's own allomorph carries stemName="snG5", which + mrStemReq's requiredStemName="snG5" requires, so it applies. + provenance: "G5 coverage sweep (second sweep), SynthesisAffixProcessRule.RequiredStemName" + parses: + - signature: "DAL+STEMREQ|daleh" + gloss: dig-STEMREQ + rules: [mrStemReq] + exercises: + - "Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable)" + + - word: daheh + note: >- + DAH ("climb", posV7) + mrStemReq -- DAH's own allomorph carries no stem name at all, so + mrStemReq's requiredStemName="snG5" is never satisfied and this has zero parses. + provenance: "G5 coverage sweep (second sweep), SynthesisAffixProcessRule.RequiredStemName" + expect_fail: true + blocked_by: [mrStemReq] + exercises: + - "Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable)" + + - word: zimed + note: >- + ZIM ("fly", posV7, mprRRealTest) + rrRRealTest ("-ed") -- ZIM's own AssignedHeadFeatures + (featG5=symG5A) genuinely conflicts with the rule's own RequiredHeadFeatures (featG5=symG5B), + so RequiredSyntacticFeatureStruct.Unify fails and this has zero parses. mprRRealTest is a + hard set-membership gate (see grammar.xml), so this rule can never spuriously apply to any + other word in the grammar regardless of this outcome. + provenance: "G5 coverage sweep (second sweep), SynthesisRealizationalAffixProcessRule.RequiredSyntacticFeatureStruct" + expect_fail: true + blocked_by: [rrRRealTest] + exercises: + - "Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable)" + + - word: zamed + note: >- + ZAM ("swim", posV7, mprRRealTest, family famRRealBlock) + rrRRealTest -- ZAM's featG5=symG5B + matches the rule's RequiredHeadFeatures, so the rule applies; but ZAM's family also contains + the suppletive WEP (also featG5=symG5B), and CheckBlocking substitutes WEP's own entry in + wherever ZAM's own output would surface, so the resynthesis ("wep") no longer matches this + word's own input shape ("zamed"), and this has zero parses. First time this suite exercises + Blockable/CheckBlocking through a RealizationalRule rather than an ordinary MorphologicalRule + or a CompoundingRule. + provenance: "G5 coverage sweep (second sweep), SynthesisRealizationalAffixProcessRule.Blockable" + expect_fail: true + blocked_by: [rrRRealTest] + exercises: + - "Ordinary/realizational rule constraints (MaxApplicationCount/RequiredStemName/Blockable)" diff --git a/conformance/languages/fusional-latin/grammar.xml b/conformance/languages/fusional-latin/grammar.xml new file mode 100644 index 000000000..48cda809c --- /dev/null +++ b/conformance/languages/fusional-latin/grammar.xml @@ -0,0 +1,648 @@ + + + + + Vetruna + + v + n + + vAblaut + + n1 + v1 + n2 + v2 + + compH + compNH + + + + + id + + abcd + efgh + iklm + nopr + stuv + wxyz + + + + + tenseprespast + pers123 + numsgpl + + + + PedA + PedB + ConjA + ConjB + ConjC + ConjD + + CompReq + CompSubA + ThemeGroup + AllGroup + AnyGroup + + + + + PrincipalPartA + + + + + + + PrincipalPartB + + + + + + + + Main + + a + b + c + d + e + f + g + h + i + k + l + m + n + o + p + r + s + t + u + v + w + x + y + z + + + + + + + + + + Any + + AblautI + AblautA + + + + FamCarry + + FamCompBlock + + + + + Main + + + + pers1 + + + + o + + + + P1 + + + pers2 + + + + s + + + + P2 + + + pers3 + + + + t + + + + P3 + + + + tense + + + + es + + + + TENSE + + + + realizPast + + + + id + + + + RPAST + + + + past2 + + + + it + + + + PAST2 + + + plural2 + + + + unt + + + + PL2 + + + + gePartT + + + + + ge + + t + + + + GET + + + + suffixS3 + + + + h + + + S3 + + + + nasalInfix + + + + + + + + + n + + + + + PRESSTEM + + + + compoundHeadNonHead + + + + + + + + + + + prefixBe1 + + + + be + + + + BEPFX1 + + + compoundNonHeadHead + + + + + + + + + + + prefixBe2 + + + + be + + + + BEPFX2 + + + + ablautPast + + + + + + + + + + + + + + + + ABLAUT + + + + compoundConstrained + + + + + + + + + + + + + + + + + + + + + + mun + man + min + + STEM + bring + + + + fer + FER + carry + + + + duc + DUC + lead + + + tul + + TUL + carry.PST.SUPPL + + + + lob + LOB + praise + + + + vic + VIC + conquer + + + + pan + PAN + bread + + + nox + NOXN + night + + + nox + NOXV + harm + + + + lex + LEX + law + + + dom + DOMN + house + + + dom + DOMV + tame + + + + sing + SING + sing + + + gen + GEN + kind + + + + nov + + NOV + new + + + mit + MIT + send + + + fic + FIC + make + + + + sec + SEC + cut + + + lav + LAV + wash + + + + + + MprGroups + + + + themeX + + + + x + + + THEMEX + + + themeY + + + + y + + + THEMEY + + + endZ + + + + z + + + ENDZ + + + + endW + + + + w + + + ENDW + + + endH + + + + h + + + ENDH + + + + + + ped + PED + foot + + + + kat + KAT + cat + + + sod + SOD + salt + + + lim + LIM + threshold + + + + + + diff --git a/conformance/languages/fusional-latin/words.yaml b/conformance/languages/fusional-latin/words.yaml new file mode 100644 index 000000000..0b46c239d --- /dev/null +++ b/conformance/languages/fusional-latin/words.yaml @@ -0,0 +1,614 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). This grammar has no phonological rules and no +# AffixTemplate at all (every MorphologicalRule/RealizationalRule/CompoundingRule applies freely, +# unordered) -- so a bare root is a valid word whenever nothing else (a StemName region, a Family +# collision, an IsBlocked check) specifically forbids it. +# +# 1. mun -> STEM's default (unrestricted) allomorph, bare, no person feature assigned at all. The +# default allomorph carries no stemName, so it has no region to fail to satisfy. +# -> STEM|mun, rules: [] +# 2. muno -> STEM's default allomorph + P1 ("-o"). Once a person feature IS assigned, snP1P2 and +# snP1P3's regions jointly exhaust the whole person space {1,2,3} -- so the default allomorph, +# valid only in the absence of any person-governed region, can never combine with ANY person +# suffix. Zero parses. +# -> expect_fail +# 3. mano -> STEM's snP1P2-restricted allomorph "man" + P1. P1 is in snP1P2's region {1,2} -> valid. +# -> STEM+P1|mano, rules: [mrPers1] +# 4. mant -> STEM's snP1P2-restricted allomorph "man" + P3 ("-t"). P3 is NOT in snP1P2's region +# {1,2} -> zero parses. +# -> expect_fail +# 5. fer -> FER, bare, no lexical head features at all, no rule touches it -> trivially valid. +# -> FER|fer, rules: [] +# +# The remaining words either reuse these same mechanisms on independent lexical material (the +# realizational-rule/family-blocking/circumfix/infix/compounding/MPR-group/ablaut sections) or are +# expect_fail words whose signature is the empty set. +# +# Reconciliation against the oracle (self-check): the 5 words above matched on the first run with no +# disagreement. The remaining 44 words needed several real fixes before matching -- none were hand- +# derivation errors in the sense of "wrong linguistic reasoning"; all were genuine grammar-authoring/ +# engine-interaction discoveries, reported here in full rather than silently smoothed over: +# +# 1. Morph-chain order for a PREFIX is LEFT-TO-RIGHT SURFACE POSITION (prefix morpheme first), not +# "root first, affixes after" the way every suffix-only example in this suite (and suffixing- +# quechua) reads -- none of the ledger's source v1 fixtures used a real MorphemeId on a +# prefixing rule (they tested the mechanism only, MorphemeId left unset), so there was no +# precedent to crib from. Resolved empirically via --propose: confirmed on gelobt (GET+LOB, not +# LOB+GET) and xped (THEMEX+PED). +# 2. The compounding seam's boundary marker "+" must be declared as a BoundaryDefinition (DTD +# element under CharacterDefinitionTable), not a plain SegmentDefinition -- declaring it as a +# segment made every compound word find ZERO parses (confirmed by isolated probing against v1 +# compounding/nonhead-not-root's own grammar.xml, which uses BoundaryDefinition). It renders as +# "+?" in the signature (matching v1's own "(pʰ)ut+?dat"), never as a literal character in the +# surface word text. +# 3. mrSuffixS3's literal suffix was originally "-s", identical to mrPers1's own "-s" ending on +# any posV root sharing no AffixTemplate -- every "-s"-suffixed word (muns, lobs) spuriously +# double-analyzed as both. Renamed S3's suffix to "-h" (mrSuffixS3 requiredPartsOfSpeech=posV; +# mrEndH is a DIFFERENT rule, gated posN, so the two "-h" suffixes never collide). +# 4. mrAblaut, gated on the shared "posV", let ANY posV word containing "a" get a spurious extra +# analysis where the "a" was hypothesized as an ablaut-changed "i" reverting to some OTHER posV +# root -- concretely, "mano" (STEM+P1) ALSO analyzed as STEM(reverting "man"'s "a" back to "i", +# landing on the real "min" allomorph)+ABLAUT+P1, a genuine cross-construct collision between +# two unrelated grammar sections that happened to share both a POS gate and a coincidental +# vowel-alternation pattern. Fixed by giving SING (mrAblaut's only intended target) its own POS, +# posAblautV, confining the reversal search to exactly the one root it describes. +# 5. The two CompoundingRules (mrCompoundHN, mrCompoundNH) both had unrestricted wildcard head/ +# nonhead patterns and no POS gating, so mrCompoundNH ALSO matched "pannox" (reading it as +# nonhead="pan"+head="nox", producing the identical signature mrCompoundHN already derives) -- +# every compound word double-counted. Fixed by giving each CompoundingRule's Set its own disjoint +# POS pair (posN1/posV1 for Set 1, posN2/posV2 for Set 2) via headPartsOfSpeech/ +# nonHeadPartsOfSpeech, confining each rule to its own lexical material. +# 6. Set 2 (prefix-commute) originally put the homophone-ambiguity pair on the NON-head (mirroring +# Set 1) with mrPrefixBe2 (posV-gated) attaching to the HEAD (a plain noun, "dom") -- but a +# posV-gated rule can never apply to a noun, so lexbedom always found zero parses. Fixed by +# moving the homophone pair (DOMN/DOMV) onto the HEAD instead, so mrPrefixBe2 has a posV2 +# reading to attach to; the non-head (LEX) is a plain, unambiguous noun in this set instead. +# 7. Even after fix 6, lexbedom still found zero parses: mpr-groups/output-overwrite and +# compounding/prefix-commute have genuinely CONFLICTING requirements on morphologicalRuleOrder +# within one stratum. output-overwrite needs "linear" order to be observable at all (a v1 +# fixture requirement); prefix-commute's head-recursion (the prefix unapplying from the +# compounding HEAD span before the compound itself unapplies) only succeeds under "unordered" -- +# confirmed by toggling the setting and watching lexbedom and yxpedz trade places. Resolved by +# splitting the grammar into two Strata sharing the same CharacterDefinitionTable: "Main" +# (unordered, everything else) and "MprGroups" (linear, only the two mpr-groups constructs). +# +# Findings 2-7 are documented at their point of relevance in grammar.xml as well. None required a +# harness change; all were resolved within the grammar itself once understood. + +language: Vetruna +inspired_by: ["Latin (Italic)", "Russian (Slavic)", "German (Germanic) ablaut"] +sources: + - "WALS - Fusion, Exponence, Inflectional Synthesis in Inflectional Morphology chapters" + - "Corbett, Suppletion (2007) - inflectional classes, syncretism, principal parts" +requires: [] +words: + - word: mun + note: STEM's default (unrestricted) allomorph, bare -- no person feature assigned, so there is no StemName region to fail. + parses: + - signature: "STEM|mun" + gloss: bring + rules: [] + exercises: + - "Stem names" + + - word: muno + note: STEM default allomorph + P1 -- once ANY person feature is assigned, snP1P2/snP1P3's regions jointly exhaust {1,2,3}, so the region-less default allomorph can never combine with a person suffix at all. + expect_fail: true + exercises: + - "Stem names" + + - word: muns + note: STEM default allomorph + P2 -- same reasoning as muno. + expect_fail: true + exercises: + - "Stem names" + + - word: munt + note: STEM default allomorph + P3 -- same reasoning as muno. + expect_fail: true + exercises: + - "Stem names" + + - word: mano + note: STEM's snP1P2-restricted allomorph "man" + P1 ("-o") -- P1 is in snP1P2's region {1,2}. + parses: + - signature: "STEM+P1|mano" + gloss: bring-1SG + rules: [mrPers1] + exercises: + - "Stem names" + + - word: mans + note: STEM's snP1P2-restricted allomorph "man" + P2 ("-s") -- P2 is in snP1P2's region {1,2}. + parses: + - signature: "STEM+P2|mans" + gloss: bring-2SG + rules: [mrPers2] + exercises: + - "Stem names" + + - word: mant + note: STEM's snP1P2-restricted allomorph "man" + P3 -- P3 is NOT in snP1P2's region {1,2} -- zero parses. + expect_fail: true + exercises: + - "Stem names" + + - word: man + note: STEM's snP1P2-restricted allomorph, bare -- no person feature assigned means no region is satisfied, so this stemName-governed allomorph fails standalone (unlike the default allomorph's "mun"). + expect_fail: true + exercises: + - "Stem names" + + - word: mino + note: >- + STEM's snP1P3-restricted allomorph "min" + P1 -- P1 is in snP1P3's region {1,3}, AND also in + snP1P2's region {1,2}: the shared-region row, proving 1st-person overlap between the two stem + names doesn't spuriously block either one. + provenance: "W5, StemName selection + default-stem fallback" + parses: + - signature: "STEM+P1|mino" + gloss: bring-1SG + rules: [mrPers1] + exercises: + - "Stem names" + + - word: mins + note: STEM's snP1P3-restricted allomorph "min" + P2 -- P2 is NOT in snP1P3's region {1,3} -- zero parses. + expect_fail: true + exercises: + - "Stem names" + + - word: mint + note: STEM's snP1P3-restricted allomorph "min" + P3 -- P3 is in snP1P3's region {1,3}. + parses: + - signature: "STEM+P3|mint" + gloss: bring-3SG + rules: [mrPers3] + exercises: + - "Stem names" + + - word: min + note: STEM's snP1P3-restricted allomorph, bare -- fails standalone for the same reason "man" does. + expect_fail: true + exercises: + - "Stem names" + + - word: fer + note: FER ("carry"), bare, no lexical head features at all -- trivially valid, no rule touches it. + parses: + - signature: "FER|fer" + gloss: carry + rules: [] + exercises: [] + + - word: ferid + note: FER + RPAST (a RealizationalRule, no AffixTemplate/Slot wrapper needed) -- the root's own FS has no tense value at all, so RPAST's IsBlocked presence check finds nothing to collide with. + provenance: "W5, RealizationalAffixProcessRule core mechanics + IsBlocked" + parses: + - signature: "FER+RPAST|ferid" + gloss: carry-PST.REALIZ + rules: [rrPast] + exercises: + - "RealizationalAffixProcessRule" + + - word: feres + note: FER + TENSE ("-es", ordinary affix, sets tense=pres) -- pre-loads a tense value so the realizational rule's blocking check below has something to trip on. + parses: + - signature: "FER+TENSE|feres" + gloss: carry-PRES + rules: [mrTense] + exercises: + - "RealizationalAffixProcessRule" + + - word: feresid + note: >- + FER + TENSE + RPAST attempted together -- mrTense sets tense=pres first, then RPAST's IsBlocked + check sees tense is ALREADY present (even though it's pres, not past) and blocks -- a + value-equality mis-implementation would wrongly accept this; presence alone is what matters. + expect_fail: true + blocked_by: [rrPast] + exercises: + - "RealizationalAffixProcessRule" + + - word: duc + note: DUC ("lead"), bare, regular member of famCarry -- no collision, trivially valid. + parses: + - signature: "DUC|duc" + gloss: lead + rules: [] + exercises: [] + + - word: tul + note: TUL ("carry.PST.SUPPL"), bare, irregular suppletive member of famCarry with tense lexically fixed to past -- direct lexical lookup never reaches CheckBlocking. + parses: + - signature: "TUL|tul" + gloss: carry.PST.SUPPL + rules: [] + exercises: + - "RealizationalAffixProcessRule" + + - word: ducit + note: >- + DUC + PAST2 ("-it", sets tense=past) attempted -- the synthesized candidate's tense=past FS + subsumes TUL's own lexically-fixed FS (family collision), so Word.CheckBlocking fires and + substitutes in TUL's own shape; since that substituted shape ("tul") does not match the surface + "ducit", the candidate is discarded. Zero parses. + expect_fail: true + blocked_by: [mrPast2] + provenance: "W5, LexFamily blocks a more-general family member (Word.CheckBlocking)" + exercises: + - "RealizationalAffixProcessRule" + + - word: ducunt + note: DUC + PL2 ("-unt", sets num=pl only, never touches tense) -- never collides with TUL's tense-only lexical fixing, so not blocked. + parses: + - signature: "DUC+PL2|ducunt" + gloss: lead-PL + rules: [mrPl2] + exercises: + - "RealizationalAffixProcessRule" + + - word: lob + note: LOB ("praise"), bare. + parses: + - signature: "LOB|lob" + gloss: praise + rules: [] + exercises: [] + + - word: gelobt + note: LOB circumfixed "ge-...-t" (German ge-...-t past-participle-plausible) -- simultaneous prefix+suffix wrapped around one copied stem span, both pieces contributed by the SAME rule/morpheme id. + provenance: "W9.1 probe, AffixProcessRuleTests.CircumfixRules" + parses: + - signature: "GET+LOB|gelobt" + gloss: PTCP-praise + rules: [mrCircumfixGeT] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: lobh + note: >- + LOB + S3 ("-h", ordinary suffix, independent of the circumfix rule). S3's suffix is "-h", not + "-s": isolated probing found that "-s" collided with mrPers2's own literal "-s" suffix (both + apply freely to any posV root), spuriously double-analyzing every "-s"-suffixed posV word as + both S3 and P2 -- see grammar.xml's mrSuffixS3 comment. + parses: + - signature: "LOB+S3|lobh" + gloss: praise-3SG + rules: [mrSuffixS3] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: gelobth + note: LOB circumfixed AND suffixed with S3 in the same word -- proves the circumfix composes with an independent ordinary suffix rule. + parses: + - signature: "GET+LOB+S3|gelobth" + gloss: PTCP-praise-3SG + rules: [mrCircumfixGeT, mrSuffixS3] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: gelobthh + note: A doubled S3 suffix -- no rule combination produces two trailing "h"s. Negative control, mirroring v1 affix-shapes/circumfix's own doubled-suffix negative row. + expect_fail: true + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: vic + note: VIC ("conquer"), bare -- the perfect-stem shape (Latin vici-plausible), no infix. + parses: + - signature: "VIC|vic" + gloss: conquer + rules: [] + exercises: [] + + - word: vinc + note: VIC's present stem, nasal-infixed ("vi" + "n" + "c") -- Latin vincere-plausible; two MorphologicalInput parts (first 2 segments, then the remainder), "n" inserted between them. + provenance: "W9.1 probe, AffixProcessRuleTests.InfixRules" + parses: + - signature: "VIC+PRESSTEM|vinc" + gloss: conquer-PRES.STEM + rules: [mrNasalInfix] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: pannox + note: >- + PAN (head, "bread") + NOX (nonhead, bare root) compounded head+nonhead -- NOX is homophonous + across posN ("night") and posV ("harm"), so both are independently valid non-head roots for the + same surface string: two genuinely distinct analyses. The compounding seam's boundary marker + "+" is a BoundaryDefinition (not a plain segment, see grammar.xml's CharacterDefinitionTable + comment); it renders as "+?" in the signature's shape half, matching v1 compounding's own + "(pʰ)ut+?dat"-style rendering -- it is never part of the literal surface word text itself. + parses: + - signature: "PAN+NOXN|pan+?nox" + gloss: bread-night + rules: [mrCompoundHN] + exercises: + - "CompoundingRule" + - signature: "PAN+NOXV|pan+?nox" + gloss: bread-harm + rules: [mrCompoundHN] + exercises: + - "CompoundingRule" + + - word: benox + note: >- + NOXV, be-prefixed, standalone (no compounding involved) -- proves mrPrefixBe1 itself works + correctly in isolation, so panbenox's rejection below is specifically about the compounding + non-head-must-be-a-root constraint, not some defect in the prefix rule itself. + parses: + - signature: "BEPFX1+NOXV|benox" + gloss: PST-harm + rules: [mrPrefixBe1] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: panbenox + note: >- + PAN + be-prefixed NOX compounded head+nonhead -- the non-head span "benox" is prefix+root, NOT + a bare root, and CompoundingRule's analysis-side search requires the non-head to be a bare root + (computational-complexity reasons, per AnalysisCompoundingRule.cs) -- the prefix is discarded as + an invalid analysis rather than recursively re-applied. Zero parses. + expect_fail: true + blocked_by: [mrCompoundHN] + provenance: "P3, CompoundingRuleTests.SimpleRules reconfiguration 3 (non-head-not-root variant)" + exercises: + - "CompoundingRule" + + - word: pandex + note: PAN + "dex" -- "dex" is neither a root nor any rule's output, so no compound analysis exists at all. + expect_fail: true + exercises: + - "CompoundingRule" + + - word: lexdom + note: >- + LEX (nonhead, "law", posN2 only) + DOM (head, bare) compounded nonhead+head -- DOM is + homophonous across posN2 ("house") and posV2 ("tame"), so both are independently valid head + roots for the same surface string (the mirror image of Set 1's ambiguity, which sat on the + non-head instead). + parses: + - signature: "LEX+DOMN|lex+?dom" + gloss: law-house + rules: [mrCompoundNH] + exercises: + - "CompoundingRule" + - signature: "LEX+DOMV|lex+?dom" + gloss: law-tame + rules: [mrCompoundNH] + exercises: + - "CompoundingRule" + + - word: lexbedom + note: >- + LEX (nonhead) + be-prefixed DOM (head) compounded nonhead+head -- here the PREFIX attaches to + the HEAD, not the non-head, and CompoundingRule's non-head-must-be-a-root requirement never + constrains the head span at all, so this succeeds where panbenox failed: the prefix unapplies + cleanly from the head span "bedom" -- but mrPrefixBe2 requires posV2, so only DOM's VERB reading + (DOMV, "tame") is prefix-eligible; the noun reading (DOMN) can never combine with the prefix at + all, so unlike lexdom's bare-head ambiguity, this word has exactly one analysis, not two. The + prefix commutes with compounding rule ordering exactly as v1's own prefix-commute fixture pins. + provenance: "P3, CompoundingRuleTests.SimpleRules reconfiguration 3 (prefix-commute variant)" + parses: + - signature: "LEX+BEPFX2+DOMV|lex+?bedom" + gloss: law-PST-tame + rules: [mrCompoundNH, mrPrefixBe2] + exercises: + - "CompoundingRule" + + - word: lexbex + note: LEX + "bex" -- "bex" is neither DOM nor any valid prefixed-head shape, so no compound analysis exists. + expect_fail: true + exercises: + - "CompoundingRule" + + - word: ped + note: PED ("foot"), bare -- no MPR feature preset on this lexical entry, trivially valid. + parses: + - signature: "PED|ped" + gloss: foot + rules: [] + exercises: [] + + - word: xped + note: PED prefixed by mrThemeX ("x-", sets MPR feature mprPedA). + provenance: "W3.1, MprFeatureGroupOutput.Overwrite in MPR output accumulation" + parses: + - signature: "THEMEX+PED|xped" + gloss: THEME.A-foot + rules: [mrThemeX] + exercises: + - "MPR features/groups" + + - word: yxped + note: PED prefixed by mrThemeX then mrThemeY ("y-", sets mprPedB) -- ThemeGroup's outputType="overwrite" means mrThemeY's own output DROPS mprPedA rather than accumulating it alongside mprPedB. + parses: + - signature: "THEMEY+THEMEX+PED|yxped" + gloss: THEME.B-THEME.A-foot + rules: [mrThemeX, mrThemeY] + exercises: + - "MPR features/groups" + + - word: xpedz + note: PED prefixed by mrThemeX then suffixed by mrEndZ (requiredMPRFeatures="mprPedA") -- mprPedA is still present (only mrThemeX applied), so mrEndZ's requirement is satisfied. + parses: + - signature: "THEMEX+PED+ENDZ|xpedz" + gloss: THEME.A-foot-END + rules: [mrThemeX, mrEndZ] + exercises: + - "MPR features/groups" + + - word: yxpedz + note: >- + PED prefixed by mrThemeX then mrThemeY then suffixed by mrEndZ -- the DISTINGUISHING row: after + mrThemeY, ThemeGroup's Overwrite semantics have already dropped mprPedA, so mrEndZ's + requiredMPRFeatures="mprPedA" fails. Zero parses. A pre-fix "flat union" mis-implementation + would wrongly accept this. + expect_fail: true + blocked_by: [mrEndZ] + exercises: + - "MPR features/groups" + + - word: kat + note: KAT ("cat"), bare -- ruleFeatures preset both AllGroup members (mprConjA, mprConjB) directly on the lexical entry. + parses: + - signature: "KAT|kat" + gloss: cat + rules: [] + exercises: [] + + - word: sod + note: SOD ("salt"), bare -- ruleFeatures preset mprConjA (half of AllGroup) and mprConjC (half of AnyGroup). + parses: + - signature: "SOD|sod" + gloss: salt + rules: [] + exercises: [] + + - word: lim + note: LIM ("threshold"), bare -- no ruleFeatures preset at all. + parses: + - signature: "LIM|lim" + gloss: threshold + rules: [] + exercises: [] + + - word: katw + note: KAT + mrEndW (requiredMPRFeatures="mprConjA mprConjB", matchType="all") -- KAT has both AllGroup members, so the ALL requirement is satisfied. + provenance: "W3.1, MprFeatureGroupMatchType.All/Any in required-MPR gating" + parses: + - signature: "KAT+ENDW|katw" + gloss: cat-ALL.END + rules: [mrEndW] + exercises: + - "MPR features/groups" + + - word: sodw + note: SOD + mrEndW -- SOD has only mprConjA, not mprConjB, so the ALL requirement (both members) is NOT satisfied. Zero parses -- the pre-fix flat-overlap bug would have wrongly accepted this. + expect_fail: true + blocked_by: [mrEndW] + exercises: + - "MPR features/groups" + + - word: limw + note: LIM + mrEndW -- LIM has neither AllGroup member, so the ALL requirement fails outright. + expect_fail: true + exercises: + - "MPR features/groups" + + - word: kath + note: KAT + mrEndH (requiredMPRFeatures="mprConjC mprConjD", matchType="any") -- KAT has NEITHER AnyGroup member, so the ANY requirement fails. + expect_fail: true + exercises: + - "MPR features/groups" + + - word: sodh + note: SOD + mrEndH -- SOD has mprConjC (one AnyGroup member), so the ANY requirement (at least one) is satisfied. + parses: + - signature: "SOD+ENDH|sodh" + gloss: salt-ANY.END + rules: [mrEndH] + exercises: + - "MPR features/groups" + + - word: limh + note: LIM + mrEndH -- LIM has neither AnyGroup member, so the ANY requirement fails. + expect_fail: true + exercises: + - "MPR features/groups" + + - word: sing + note: SING ("sing", German singen-plausible), bare -- no lexical head features, present-stem citation form. + parses: + - signature: "SING|sing" + gloss: sing + rules: [] + exercises: [] + + - word: sang + note: >- + SING's past-tense form, realized ENTIRELY by changing the root's own vowel ("i"->"a") via + ModifyFromInput -- no affix at all. Ablaut as the sole exponent of the tense category (German + singen/sang-plausible). + parses: + - signature: "SING+ABLAUT|sang" + gloss: sing-PST.ABLAUT + rules: [mrAblaut] + exercises: + - "MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext" + + - word: mitlav + note: >- + MIT (head, mprCompReq+mprSubA) + LAV (nonhead) -- mrCompoundConstrained's subrule 0 + (requiredMPRFeatures="mprSubA" on HeadMorphologicalInput) matches since MIT carries mprSubA. + G5 (code-coverage-driven): previously zero coverage for a CompoundingSubrule's own per-subrule + requiredMPRFeatures/excludedMPRFeatures gating (distinct from the ordinary-MorphologicalRule + MPR-groups material elsewhere in this file). + provenance: "G5 coverage sweep, SynthesisCompoundingRule requiredMPRFeatures/excludedMPRFeatures" + parses: + - signature: "MIT+LAV|mit+?lav" + gloss: send-wash + rules: [mrCompoundConstrained] + exercises: + - "CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features)" + + - word: ficlav + note: >- + FIC (head, mprCompReq only, no mprSubA) + LAV (nonhead) -- subrule 0 fails (FIC lacks + mprSubA), so subrule 1 (excludedMPRFeatures="mprSubA") is tried instead and matches, since FIC + genuinely excludes mprSubA. Proves the per-subrule MPR gate actually discriminates between the + two subrules, not just falls through by pattern order. + provenance: "G5 coverage sweep, SynthesisCompoundingRule requiredMPRFeatures/excludedMPRFeatures" + parses: + - signature: "FIC+LAV|fic+?lav" + gloss: make-wash + rules: [mrCompoundConstrained] + exercises: + - "CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features)" + + - word: seclav + note: >- + SEC (head, no ruleFeatures at all) + LAV (nonhead) -- mrCompoundConstrained's own + headProdRestrictionsMprFeatures="mprCompReq" requires the head to carry mprCompReq, which SEC + does not, so the rule never applies at all (both subrules unreachable). G5 (code-coverage- + driven): previously zero coverage for CompoundingRule.headProdRestrictionsMprFeatures. + provenance: "G5 coverage sweep, SynthesisCompoundingRule.headProdRestrictionsMprFeatures" + expect_fail: true + blocked_by: [mrCompoundConstrained] + exercises: + - "CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features)" + + - word: genlav + note: >- + GEN (head, mprCompReq, family famCompBlock) + LAV (nonhead) -- the compounding rule itself + applies successfully, but GEN's family also contains the suppletive NOV, and CheckBlocking + substitutes NOV's own entry in wherever GEN's compound output would surface; the resulting + resynthesis ("nov") no longer matches this word's own input shape ("gen+lav"), so the overall + analysis has zero parses. G5 (code-coverage-driven): previously zero coverage for + CompoundingRule's own Blockable/CheckBlocking path (only ever exercised through an ordinary + MorphologicalRule elsewhere in this file, via famCarry). + provenance: "G5 coverage sweep, SynthesisCompoundingRule Blockable/CheckBlocking" + expect_fail: true + blocked_by: [mrCompoundConstrained] + exercises: + - "CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features)" + + - word: mitlavlav + note: >- + MIT+LAV (mitlav, above) attempted as the HEAD of a further application of the same + compounding rule, with LAV again as the nonhead. Hand-derived to hit MaxApplicationCount (the + DTD's own default multipleApplication="1" caps one application per derivation), but a + dotnet-coverage check after this word was added showed SynthesisCompoundingRule's + MaxApplicationCount lines still unreached -- the zero-parse result is real (confirmed by + self-check) but its cause is not confirmed to be MaxApplicationCount specifically; most likely + analysis-direction compound-splitting does not recurse into a derived (non-lexical) head span + at all, rejecting this candidate before MaxApplicationCount is ever checked. Left in as a + correct expect_fail pin, but MaxApplicationCount itself remains a documented residual gap (see + docs/conformance-language-suite-plan.md G5). + provenance: "G5 coverage sweep, SynthesisCompoundingRule (MaxApplicationCount attempt, gap remains)" + expect_fail: true + blocked_by: [mrCompoundConstrained] + exercises: + - "CompoundingRule constraints (MaxApplicationCount/Blockable/head-nonhead syntactic features)" diff --git a/conformance/languages/polysynthetic-inuit/grammar.xml b/conformance/languages/polysynthetic-inuit/grammar.xml new file mode 100644 index 000000000..8230edf9e --- /dev/null +++ b/conformance/languages/polysynthetic-inuit/grammar.xml @@ -0,0 +1,427 @@ + + + + + Nuvattak + + + n + v + n1 + v1 + n2 + v2 + nDelR + nMultiSeg + nMDC + + + + cons+- + voc+- + high+- + low+- + back+- + round+- + voice+- + + place + + bilabial + labiodental + alveolar + sibilant + lateral + velar + uvular + nasalLab + nasalAlv + vocalic + + + + + + Main + + + a + + + + + + i + + + + + + u + + + + + + + p + + + + + + b + + + + + + m + + + + + + v + + + + + + t + + + + + + s + + + + + + l + + + + + + n + + + + + + k + + + + + + q + + + + + + + + + + + + + + Any + + HighVowel + + + + HFUVowel + + + + + VowelU + + + + + BackRndVowel + + + + + TSeg + + + + + iDeletion + + + + + + + + + + + + + multiSegRaising + + + + + + + + + + + + + mdcRaising + + + + + + + + + + + + mdcTDeletion + + + + + + + + + + + + + + + Phonology + + + + bubui + DR1 + probe.bubui + + + buibu + DR2 + probe.buibu + + + buibui + DR3 + probe.buibui + + + + kuiikuii + MS + probe.multiseg + + + + buiibuii + MDC + probe.mdc + + + + matu + MDC2 + probe.mdc2 + + + + + + Derivation + + + + verbalize + + + + liq + + + DERIV + + + + compoundHeadNonHead + + + + + + + + + + + prefixPi1 + + + + pi + + + PIPFX1 + + + + compoundNonHeadHead + + + + + + + + + + + prefixMa2 + + + + ma + + + MAPFX2 + + + + + + [Any]* + + + nuna + NUNA + land + + + + aku + AKU + meat + + + tat + TATN + shadow + + + tat + TATV + tie + + + + sila + SILA + weather + + + nuk + NUKN + horn + + + nuk + NUKV + pull + + + + + + Inflection + + + + indic3sg + + + + vuq + + + INFL + + + + + + diff --git a/conformance/languages/polysynthetic-inuit/words.yaml b/conformance/languages/polysynthetic-inuit/words.yaml new file mode 100644 index 000000000..03cb21fcd --- /dev/null +++ b/conformance/languages/polysynthetic-inuit/words.yaml @@ -0,0 +1,291 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). +# +# 1. nuna -> NUNA ("land", posN) bare noun root, no rule -> trivially valid. +# -> NUNA|nuna, rules: [] +# 2. nunaliq -> NUNA + DERIV ("-liq", posN->posV verbalizer, in the deep Derivation stratum): the +# root is verbalized, no inflection yet. +# -> NUNA+DERIV|nunaliq, rules: [mrDeriv] +# 3. nunaliqvuq -> NUNA + DERIV + INFL ("-vuq", posV->posV, in the shallower Inflection stratum): +# the derived posV stem built in stratum 0 feeds inflection in stratum 1 -- recursion across +# strata, the stratum-ordering headline. +# -> NUNA+DERIV+INFL|nunaliqvuq, rules: [mrDeriv, mrInfl] +# 4. nunavuq -> INFL attempted directly on the bare noun "nuna": mrInfl requires posV, but "nuna" +# is posN and nothing verbalized it, so inflection can never attach -> zero parses. +# -> expect_fail, blocked_by: [mrInfl] +# 5. akutat -> AKU ("meat", posN1 head) + TAT (posN1/posV1 nonhead, homophonous) compounded +# head+nonHead via mrCompoundHN: the nonhead "tat" resolves as BOTH its noun (TATN) and verb +# (TATV) reading, so two analyses. The "+" seam renders as "+?". +# -> AKU+TATN|aku+?tat ; AKU+TATV|aku+?tat, rules: [mrCompoundHN] each +# +# The remaining words reuse these mechanisms on independent lexical/rule material -- the prefix- +# commute compounding Set 2, the Guesser/LexicalGuess pattern, and the three seam-phonology probes +# (i-deletion multi-site unapplication, multi-segment raising, and the P6 deletion-composition) -- or +# are expect_fail words whose signature is the empty set. +# +# Reconciliation against the oracle (self-check): documented per word where the oracle disagreed with +# the hand derivation; the trace is authoritative per the task discipline. In particular, the +# multi-parse "bubu" word carries prDelReins on every parse because TraceRuleAttributor unions the +# phonological rules fired across all of a word's analyses (its own documented ambiguous-word +# limitation, the same one templatic-semitic's gigugu note records) -- here that union is honest +# anyway, since every "bubu" analysis genuinely deletes an "i". + +language: Nuvattak +inspired_by: ["Central Alaskan Yup'ik (Eskimo-Aleut)", "Kalaallisut (Eskimo-Aleut)", "Mohawk (Iroquoian)"] +sources: + - "WALS - polysynthesis, noun incorporation, and derivation chapters" + - "Mithun, The Languages of Native North America (1999) - Eskimo-Aleut derivation strata, Iroquoian incorporation" + - "Fortescue, West Greenlandic (Kalaallisut) - recursive derivational suffixation and postbase ordering" +requires: [phonology] +words: + - word: nuna + note: NUNA ("land", posN) bare noun root -- no rule touches it, trivially valid. + parses: + - signature: "NUNA|nuna" + gloss: land + rules: [] + exercises: [] + + - word: nunaliq + note: NUNA + DERIV ("-liq", posN->posV verbalizer) in the deep Derivation stratum -- verbalized, not yet inflected. + parses: + - signature: "NUNA+DERIV|nunaliq" + gloss: land-VBLZ + rules: [mrDeriv] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: nunaliqvuq + note: >- + NUNA + DERIV + INFL -- the derivational "-liq" applies in the deep Derivation stratum (posN-> + posV), then the derived verb stem feeds the inflectional "-vuq" (3sg indicative) in the + shallower Inflection stratum: recursion across strata, the stratum-ordering headline construct. + parses: + - signature: "NUNA+DERIV+INFL|nunaliqvuq" + gloss: land-VBLZ-3SG.IND + rules: [mrDeriv, mrInfl] + exercises: + - "Stratum (Linear/Unordered rule order)" + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: nunavuq + note: INFL attempted directly on the bare noun "nuna" -- mrInfl requires a posV stem, but nothing verbalized "nuna" (posN), so inflection can never attach. Zero parses (inflection presupposes the derivation stratum ran). + expect_fail: true + blocked_by: [mrInfl] + exercises: + - "Stratum (Linear/Unordered rule order)" + + - word: akutat + note: >- + AKU ("meat", posN1 head) + TAT (nonhead) compounded head+nonHead via mrCompoundHN -- "tat" is + homophonous between its noun (TATN) and verb (TATV) reading and both resolve as the non-head + root, so two analyses sharing the surface "aku+tat" (rendered "aku+?tat" with the "+" boundary). + parses: + - signature: "AKU+TATN|aku+?tat" + gloss: meat-shadow + rules: [mrCompoundHN] + exercises: + - "CompoundingRule" + - "Boundary markers (CharacterDefinitionTable)" + - signature: "AKU+TATV|aku+?tat" + gloss: meat-tie + rules: [mrCompoundHN] + exercises: + - "CompoundingRule" + - "Boundary markers (CharacterDefinitionTable)" + + - word: pitat + note: TAT's verb reading (TATV, posV1) with the pi- prefix, standalone (no compounding) -- proves mrPrefixPi1 works in isolation, so akupitat's rejection below is specifically the non-head-must-be-a-root constraint. + parses: + - signature: "PIPFX1+TATV|pitat" + gloss: PFX-tie + rules: [mrPrefixPi1] + exercises: + - "AffixProcessRule: prefix/suffix/circumfix/infix" + + - word: akupitat + note: >- + AKU (head) + pi-prefixed TAT compounded head+nonHead -- the non-head span "pitat" is prefix+root, + NOT a bare root, and mrCompoundHN's analysis-side search requires the non-head to already be a + bare root (AnalysisCompoundingRule discards a non-root non-head), so this has zero parses -- the + distinguishing non-head-not-root pin. + expect_fail: true + blocked_by: [mrCompoundHN] + provenance: "P3, CompoundingRuleTests.SimpleRules reconfiguration 3 (non-head-not-root variant)" + exercises: + - "CompoundingRule" + + - word: akutas + note: AKU + "tas" -- "tas" is neither a root nor any rule's output, so no compound analysis exists at all. + expect_fail: true + exercises: + - "CompoundingRule" + + - word: silanuk + note: >- + SILA ("weather", posN2 nonhead) + NUK (head) compounded nonHead+head via mrCompoundNH -- "nuk" + is homophonous between its noun (NUKN) and verb (NUKV) reading on the HEAD, so two analyses + sharing "sila+nuk" (rendered "sila+?nuk"). + parses: + - signature: "SILA+NUKN|sila+?nuk" + gloss: weather-horn + rules: [mrCompoundNH] + exercises: + - "CompoundingRule" + - "Boundary markers (CharacterDefinitionTable)" + - signature: "SILA+NUKV|sila+?nuk" + gloss: weather-pull + rules: [mrCompoundNH] + exercises: + - "CompoundingRule" + - "Boundary markers (CharacterDefinitionTable)" + + - word: silamanuk + note: >- + SILA (nonhead) + ma-prefixed NUK (head) compounded nonHead+head -- here the prefix attaches to + the HEAD span, which mrCompoundNH's non-head-root requirement never constrains, so it succeeds + where akupitat failed. mrPrefixMa2 requires posV2, so only NUK's verb reading (NUKV) is + prefix-eligible: exactly one analysis, not two. The prefix commutes with compounding. + provenance: "P3, CompoundingRuleTests.SimpleRules reconfiguration 3 (prefix-commute variant)" + parses: + - signature: "SILA+MAPFX2+NUKV|sila+?manuk" + gloss: weather-PFX-pull + rules: [mrCompoundNH, mrPrefixMa2] + exercises: + - "CompoundingRule" + + - word: silamak + note: SILA + "mak" -- "mak" is neither NUK nor any valid prefixed-head shape, so no compound analysis exists. + expect_fail: true + exercises: + - "CompoundingRule" + + - word: takuvuq + note: >- + Guesser/LexicalGuess: "taku" (see) is deliberately absent from the lexicon, so a normal parse + finds nothing; with root-guessing on, the "[Any]*" lexical pattern guesses a posV root, which + the inflection "-vuq" then attaches to. Two distinct guesses per PROTOCOL.md's pinned rendering + ("taku"+INFL, and the whole surface as a bare unaffixed root); a guessed root's morph-chain + component is the literal guessed substring itself, with no distinguishing marker. Each distinct + guess appears TWICE because this three-stratum grammar's analysis reaches the same guessable + shape at more than one stratum level (the guesser runs per analysis candidate, and the signature + multiset does not dedupe -- PROTOCOL.md section 3), so the honest multiset is 2x2. Self-check + only -- BatchCommand cannot guess, see PROTOCOL.md; the harness omits this word from adapter-mode + materialization. + parses: + - signature: "taku+INFL|takuvuq" + gloss: see-3SG.IND + guess: true + rules: [mrInfl] + exercises: + - "Guesser/LexicalGuess" + - signature: "taku+INFL|takuvuq" + guess: true + rules: [mrInfl] + exercises: + - "Guesser/LexicalGuess" + - signature: "takuvuq|takuvuq" + gloss: see.3SG.IND + guess: true + rules: [] + exercises: + - "Guesser/LexicalGuess" + - signature: "takuvuq|takuvuq" + guess: true + rules: [] + exercises: + - "Guesser/LexicalGuess" + + - word: bubu + note: >- + rewrite/deletion-reinsertion (P2 multi-site deletion unapplication): "i" is obligatorily deleted + after a high vowel, so the one surface "bubu" has THREE distinct underlying analyses -- DR1 + ("bubui", final i deleted), DR2 ("buibu", medial i deleted), DR3 ("buibui", both deleted). Every + analysis genuinely fired prDelReins, so the trace-union (see file header) is honest on every + parse. + provenance: "P2, RewriteRuleTests.DeletionRules first reconfiguration (multi-site deletion unapplication)" + parses: + - signature: "DR1|bubu" + gloss: probe.bubui + rules: [prDelReins] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + - signature: "DR2|bubu" + gloss: probe.buibu + rules: [prDelReins] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + - signature: "DR3|bubu" + gloss: probe.buibui + rules: [prDelReins] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: buibu + note: The raw underlying shape with an undeleted "i" after a high vowel -- prDelReins is obligatory, so this surface form can never itself be a valid analysis. + expect_fail: true + blocked_by: [prDelReins] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: kuuukuuu + note: >- + rewrite/multiplesegment (W9.2): MS ("kuiikuii") surfaces with each "ii" raised to "uu" after a + back-round vowel -- a 2-segment target rewrite, "kuiikuii" -> "kuuukuuu". + provenance: "W9.2 probe, RewriteRuleTests.MultipleSegmentRules" + parses: + - signature: "MS|kuuukuuu" + gloss: probe.multiseg + rules: [prMultiSeg] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: kuiikuii + note: The raw underlying shape with un-raised "ii" -- prMultiSeg is obligatory wherever an "ii" follows a back-round vowel, so this surface form can never itself be a valid analysis. + expect_fail: true + blocked_by: [prMultiSeg] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: buuubuuu + note: >- + rewrite/multiplesegment-deletion-composition (P6 group-capture fix): MDC ("buiibuii") surfaces + as "buuubuuu" via the same 2-segment raising (prMDC1), composed in the same stratum with a + pure-deletion rule (prMDC2, t -> nothing) that never fires here (no "t") but whose reverse- + analysis interposes an optional "t" between the raising rule's target pair -- the exact + composition the P6 fix addressed. Only prMDC1 fires in synthesis, so rules names just it. + provenance: "P6, RewriteRuleTests.MultipleSegmentRules second reconfiguration (group-capture fix)" + parses: + - signature: "MDC|buuubuuu" + gloss: probe.mdc + rules: [prMDC1] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: buiibuii + note: The raw underlying shape with un-raised "ii" -- prMDC1 is obligatory, so this surface form can never itself be a valid analysis. + expect_fail: true + blocked_by: [prMDC1] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: mau + note: >- + MDC2 ("matu") exercises the P6 grammar's SECOND rule (prMDC2, t -> nothing before a back-round + vowel) on its own: "matu" has a "t" before "u", so it surfaces as "mau". This rule is inert on + the buuubuuu composition above (no "t"), where only its analysis-direction reversal matters; here + it actually fires. + parses: + - signature: "MDC2|mau" + gloss: probe.mdc2 + rules: [prMDC2] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: matu + note: The raw underlying shape with an undeleted "t" before "u" -- prMDC2 is obligatory, so this surface form can never itself be a valid analysis (see mau). + expect_fail: true + blocked_by: [prMDC2] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" diff --git a/conformance/languages/prefixal-athabaskan/grammar.xml b/conformance/languages/prefixal-athabaskan/grammar.xml new file mode 100644 index 000000000..9eb70b319 --- /dev/null +++ b/conformance/languages/prefixal-athabaskan/grammar.xml @@ -0,0 +1,198 @@ + + + + + Ketselan + + v + + + + ClassA + ClassB + HighTrans + + + Main + + a + i + o + b + d + g + h + k + l + n + s + t + w + y + + + + + Any + + + + Main + + + + classifier + + + + d + + + CLF + CLF + + + + modeTransitivePerfective + + + + yi + + + PRF + TRANS.PRF + + + modeIntransitive + + + + wo + + + IMPF + INTR + + + + subject + + + + shi + + + + ni + + + SBJ + SBJ + + + + object + + + + bi + + + OBJ + 3OBJ + + + + disjunctA + + + + ga + + + ADVA + ITER + + + disjunctB + + + + ho + + + ADVB + AREAL + + + + + + verbPrefixTemplate + classifier + mode + subject + object + advA + advB + + + + + kal + KAL + carry + + + tan + TAN + speak + + + + + + diff --git a/conformance/languages/prefixal-athabaskan/words.yaml b/conformance/languages/prefixal-athabaskan/words.yaml new file mode 100644 index 000000000..d2ccc87f6 --- /dev/null +++ b/conformance/languages/prefixal-athabaskan/words.yaml @@ -0,0 +1,220 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). This grammar is a pure STRESS grammar (spec Sec3 +# row 7): it embeds no v1 fixture and carries no provenance -- its job is exercising AffixTemplate +# morphology from the LEFT edge (a prefixal position-class template), which every other roster +# grammar (all suffix-dominant) leaves untested. +# +# The template (innermost -> outermost, i.e. application order for a prefixing rule -- see +# grammar.xml's AffixTemplate comment): classifier(1, obligatory) - mode(2, obligatory 2-way +# disjunctive slot) - subject(3, obligatory, adjacent-dependency disjunctive allomorphy) - +# object(4, optional, DISCONTINUOUS-dependency gate) - advA(5, optional, unconstrained) - +# advB(6, optional, unconstrained, outermost). +# +# 1. shiyidkal -> KAL ("carry", ruleFeatures="mprClassA") + the three OBLIGATORY slots only: +# mrClass ("d-") applies first (innermost) -> "dkal"; mrModeTrans ("yi-", ALSO sets +# mprHighTrans -- the FUSED portmanteau: one exponent realizing both the aspectual value and +# the transitivity-class marking a later slot depends on) applies next -> "yidkal"; mrSubj's +# subrule A (gated by mprClassA, "shi-") applies last (outermost of the three) -> "shiyidkal". +# Morph chain reads outermost-first (the deep-optional-affix-nesting P12..P1 convention): +# -> SBJ+PRF+CLF+KAL|shiyidkal, rules: [mrClass, mrModeTrans, mrSubj] +# 2. niyidtan -> TAN ("speak", ruleFeatures="mprClassB"), the SAME obligatory chain but mrSubj's +# OTHER subrule fires (gated by mprClassB, "ni-" instead of "shi-") -- proves the ADJACENT +# lexically-preset-class dependency picks the correct allomorph per root. +# -> SBJ+PRF+CLF+TAN|niyidtan, rules: [mrClass, mrModeTrans, mrSubj] +# 3. bishiyidkal -> KAL's obligatory chain (as in 1) PLUS mrObj ("bi-"): mrObj requires +# mprHighTrans, set two slots inward by mrModeTrans (slot 2) -- slot 3 (subject) sits in +# between and never touches this feature at all, so this is the DISCONTINUOUS dependency +# firing successfully across a skipped, irrelevant obligatory slot. +# -> OBJ+SBJ+PRF+CLF+KAL|bishiyidkal, rules: [mrClass, mrModeTrans, mrSubj, mrObj] +# 4. bishiwodkal -> the DISCONTINUOUS dependency's NEGATIVE control: KAL + mrClass + mrModeIntrans +# ("wo-", which does NOT set mprHighTrans -- a genuinely contrastive mode choice, not a free +# variant of mrModeTrans) + mrSubj, with mrObj ("bi-") attempted on top. mrObj's +# requiredMPRFeatures="mprHighTrans" is unsatisfied (only mrModeTrans sets it), so the object +# prefix cannot apply here at all -- zero parses, even though every other slot's own +# requirement is individually satisfied. +# -> expect_fail, blocked_by: [mrObj] +# 5. yidkal -> KAL + mrClass + mrModeTrans, but slot 3 (subject) is OBLIGATORY and omitted +# entirely -- the template cannot synthesize (or analyze) a completion missing an obligatory +# slot (mirrors bantu-verbal's andik / suffixing-quechua's walakchik precedent, mirrored here +# from the LEFT edge instead of the right). +# -> expect_fail, blocked_by: [mrSubj] +# +# The remaining words reuse these same mechanisms on independent combinations (mrModeIntrans alone +# with no object attempted; the two-optional-slot stack advA+advB; a bare-root omitting ALL THREE +# obligatory slots) or exercise the LEFT-edge mirror of bantu-verbal's own G2c-flagged finding: +# +# Reconciliation against the oracle (self-check): words 1-5 above, and every other word except one, +# matched the hand derivation on the first self-check run, including both expect_fail words' +# zero-parse status. ONE deliberate prediction was wrong, and the correction is itself the most +# useful finding in this file: "gahobishiyidkal" (the textual reverse of the flagship +# "hogabishiyidkal" -- advA's "ga-" and advB's "ho-" swapped in linear position). Having read +# bantu-verbal's own andikilisha finding (G2c: an AffixTemplate's slot order is a hard SYNTHESIS +# constraint, but with two OPTIONAL, generically-shaped ["any segment, 1+"] slots, ANALYSIS can +# retry to a fixpoint and peel the two affixes off in either linear order), this word was +# hand-predicted to be ACCEPTED by the same mechanism -- mrAdvA/mrAdvB are just as optional and +# unconstrained as bantu-verbal's causative/applicative pair. THE ORACLE REJECTS IT (zero parses), +# contradicting the prediction. Corrected in place below (expect_fail), and the reason why bantu- +# verbal's trick does NOT generalize here is itself instructive enough to trace all the way to +# engine source: `AnalysisAffixTemplateRule.ApplySlots` (src/SIL.Machine.Morphology.HermitCrab/ +# AnalysisAffixTemplateRule.cs) walks a template's slots from OUTERMOST to INNERMOST in ONE pass; at +# each slot index i it tries THAT slot's own rule at the word's CURRENT outer edge, and only if slot +# i is optional does it fall through to try slot i-1 at the SAME (unstripped) edge -- an obligatory +# slot's failure to match terminates the whole branch immediately, with no way back to a +# higher-indexed (already-passed) slot. Bantu-verbal's extensionTemplate has its ONE obligatory slot +# (finalVowel) at the OUTERMOST position, stripped first and unconditionally, so BOTH of its optional +# slots (causative, applicative) sit together, unobstructed, all the way to the root -- any +# out-of-turn match among them can fall through past the other via the optional-skip branch with +# nothing obligatory in the way. This grammar's swappable pair (advA, advB) is also outermost, but +# its three obligatory slots (subject, mode, classifier) sit BETWEEN that pair and the root: once +# advA is stripped out of its canonical (innermost-of-the-pair) turn, the very next slot the +# algorithm reaches inward is the OBLIGATORY subject slot -- and the actual next characters at the +# edge are advB's still-unstripped "ho-", not subject's "shi-"/"ni-", so subject's match fails and +# the whole branch dies with advB never given a chance to be tried (recursion only moves inward, it +# never revisits a slot index once passed). The complementary "skip advA first, try advB" branch +# dies exactly the same way one slot later. So the finding, honestly reconciled: an AffixTemplate's +# fixpoint analysis retry rescues an out-of-canonical-order pair of optional/unconstrained slots +# ONLY when every slot between that pair and the rest of the successful decomposition is ALSO +# optional -- an obligatory slot standing between the swappable pair and the root (as this prefixal +# template's obligatory-innermost design produces, the mirror image of bantu-verbal's +# obligatory-outermost design) is a hard analysis-side order-enforcement mechanism after all, for +# THAT specific slot arrangement. G2c's finding is real but not universal; this grammar's structure +# is the counter-example that shows what condition it actually depends on. + +language: Ketselan +inspired_by: ["Navajo (Athabaskan)"] +sources: + - "WALS - Prefixing vs. suffixing morphology chapter; Athabaskan verb-template surveys" + - "Young & Morgan, The Navajo Language: A Grammar and Colloquial Dictionary (verb prefix position classes)" + - "Rice, Morpheme Order and Semantic Scope: Word Formation in the Athapaskan Verb (discontinuous/fused prefix dependencies)" +requires: [] +words: + - word: shiyidkal + note: >- + KAL ("carry") + the three OBLIGATORY slots only (classifier "d-", mode mrModeTrans "yi-", + subject mrSubj subrule A "shi-", gated by the root's own preset mprClassA). No optional slot + fires. Establishes the minimal well-formed word under this template. + parses: + - signature: "SBJ+PRF+CLF+KAL|shiyidkal" + gloss: carry-CLF-TRANS.PRF-SBJ + rules: [mrClass, mrModeTrans, mrSubj] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: niyidtan + note: >- + TAN ("speak") + the same obligatory chain as shiyidkal, but mrSubj's OTHER subrule fires + ("ni-", gated by the root's preset mprClassB instead of mprClassA) -- proves the + lexically-preset agreement class correctly conditions which subject allomorph a given root + takes (bantu-verbal's kib/bel/nal precedent, applied to prefix-shape selection here). + parses: + - signature: "SBJ+PRF+CLF+TAN|niyidtan" + gloss: speak-CLF-TRANS.PRF-SBJ + rules: [mrClass, mrModeTrans, mrSubj] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: bishiyidkal + note: >- + KAL's obligatory chain (as in shiyidkal) plus mrObj ("bi-"): mrObj requires mprHighTrans, set + TWO slots inward by mrModeTrans (slot 2) -- slot 3 (subject) sits in between and never + touches this feature at all. This is the DISCONTINUOUS slot dependency the roster names: an + outer prefix's admissibility hinges on a non-adjacent inner prefix's own choice, skipping an + intervening (obligatory but irrelevant-to-this-gate) slot. + parses: + - signature: "OBJ+SBJ+PRF+CLF+KAL|bishiyidkal" + gloss: carry-CLF-TRANS.PRF-SBJ-3OBJ + rules: [mrClass, mrModeTrans, mrSubj, mrObj] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: bishiwodkal + note: >- + The discontinuous dependency's NEGATIVE control: KAL + mrClass + mrModeIntrans ("wo-", a + genuinely contrastive mode choice that does NOT set mprHighTrans) + mrSubj, with mrObj + ("bi-") attempted on top. mrObj's gate is unsatisfied (only mrModeTrans sets mprHighTrans), + so the object prefix cannot apply here even though every other slot's own requirement holds + individually. Zero parses -- proves the discontinuous dependency is a genuine gate, not a + trivially-always-true one (mrClass/mrModeTrans/mrModeIntrans/mrSubj are all obligatory-slot + or always-available material, so without this gate "bishiwodkal" would otherwise parse fine). + expect_fail: true + blocked_by: [mrObj] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: yidkal + note: >- + KAL + mrClass + mrModeTrans, but slot 3 (subject) is OBLIGATORY in the AffixTemplate and + omitted entirely -- the template cannot synthesize (or analyze) a completion missing an + obligatory slot, mirroring bantu-verbal's andik / suffixing-quechua's walakchik precedent + from the LEFT edge instead of the right. + expect_fail: true + blocked_by: [mrSubj] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: kal + note: >- + KAL entirely bare -- all three obligatory slots (classifier, mode, subject) are omitted at + once, the simplest possible negative control for an obligatory-slot-heavy template. + expect_fail: true + blocked_by: [mrClass] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: shiwodkal + note: >- + KAL + mrClass + mrModeIntrans ("wo-") + mrSubj, with NO object slot attempted -- proves + mrModeIntrans is independently well-formed on its own (the discontinuous gate above rejects + it only in combination with mrObj, not on its own terms). + parses: + - signature: "SBJ+IMPF+CLF+KAL|shiwodkal" + gloss: carry-CLF-INTR-SBJ + rules: [mrClass, mrModeIntrans, mrSubj] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: gabishiyidkal + note: bishiyidkal's chain plus mrAdvA ("ga-"), the first of the two outermost optional, unconstrained slots. + parses: + - signature: "ADVA+OBJ+SBJ+PRF+CLF+KAL|gabishiyidkal" + gloss: carry-CLF-TRANS.PRF-SBJ-3OBJ-ITER + rules: [mrClass, mrModeTrans, mrSubj, mrObj, mrAdvA] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: hogabishiyidkal + note: >- + The flagship, fully-loaded word: ALL SIX slots fire (classifier, mode, subject, object, advA, + advB), in the template's own canonical linear order -- advB ("ho-") outermost, advA ("ga-") + just inside it. Exercises 6 of this grammar's 7 rule ids in a single word. + parses: + - signature: "ADVB+ADVA+OBJ+SBJ+PRF+CLF+KAL|hogabishiyidkal" + gloss: carry-CLF-TRANS.PRF-SBJ-3OBJ-ITER-AREAL + rules: [mrClass, mrModeTrans, mrSubj, mrObj, mrAdvA, mrAdvB] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: gahobishiyidkal + note: >- + THE order-swap reconciliation word (see header): the textual reverse of hogabishiyidkal's two + outermost prefixes (advA's "ga-" now outermost, advB's "ho-" now just inside it). Hand-predicted + ACCEPTED on the strength of bantu-verbal's own andikilisha finding (G2c) -- WRONG: the oracle + REJECTS it (zero parses). Reconciled by tracing AnalysisAffixTemplateRule.ApplySlots: bantu- + verbal's swappable pair had nothing but optional slots between it and the root (its one + obligatory slot, finalVowel, sits outermost and is stripped first, unconditionally); this + grammar's swappable pair (advA/advB) has THREE OBLIGATORY slots (subject, mode, classifier) + between it and the root. Once advA is stripped out of its canonical turn, the very next slot + the single-pass, outer-to-inner analysis walk reaches is the obligatory subject slot -- but the + actual next characters are advB's still-unstripped "ho-", not a subject prefix, so subject's + match fails and the whole branch dies immediately (the algorithm only moves inward, it can + never revisit advB once passed). The symmetric "skip advA, try advB out of turn" branch dies + the same way one slot later. So: a fixpoint-retry rescue for an out-of-order optional pair + requires EVERY slot between that pair and the root to ALSO be optional -- an obligatory slot in + the way (this template's obligatory-innermost design, the mirror of bantu-verbal's + obligatory-outermost one) is a real, working order-enforcement mechanism for analysis too, not + just synthesis. G2c's finding does not generalize to every template shape; this is the + counter-example that shows the actual condition it depends on. + expect_fail: true + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" diff --git a/conformance/languages/suffixing-quechua/grammar.xml b/conformance/languages/suffixing-quechua/grammar.xml new file mode 100644 index 000000000..7eef9c013 --- /dev/null +++ b/conformance/languages/suffixing-quechua/grammar.xml @@ -0,0 +1,334 @@ + + + + + Chaqru + + v + + + Main + + + a + i + u + + k + l + m + n + p + s + sh + t + w + y + ch + + + + + Any + + Vowel + + + Consonant + + + + + + + + Main + + + + person1sg + + + + + + + + ni + + + + + + + + + + + + y + + + + + + + 1SG + + + + person2sg + + + + nki + + + 2SG + + + + person3sg + + + + n + + + 3SG + + + + person1plIncl + + + + tuku + + + 1PL.INCL + + + + person2pl + + + + lupa + + + 2PL + + + + plural + + + + chik + + + PL + + + + evidDirect + + + + mi + + + DIR + + + + evidReportative + + + + si + + + + shi + + + REP + + + + evidConjectural + + + + cha + + + CONJ + + + + evidAssumptive + + + + tan + + + ASSUM + + + + evidMirative + + + + was + + + MIR + + + + + verbTemplate + + person + + number + + evidential + + + + + walak + WALAK + carry.about + + + sipu + SIPU + weave + + + + tupa + TUPA + gather + + + tupa + TUPA2 + arrive + + + + + taki + kanta + + SING + sing + + + + + + + + + + + + + + + + + + + + + diff --git a/conformance/languages/suffixing-quechua/words.yaml b/conformance/languages/suffixing-quechua/words.yaml new file mode 100644 index 000000000..1e798bc05 --- /dev/null +++ b/conformance/languages/suffixing-quechua/words.yaml @@ -0,0 +1,399 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G1's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from): +# +# 1. tupay = TUPA-or-TUPA2 (root, homophonous pair) + 1SG. Root ends in vowel "a" -> mrPerson1's +# post-vowel allomorph "-y" fires -> "tupa"+"y" = "tupay". Both eTupa and eTupa2 share the exact +# shape "tupa", so BOTH are valid roots for this surface word: two distinct analyses. +# -> TUPA+1SG|tupay ; TUPA2+1SG|tupay (ambiguous, 2 parses) +# 2. walakni = WALAK + 1SG. Root ends in consonant "k" -> post-consonant allomorph "-ni" fires. +# -> WALAK+1SG|walakni +# 3. walaknichik = WALAK + 1SG("-ni") + PL("-chik"): obligatory slot 1 + optional slot 2. +# -> WALAK+1SG+PL|walaknichik [see reconciliation note below: the oracle disagreed here] +# 4. walaknkicha = WALAK + 2SG("-nki") + CONJ("-cha"): the MorphemeCoOccurrenceRule excludes +# mrEvidConjectural from co-occurring with mrPerson2 (anywhere) -> zero parses. +# -> expect_fail, blocked_by: [mrEvidConjectural] +# 5. walakchik = WALAK + PL("-chik") only, no person suffix at all: slot 1 (person) is obligatory +# and unfilled, so the AffixTemplate can never produce this output (same mechanism as +# conformance/negative/obligatory-tense-slot's "tunz") -> zero parses. +# -> expect_fail +# +# Reconciliation against the oracle (self-check): 4 of 5 matched on the first run. #3 +# (walaknichik) did NOT: the oracle produced "walakni(ch)ik", not the plain-concatenation +# "walaknichik" this hand derivation assumed. The oracle is right and the hand derivation was +# naive: HermitCrabExtensions.ToRegexString unconditionally wraps any character-definition segment +# whose string representation is more than one character (here, the "ch"/"sh" digraph segments this +# grammar defines for the plural/reportative suffixes) in its own parentheses, regardless of +# ambiguity -- see conformance/PROTOCOL.md section 3's addendum. This affected every word in the +# fixture containing a "ch" or "sh" digraph (walaknichik, walaknkishi, sipuncha, walaknkichikmi), +# corrected below to match the oracle once this was understood; every other signature in this file +# matched the hand derivation (or the same pattern, once recognized) with no further disagreement. +# +# G2e ADDENDUM (coverage-parity fix, W6): this grammar was authored in the G1 pilot before the +# provenance-carrying discipline was enforced, and its one MorphemeCoOccurrenceRule (mrEvidConjectural +# excludes mrPerson2) did not faithfully embed 3 v1 cooccurrence/* fixtures the ledger assigns to it. +# grammar.xml gained: (1) a new eTaki/SING morpheme with two free-variation allomorphs (aTaki/aKanta), +# one (aKanta) carrying an AllomorphCoOccurrenceRule excluding CONJ -- cooccurrence/allomorph-basic; +# (2) 4 new slot-1/slot-3 members (mrPerson1PlIncl/mrPerson2Pl/mrEvidAssum/mrEvidMir) each carrying one +# of the remaining 4 MorphemeCoOccurrenceRule adjacency kinds (mrEvidConjectural's rule already +# exercised "anywhere") -- cooccurrence/morpheme-adjacency; (3) a second exclude rule on +# mrEvidConjectural (excludes eTupa2, a root that never co-occurs with CONJ here, so trivially +# satisfied) -- cooccurrence/and-semantics-pin (LT-22156 AND-not-OR semantics). Hand derivations for +# the new words below, BEFORE the oracle: +# 6. takiy = SING(aTaki, "taki")+1SG. Root ends vowel "i" -> mrPerson1 post-vowel "-y". +# -> SING+1SG|takiy +# 7. kantay = SING(aKanta, "kanta")+1SG, same allomorph-selection rule, CONJ absent so aKanta's +# AllomorphCoOccurrenceRule (excludes subEvidConj) is vacuously satisfied. +# -> SING+1SG|kantay +# 8. takincha = SING(aTaki)+3SG("-n")+CONJ("-cha"): aTaki carries NO co-occurrence rule, so it +# combines with CONJ freely -- the distinguishing row proving the rule is scoped to aKanta only, +# not to the whole SING morpheme. "ch" digraph -> paren-wrapped, same as sipun(ch)a. +# -> SING+3SG+CONJ|takin(ch)a +# 9. kantancha = SING(aKanta)+3SG+CONJ: aKanta's AllomorphCoOccurrenceRule fires (subEvidConj +# present) -> zero parses. +# -> expect_fail, blocked_by: [aKanta] +# 10. walaknitan = WALAK+1SG("-ni")+ASSUM("-tan"), adjacent -> mrEvidAssum's "somewhereToLeft" +# requirement on mrPerson1 is satisfied (trivially, since adjacent is also "somewhere"). +# -> WALAK+1SG+ASSUM|walaknitan +# 11. walaknichiktan = WALAK+1SG+PL("-chik")+ASSUM: PL intervenes between PERSON and ASSUM, but +# "somewhereToLeft" only needs mrPerson1 present somewhere left, so it STILL passes. +# -> WALAK+1SG+PL+ASSUM|walakni(ch)iktan +# 12. walakniwas = WALAK+1SG+MIR("-was"), adjacent -> mrEvidMir's "adjacentToLeft" requirement on +# mrPerson1 is satisfied (immediately adjacent). +# -> WALAK+1SG+MIR|walakniwas +# 13. walaknichikwas = WALAK+1SG+PL+MIR: PL now sits directly between PERSON and MIR, so +# "adjacentToLeft" FAILS (the somewhereToLeft/adjacentToLeft distinguishing pair, vs #11) -> +# zero parses. +# -> expect_fail, blocked_by: [mrEvidMir] +# 13b. walakntan = WALAK+3SG("-n")+ASSUM: mrPerson1 (not just any person) is required, and 3SG is +# not mrPerson1, so "somewhereToLeft" fails even with no PL involved at all -- guards against +# a no-op "somewhereToLeft" that #10/#11 alone would not catch (both keep mrPerson1 present). +# -> expect_fail, blocked_by: [mrEvidAssum] +# 14. siputukumi = SIPU+1PL.INCL("-tuku")+DIR("-mi"), adjacent -> mrPerson1PlIncl's +# "somewhereToRight" requirement on mrEvidDirect is satisfied. +# -> SIPU+1PL.INCL+DIR|siputukumi +# 15. siputukuchikmi = SIPU+1PL.INCL+PL+DIR: PL intervenes between the person suffix and DIR, but +# "somewhereToRight" only needs mrEvidDirect present somewhere right, so it STILL passes. +# -> SIPU+1PL.INCL+PL+DIR|siputuku(ch)ikmi +# 15b. siputuku = SIPU+1PL.INCL alone, no evidential at all: mrEvidDirect is absent entirely, so +# "somewhereToRight" fails even with no PL involved -- guards against a no-op +# "somewhereToRight" that #14/#15 alone would not catch (both keep mrEvidDirect present). +# -> expect_fail, blocked_by: [mrPerson1PlIncl] +# 16. sipulupami = SIPU+2PL("-lupa")+DIR, adjacent -> mrPerson2Pl's "adjacentToRight" requirement +# on mrEvidDirect is satisfied (immediately adjacent). +# -> SIPU+2PL+DIR|sipulupami +# 17. sipulupachikmi = SIPU+2PL+PL+DIR: PL now sits directly between the person suffix and DIR, so +# "adjacentToRight" FAILS (distinguishing pair vs #15) -> zero parses. +# -> expect_fail, blocked_by: [mrPerson2Pl] + +language: Chaqru +inspired_by: ["Cuzco Quechua (Quechuan)"] +sources: + - "WALS - Suffixing vs. Prefixing in Inflectional Morphology" + - "Adelaar & Muysken, The Languages of the Andes (Quechuan verbal morphology, evidentiality)" + - "Aikhenvald, Evidentiality (2004)" +requires: [] +words: + - word: tupay + note: >- + Homophonous-root ambiguity: eTupa and eTupa2 share the identical shape "tupa", so this + surface word has two genuinely distinct analyses. Root-final vowel "a" selects mrPerson1's + post-vowel allomorph "-y". + parses: + - signature: "TUPA+1SG|tupay" + gloss: gather-1SG + rules: [mrPerson1] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + - signature: "TUPA2+1SG|tupay" + gloss: arrive-1SG + rules: [mrPerson1] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: sipuy + note: Root SIPU ends in vowel "u"; mrPerson1's post-vowel allomorph "-y" fires, same as tupay but with a non-homophonous root (single unambiguous parse). + parses: + - signature: "SIPU+1SG|sipuy" + gloss: weave-1SG + rules: [mrPerson1] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: walakni + note: Root WALAK ends in consonant "k"; mrPerson1's post-consonant allomorph "-ni" fires -- the environment-conditioned allomorph selection pair with sipuy/tupay, done lexically (RequiredEnvironments), not via a phonological rule. + parses: + - signature: "WALAK+1SG|walakni" + gloss: carry.about-1SG + rules: [mrPerson1] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: walaknki + note: Root WALAK + 2SG ("-nki"), the unconstrained disjunctive-slot-1 sibling of mrPerson1/mrPerson3. + parses: + - signature: "WALAK+2SG|walaknki" + gloss: carry.about-2SG + rules: [mrPerson2] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: sipun + note: Root SIPU + 3SG ("-n"), the third disjunctive slot-1 choice. + parses: + - signature: "SIPU+3SG|sipun" + gloss: weave-3SG + rules: [mrPerson3] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: walaknichik + note: Root WALAK + 1SG("-ni", obligatory slot 1) + PL("-chik", optional slot 2) -- proves an optional slot composes once the obligatory slot is satisfied. + parses: + - signature: "WALAK+1SG+PL|walakni(ch)ik" + gloss: carry.about-1SG-PL + rules: [mrPerson1, mrPlural] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: sipuymi + note: Root SIPU + 1SG("-y") + DIR evidential ("-mi", optional slot 3) -- the evidential chain's direct-evidence member, skipping the optional number slot entirely. + parses: + - signature: "SIPU+1SG+DIR|sipuymi" + gloss: weave-1SG-DIR + rules: [mrPerson1, mrEvidDirect] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: walaknkisi + note: Root WALAK + 2SG + REP evidential, allomorph A ("-si"). Pairs with walaknkishi to demonstrate free-fluctuation -- neither allomorph is rejected in favor of the other (Allomorph.FreeFluctuatesWith), they are simply two independently valid surface realizations of REP. + parses: + - signature: "WALAK+2SG+REP|walaknkisi" + gloss: carry.about-2SG-REP + rules: [mrPerson2, mrEvidReportative] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: walaknkishi + note: Root WALAK + 2SG + REP evidential, allomorph B ("-shi") -- the free-fluctuation partner of walaknkisi (see that word's note). + parses: + - signature: "WALAK+2SG+REP|walaknki(sh)i" + gloss: carry.about-2SG-REP + rules: [mrPerson2, mrEvidReportative] + exercises: + - "Disjunctive allomorphs / free-fluctuation" + + - word: sipuncha + note: >- + Root SIPU + 3SG + CONJ evidential ("-cha") -- a positive exercise of mrEvidConjectural, + unblocked because BOTH of its MorphemeCoOccurrenceRules pass: rule1 (excludes mrPerson2) is + satisfied since person is 3SG, not 2SG; rule2 (excludes eTupa2, see walaknkicha's note) is + satisfied since the root here is SIPU, not eTupa2. + parses: + - signature: "SIPU+3SG+CONJ|sipun(ch)a" + gloss: weave-3SG-CONJ + rules: [mrPerson3, mrEvidConjectural] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: takiy + note: >- + SING(aTaki, "taki")+1SG: root-final vowel "i" selects mrPerson1's post-vowel allomorph "-y", + same mechanism as sipuy/tupay. eTaki is a new morpheme with two free-variation allomorphs + (aTaki/aKanta, a native/Spanish-contact-loan doublet); this word uses the allomorph that + carries NO AllomorphCoOccurrenceRule at all. + parses: + - signature: "SING+1SG|takiy" + gloss: sing-1SG + rules: [mrPerson1] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: kantay + note: >- + SING(aKanta, "kanta")+1SG, the free-variation partner of takiy: aKanta carries an + AllomorphCoOccurrenceRule excluding co-occurrence with CONJ's subrule (subEvidConj), but CONJ + is absent here, so the rule is vacuously satisfied and the word is valid. + parses: + - signature: "SING+1SG|kantay" + gloss: sing-1SG + rules: [mrPerson1] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: takincha + note: >- + SING(aTaki)+3SG("-n")+CONJ("-cha"): aTaki carries no co-occurrence rule at all, so it combines + with CONJ freely -- the distinguishing row proving aKanta's AllomorphCoOccurrenceRule (see + kantancha below) is scoped to that one allomorph, not to the whole SING morpheme (C#'s + Allomorph.AllomorphCoOccurrenceRules lives on RootAllomorph, not Morpheme). + parses: + - signature: "SING+3SG+CONJ|takin(ch)a" + gloss: sing-3SG-CONJ + rules: [mrPerson3, mrEvidConjectural] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: kantancha + note: >- + SING(aKanta)+3SG+CONJ: aKanta's AllomorphCoOccurrenceRule (excludes subEvidConj, anywhere) + fires because CONJ is present -- zero parses. Contrast with takincha above, which uses the + SAME morpheme's sibling allomorph and succeeds: the rule is allomorph-scoped, not + morpheme-scoped. + provenance: "W6, AllomorphCoOccurrenceRule granularity" + expect_fail: true + blocked_by: [aKanta] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: walaknitan + note: >- + WALAK+1SG("-ni")+ASSUM("-tan"), adjacent: mrEvidAssum (a new evidential slot-3 member) carries + a MorphemeCoOccurrenceRule requiring mrPerson1 "somewhereToLeft" -- satisfied here since + mrPerson1 is immediately to its left. + provenance: "W6, MorphemeCoOccurrenceRule + all 5 adjacency kinds" + parses: + - signature: "WALAK+1SG+ASSUM|walaknitan" + gloss: carry.about-1SG-ASSUM + rules: [mrPerson1, mrEvidAssum] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: walaknichiktan + note: >- + WALAK+1SG+PL("-chik")+ASSUM: PL now intervenes between mrPerson1 and mrEvidAssum, but + "somewhereToLeft" only requires mrPerson1 to be present somewhere to the left, not adjacent -- + still satisfied, unlike walaknichikwas below. + parses: + - signature: "WALAK+1SG+PL+ASSUM|walakni(ch)iktan" + gloss: carry.about-1SG-PL-ASSUM + rules: [mrPerson1, mrPlural, mrEvidAssum] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: walakntan + note: >- + WALAK+3SG("-n")+ASSUM("-tan"): mrEvidAssum's MorphemeCoOccurrenceRule requires mrPerson1 + SPECIFICALLY (not just any person) somewhereToLeft. Person here is 3SG, so mrPerson1 is + absent entirely, and the "somewhereToLeft" require fails regardless of adjacency -- unlike + walaknitan/walaknichiktan above, which both keep mrPerson1 present and only vary whether PL + intervenes. Without this word, a no-op "somewhereToLeft" implementation (always satisfied) + would go undetected, since every other word exercising mrEvidAssum already has mrPerson1 + present. Zero parses. + expect_fail: true + blocked_by: [mrEvidAssum] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: walakniwas + note: >- + WALAK+1SG+MIR("-was"), adjacent: mrEvidMir (the somewhereToLeft/adjacentToLeft distinguishing + partner of mrEvidAssum) carries a MorphemeCoOccurrenceRule requiring mrPerson1 + "adjacentToLeft" -- satisfied here since mrPerson1 is immediately adjacent. + parses: + - signature: "WALAK+1SG+MIR|walakniwas" + gloss: carry.about-1SG-MIR + rules: [mrPerson1, mrEvidMir] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: walaknichikwas + note: >- + WALAK+1SG+PL+MIR: PL now sits directly between mrPerson1 and mrEvidMir, so "adjacentToLeft" + fails even though mrPerson1 is present somewhere to the left -- the distinguishing row against + walaknichiktan above (same PL-intervening configuration, but "somewhereToLeft" tolerates it + and "adjacentToLeft" does not) -- zero parses. + expect_fail: true + blocked_by: [mrEvidMir] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: siputukumi + note: >- + SIPU+1PL.INCL("-tuku")+DIR("-mi"), adjacent: mrPerson1PlIncl (a new 4th disjunctive slot-1 + person member) carries a MorphemeCoOccurrenceRule requiring mrEvidDirect "somewhereToRight" -- + satisfied here since mrEvidDirect is immediately to its right. + parses: + - signature: "SIPU+1PL.INCL+DIR|siputukumi" + gloss: weave-1PL.INCL-DIR + rules: [mrPerson1PlIncl, mrEvidDirect] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: siputukuchikmi + note: >- + SIPU+1PL.INCL+PL("-chik")+DIR: PL now intervenes between mrPerson1PlIncl and mrEvidDirect, but + "somewhereToRight" only requires mrEvidDirect to be present somewhere to the right, not + adjacent -- still satisfied, unlike sipulupachikmi below. + parses: + - signature: "SIPU+1PL.INCL+PL+DIR|siputuku(ch)ikmi" + gloss: weave-1PL.INCL-PL-DIR + rules: [mrPerson1PlIncl, mrPlural, mrEvidDirect] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: siputuku + note: >- + SIPU+1PL.INCL("-tuku") alone, no evidential at all: mrPerson1PlIncl's + MorphemeCoOccurrenceRule requires mrEvidDirect somewhereToRight, but no evidential slot is + filled here, so mrEvidDirect is absent entirely and the "somewhereToRight" require fails + regardless of adjacency -- unlike siputukumi/siputukuchikmi above, which both keep + mrEvidDirect present and only vary whether PL intervenes. Without this word, a no-op + "somewhereToRight" implementation (always satisfied) would go undetected, since every other + word exercising mrPerson1PlIncl already has mrEvidDirect present. Zero parses. + expect_fail: true + blocked_by: [mrPerson1PlIncl] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: sipulupami + note: >- + SIPU+2PL("-lupa")+DIR, adjacent: mrPerson2Pl (the somewhereToRight/adjacentToRight + distinguishing partner of mrPerson1PlIncl) carries a MorphemeCoOccurrenceRule requiring + mrEvidDirect "adjacentToRight" -- satisfied here since mrEvidDirect is immediately adjacent. + parses: + - signature: "SIPU+2PL+DIR|sipulupami" + gloss: weave-2PL-DIR + rules: [mrPerson2Pl, mrEvidDirect] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: sipulupachikmi + note: >- + SIPU+2PL+PL+DIR: PL now sits directly between mrPerson2Pl and mrEvidDirect, so + "adjacentToRight" fails even though mrEvidDirect is present somewhere to the right -- the + distinguishing row against siputukuchikmi above (same PL-intervening configuration, but + "somewhereToRight" tolerates it and "adjacentToRight" does not) -- zero parses. + expect_fail: true + blocked_by: [mrPerson2Pl] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: walaknkichikmi + note: Root WALAK + 2SG(slot1, obligatory) + PL(slot2, optional) + DIR(slot3, optional) -- the full 3-slot evidential chain filled at once, showing all three slots compose together in one word. + parses: + - signature: "WALAK+2SG+PL+DIR|walaknki(ch)ikmi" + gloss: carry.about-2SG-PL-DIR + rules: [mrPerson2, mrPlural, mrEvidDirect] + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: walakchik + note: Root WALAK + PL only, no person suffix at all. Slot 1 (person) is obligatory and unfilled, so the AffixTemplate can never produce this output at all (mirrors conformance/negative/obligatory-tense-slot's "tunz" case) -- zero parses. + expect_fail: true + exercises: + - "Affix template slots (obligatory/disjunctive/ordering)" + + - word: walaknkicha + note: >- + Root WALAK + 2SG + CONJ evidential. mrEvidConjectural carries TWO MorphemeCoOccurrenceRules: + rule1 excludes mrPerson2 (fails here, 2SG is present); rule2 excludes eTupa2 (trivially + satisfied, the root here is WALAK, not eTupa2). AND-not-OR semantics (history row 5, 90dcee64, + #311/LT-22156): the word is rejected because rule1 alone fails, regardless of rule2 passing -- + pre-90dcee64 C# used `.Any(IsWordValid)`, under which rule2's trivial pass would have wrongly + rescued this word. Zero parses under the correct post-fix semantics. + provenance: "history row 5 (90dcee64, #311/LT-22156): AND-not-OR co-occurrence semantics" + expect_fail: true + blocked_by: [mrEvidConjectural] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" diff --git a/conformance/languages/templatic-semitic/grammar.xml b/conformance/languages/templatic-semitic/grammar.xml new file mode 100644 index 000000000..b76f6bb6c --- /dev/null +++ b/conformance/languages/templatic-semitic/grammar.xml @@ -0,0 +1,491 @@ + + + + + Zemreth + + + v + vFormII + vHollow + vStemName + vSimulDemo + vIterDemo + vProthesis + vGeminate + + + + cons+- + voc+- + high+- + back+- + round+- + voice+- + + place + + velar + dental + bilabial + sibilant + nasalM + uvular + lateral + glottal + rhotic + vocalic + + + + + + + aspect + + impf + perf + juss + + + + + + + ImpfPerf + + + + + + + PerfJuss + + + + + + + + Main + + + a + + + + + + i + + + + + + u + + + + + + + k + + + + + + g + + + + + + t + + + + + + d + + + + + + b + + + + + + p + + + + + + s + + + + + + m + + + + + + q + + + + + + l + + + + + + h + + + + + + r + + + + + + + + + Any + Consonant + + + + VowelA + + + + VowelI + + + + + HighVowel + + + HFUVowel + + + + BackRnd + + + + TargetD + + + + + epenthesis + + + + + + + + + + + + + + prothesis + + + + + + + + + + + + + + + + + + feedingSimul + + + + + + + + + + + + + + + feedingIter + + + + + + + + + + + + + + + geminate + + + + + + + + + + + Main + + + + perf1sg + + + + t + + + P1SG + + + perf3sg + + + + a + + + P3SG + + + + formII + + + + + + + + + + + + + + FORMII + + + + passive + + + + + + + + + + + + + + + PASS + + + + aspImpf + + + + u + + + + IMPF + + + aspPerf + + + + i + + + + PERF + + + aspJuss + + + + a + + + + JUSS + + + + + + katab + KTB + write + + + + spr + SPR + count + + + smm + SMM + curse + + + + qal + QWL + say + + + + + halak + hilak + hulak + + HLK + go + + + + gigigi + FEEDSIM + sway + + + gigigi + FEEDITER + twirl + + + + ktub + KTUB + write.IMP + + + + rada + RAD + kindle + + + + + + + + + + diff --git a/conformance/languages/templatic-semitic/words.yaml b/conformance/languages/templatic-semitic/words.yaml new file mode 100644 index 000000000..84fa4bd51 --- /dev/null +++ b/conformance/languages/templatic-semitic/words.yaml @@ -0,0 +1,318 @@ +# HAND DERIVATION (authored BEFORE running the oracle, per +# docs/conformance-language-suite-plan.md phase G2's authoring discipline -- see grammar.xml for the +# rule definitions these derivations reason from). +# +# 1. katab -> KTB bare, no suffix at all. No C_C seam exists anywhere in "katab" (k-a-t-a-b +# alternates C/V throughout), so prEpenthesis (posV-scoped) has no site to fire at. +# -> KTB|katab, rules: [] +# 2. katabit -> KTB + P1SG ("-t"): underlying concatenation is "katab"+"t" = "katabt", whose only +# C_C seam is b-t (positions 5-6). prEpenthesis (Simultaneous, obligatory wherever its +# environment holds) inserts a linking "i" there -> "katabit". +# -> KTB+P1SG|katabit, rules: [mrPerf1sg, prEpenthesis] +# 3. kataba -> KTB + P3SG ("-a"): underlying "katab"+"a" = "kataba", vowel-final suffix creates no +# C_C seam at all (b-a is C-V) -> prEpenthesis does not fire, contrasting with katabit and +# proving the rule is seam-conditioned, not blanket-obligatory on every suffixed word. +# -> KTB+P3SG|kataba, rules: [mrPerf3sg] +# 4. sapr -> SPR + FormII: MorphologicalInput splits "spr" into fIIFirst="s", fIIRest="pr"; +# InsertSimpleContext inserts VowelA ("a") between them -> "s"+"a"+"pr" = "sapr". posFormII is +# never scoped to prEpenthesis, so the resulting p-r medial cluster is left exactly as the rule +# produces it (this invented language tolerates medial CC in FormII output; only the KTB +# person-suffix paradigm has an obligatory seam-epenthesis rule). +# -> SPR+FORMII|sapr, rules: [mrFormII] +# 5. samm -> SMM + FormII attempted: same InsertSimpleContext split ("s"+"a"+"mm" = "samm"), but +# SMM's C2=C3 ("m"="m") is exactly the OCP-fragile shape the MorphemeCoOccurrenceRule excludes +# FormII from co-occurring with (primaryMorpheme="mrFormII", otherMorphemes="eSmm") -> zero +# parses, since "samm" has no other possible analysis (SMM's own bare shape is "smm", not +# "samm"). +# -> expect_fail, blocked_by: [mrFormII] +# +# The remaining words reuse these same five mechanisms (KTB's person-suffix/epenthesis pair, FormII's +# InsertSimpleContext, the OCP exclusion) on independent lexical/rule material -- the hollow-root +# internal passive (ModifyFromInput), the HLK stem-name paradigm (StemName region algebra, mirroring +# fusional-latin's own mun/man/min proof exactly), the Simultaneous-vs-Iterative feeding/bleeding +# contrast pair, word-initial prothesis, and gemination-as-expansion -- or are expect_fail words whose +# signature is the empty set. See PROTOCOL.md section 3 for the `-`/empty-signature convention. +# +# Reconciliation against the oracle (self-check): all 5 words above matched the hand derivation on +# the first self-check run once the grammar's isolation discipline (one POS per phonological-rule +# demo, requiredPartsOfSpeech on every subrule) was in place, and 23 of 25 words in the file matched +# outright with no further disagreement. The remaining 2 (gigugu, gigugi) matched on SIGNATURE but +# not on the traced rules: list -- both words' fired-rule set traced as [prSimulFeeding, +# prIterFeedingControl] together, not just the one whose candidate actually survives. This is +# TraceRuleAttributor's own documented "ambiguous word" limitation (see that class's doc comment in +# src/SIL.Machine.Morphology.HermitCrab.Conformance/V2/TraceRuleAttributor.cs): surface "gigugu"/ +# "gigugi" both unify against BOTH the eFeedSimul and eFeedIter lexical candidates (they share the +# identical underlying shape "gigigi"), and even though only one candidate's synthesis-confirm +# ultimately matches the target surface, the OTHER candidate's own (failed) confirm attempt still +# fires its rule before being discarded, and the word-level trace walk unions rules fired across +# every attempted candidate, not just the surviving one. The trace is authoritative per this task's +# discipline, so rules: was widened to the honest traced union on both words (see their own notes). + +language: Zemreth +inspired_by: ["Arabic (Semitic)", "Amharic (Ethiosemitic)"] +sources: + - "WALS - Reduplication and templatic morphology typology chapters" + - "McCarthy, Formal Problems in Semitic Phonology and Morphology (1985) - root-and-pattern, OCP" + - "Rose & Walker (eds.), Amharic verbal morphology and the HermitCrab reference corpus" +requires: [phonology] +words: + - word: katab + note: KTB bare root, no suffix -- no C_C seam exists, so prEpenthesis has no site to fire at. + parses: + - signature: "KTB|katab" + gloss: write + rules: [] + exercises: [] + + - word: katabit + note: >- + KTB + P1SG ("-t"): underlying "katab"+"t" = "katabt" has one C_C seam (b-t), obligatorily + broken by Simultaneous-mode epenthesis -- the raw unepenthesized concatenation never itself + surfaces (see "katabt" below). + provenance: "P13 synthetic oracle: Simultaneous epenthesis; confirmed live-oracle bug on row 0 (see README)" + parses: + - signature: "KTB+P1SG|katabit" + gloss: write-1SG.PERF + rules: [mrPerf1sg, prEpenthesis] + exercises: + - "RewriteRule Simultaneous" + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + + - word: katabt + note: The raw, unepenthesized concatenation of katab+"-t" -- prEpenthesis is obligatory wherever its C_C environment holds, so this surface form can never itself be a valid analysis (see katabit). + expect_fail: true + blocked_by: [prEpenthesis] + exercises: + - "RewriteRule Simultaneous" + + - word: kataba + note: KTB + P3SG ("-a"), a vowel-initial suffix -- no C_C seam forms (b-a is C-V), so epenthesis correctly does not fire, contrasting with katabit. + parses: + - signature: "KTB+P3SG|kataba" + gloss: write-3SG.PERF + rules: [mrPerf3sg] + exercises: + - "MorphologicalOutputAction: CopyFromInput/InsertSegments" + + - word: spr + note: SPR bare root (posFormII), no FormII applied -- a plain, unrestricted control. + parses: + - signature: "SPR|spr" + gloss: count + rules: [] + exercises: [] + + - word: sapr + note: >- + SPR + FormII: InsertSimpleContext splits the root into a first segment and the remainder, + inserting a fully-specified VowelA class between them -- "s"+"a"+"pr" = "sapr". This is + ZERO-coverage-today's home construct: no other v2 grammar carries InsertSimpleContext. + posFormII is never scoped to prEpenthesis, so the resulting p-r cluster is left as-is (an + invented-language phonotactic difference from the KTB paradigm, not an omission). + parses: + - signature: "SPR+FORMII|sapr" + gloss: count-FORMII + rules: [mrFormII] + exercises: + - "MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext" + + - word: smm + note: SMM bare root (posFormII), no FormII applied -- proves the root is otherwise perfectly valid on its own; only combining it with FormII is excluded (see samm below). + parses: + - signature: "SMM|smm" + gloss: curse + rules: [] + exercises: [] + + - word: samm + note: >- + SMM + FormII attempted: the same InsertSimpleContext split as sapr ("s"+"a"+"mm" = "samm"), + but SMM's C2=C3 ("m"="m") is exactly the Obligatory-Contour-Principle-fragile root shape the + MorphemeCoOccurrenceRule excludes FormII from co-occurring with -- zero parses (no other + analysis exists for "samm"). + expect_fail: true + blocked_by: [mrFormII] + exercises: + - "MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule" + + - word: qal + note: QWL ("say") hollow root, bare, active/citation form -- Arabic qala-plausible. + parses: + - signature: "QWL|qal" + gloss: say + rules: [] + exercises: [] + + - word: qil + note: >- + QWL's internal passive, realized ENTIRELY by changing the root's own medial vowel ("a"->"i") + via ModifyFromInput -- no affix at all. Arabic qila ("it was said")-plausible: a genuinely + attested Semitic phenomenon, not an invented mechanism. Second half of the zero-coverage + ModifyFromInput/InsertSimpleContext construct pair (paired with sapr's InsertSimpleContext use + above). + parses: + - signature: "QWL+PASS|qil" + gloss: say-PASS + rules: [mrPassive] + exercises: + - "MorphologicalOutputAction: ModifyFromInput/InsertSimpleContext" + + - word: halak + note: HLK's default (unrestricted) allomorph, bare -- no aspect feature assigned at all, so there is no StemName region to fail. + parses: + - signature: "HLK|halak" + gloss: go + rules: [] + exercises: + - "Stem names" + + - word: halaku + note: HLK default allomorph + IMPF ("-u") -- once ANY aspect feature is assigned, snImpfPerf/snPerfJuss's regions jointly exhaust {impf,perf,juss}, so the region-less default allomorph can never combine with an aspect suffix at all. + expect_fail: true + exercises: + - "Stem names" + + - word: hilaku + note: HLK's snImpfPerf-restricted allomorph "hilak" + IMPF ("-u") -- impf is in snImpfPerf's region {impf,perf}. + parses: + - signature: "HLK+IMPF|hilaku" + gloss: go-IMPF + rules: [mrAspImpf] + exercises: + - "Stem names" + + - word: hilaka + note: HLK's snImpfPerf-restricted allomorph "hilak" + JUSS ("-a") -- juss is NOT in snImpfPerf's region {impf,perf} -- zero parses. + expect_fail: true + exercises: + - "Stem names" + + - word: hilaki + note: >- + HLK's snImpfPerf-restricted allomorph "hilak" + PERF ("-i") -- perf is in snImpfPerf's region + {impf,perf} AND also in snPerfJuss's region {perf,juss}: the shared-region row, proving the + 1st-person-equivalent overlap between the two stem names doesn't spuriously block either one + (paired with hulaki below). + provenance: "W5, StemName selection + default-stem fallback" + parses: + - signature: "HLK+PERF|hilaki" + gloss: go-PERF + rules: [mrAspPerf] + exercises: + - "Stem names" + + - word: hulaka + note: HLK's snPerfJuss-restricted allomorph "hulak" + JUSS ("-a") -- juss is in snPerfJuss's region {perf,juss}. + parses: + - signature: "HLK+JUSS|hulaka" + gloss: go-JUSS + rules: [mrAspJuss] + exercises: + - "Stem names" + + - word: hulaku + note: HLK's snPerfJuss-restricted allomorph "hulak" + IMPF ("-u") -- impf is NOT in snPerfJuss's region {perf,juss} -- zero parses. + expect_fail: true + exercises: + - "Stem names" + + - word: hulaki + note: HLK's snPerfJuss-restricted allomorph "hulak" + PERF ("-i") -- the shared-region partner of hilaki above (perf is in BOTH regions), and both allomorphs must independently validate. + parses: + - signature: "HLK+PERF|hulaki" + gloss: go-PERF + rules: [mrAspPerf] + exercises: + - "Stem names" + + - word: gigugu + note: >- + FEEDSIM ("gigigi", posSimulDemo) under prSimulFeeding (Simultaneous): both candidate target + positions (3 and 5) have their environment checked against the SAME pre-rewrite snapshot, so + BOTH rewrite -> "gigugu". Simultaneous can never feed/bleed another match in the same pass. + Reconciliation finding: the traced rules: set for BOTH gigugu and gigugi includes both + prSimulFeeding AND prIterFeedingControl, not just the one whose candidate survives -- this is + TraceRuleAttributor's own documented "ambiguous word" limitation (see that class's doc + comment): the eFeedIter/eFeedSimul candidates are BOTH attempted during analysis (surface + "gigugu" unifies against both lexical entries' shared underlying shape "gigigi"), and even + though only the FEEDSIM candidate's synthesis-confirm ultimately matches "gigugu", the OTHER + candidate's own confirm attempt (which fires its own rule before failing to match the target + surface) is traced too -- the word-level trace walk unions rules fired across every attempted + candidate, not just the winning one. Declared rules: below is the honest traced union, per the + task brief's discipline ("the TRACE is authoritative"). + provenance: "P13 synthetic oracle, RewriteRuleTests.MultipleApplicationRules: Simultaneous blocks forward feeding" + parses: + - signature: "FEEDSIM|gigugu" + gloss: sway + rules: [prSimulFeeding, prIterFeedingControl] + exercises: + - "RewriteRule Simultaneous" + + - word: gigugi + note: >- + FEEDITER ("gigigi", posIterDemo) under prIterFeedingControl (Iterative, the default): position + 3 rewrites first (against the original shape), but by the time position 5 is checked its own + environment now reads position 3's ALREADY-rewritten value ("u", not the required front "i"), + so position 5 is NOT rewritten -> "gigugi". The mirror image of gigugu's all-at-once result. + Same traced-union finding as gigugu applies here (see that word's note) -- the FEEDSIM + candidate's own failed confirm attempt also traces prSimulFeeding. + provenance: "Iterative control sibling for rewrite/simultaneous-feeding" + parses: + - signature: "FEEDITER|gigugi" + gloss: twirl + rules: [prIterFeedingControl, prSimulFeeding] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: gigigi + note: >- + Bare "gigigi" is ambiguous between the FEEDSIM and FEEDITER lexical candidates, but each + root's own rule is obligatory wherever its environment holds -- FEEDSIM's candidate + synthesis-confirms to "gigugu" (not "gigigi"), and FEEDITER's candidate synthesis-confirms to + "gigugi" (not "gigigi") -- so neither candidate survives. Zero parses in both modes, exactly + mirroring v1's own bare-root negative control. + expect_fail: true + blocked_by: [prSimulFeeding, prIterFeedingControl] + exercises: + - "RewriteRule Simultaneous" + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: iktub + note: >- + KTUB ("write.IMP", posProthesis) bare root "ktub" begins with a word-initial 2-consonant + cluster ("kt") -- prProthesis obligatorily breaks it with a prothetic "i", Arabic hamzat + al-wasl-plausible (cf. Form I imperative "uktub"). + provenance: "P1, RewriteRuleTests.BoundaryRules (bare-root word-initial epenthesis)" + parses: + - signature: "KTUB|iktub" + gloss: write.IMP + rules: [prProthesis] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: ktub + note: The raw, un-prothesized underlying shape -- prProthesis is obligatory whenever a word begins with a consonant cluster, so this surface form can never itself be a valid analysis. + expect_fail: true + blocked_by: [prProthesis] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: radda + note: >- + RAD ("kindle", posGeminate) bare root "rada" -- prGeminate unconditionally expands its single + "d" into two identical segments (1 segment -> 2 segments, no Environment at all), modeling + gemination as spreading. The raw unexpanded "rada" can never itself surface (see rada below). + provenance: "W9.2 probe, RewriteRuleTests.ExpandRules" + parses: + - signature: "RAD|radda" + gloss: kindle + rules: [prGeminate] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + + - word: rada + note: The raw, un-geminated underlying shape -- prGeminate is unconditional (obligatory wherever "d" appears), so this surface form can never itself be a valid analysis. + expect_fail: true + blocked_by: [prGeminate] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" diff --git a/conformance/parity-check.py b/conformance/parity-check.py new file mode 100644 index 000000000..528d00ddc --- /dev/null +++ b/conformance/parity-check.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +Conformance v1->v2 migration parity proof (docs/conformance-language-suite-plan.md section 5, gate G2e). + +Mechanically verifies the migration floors that guard the ONE irreversible step -- deletion of the v1 +fixture tree -- plus an absolute v2 construct-coverage check that outlives the deletion (gate G4). +Exits NONZERO on any violation, so it can gate CI and the delete commit. + +Checks (ledger "Floor summary (G4 parity check keys)"): + A. Provenance -- every v1 manifest.json 'provenance' string appears VERBATIM (exact value, not + substring) in some v2 words.yaml 'provenance:' field. One-way v1 -> v2. + B. Per-construct -- every construct named in any v1 manifest is exercised by >=1 v2 word (coverage.csv). + C. XAmple floor -- every construct covered by a requires:[] v1 fixture is exercised by >=1 word in a + requires:[] v2 grammar (quechua/latin/athabaskan or a requires:[] edge-case). + D. Absolute -- every constructs.txt construct except 'Tracing (TraceType)' is exercised by >=1 + v2 word. Independent of v1 tooling, so it still proves G4 after the delete. + +Once the v1 tree is deleted, A/B/C find zero v1 fixtures and report "migration complete"; only D runs. + +Usage: python conformance/parity-check.py [conformance_root] (default: this script's directory) +""" +import csv +import json +import os +import re +import sys + +OUT_OF_SCOPE = "Tracing (TraceType)" +EXPECTED_V1_COUNT = 41 # the frozen v1 fixture set (docs/conformance-migration-ledger.md) + + +def die(msg): + print(f"parity-check: FATAL: {msg}", file=sys.stderr) + sys.exit(2) + + +def load_constructs_txt(path): + out = [] + with open(path, encoding="utf-8") as f: + for raw in f: + line = raw.strip() + if line and not line.startswith("#"): + out.append(line) + return out + + +def discover_v1(root): + """v1 fixture = a directory with manifest.json that is NOT under languages/ or edge-cases/ (those + are v2, keyed by words.yaml). Returns list of parsed manifests.""" + manifests = [] + for dirpath, _dirs, files in os.walk(root): + norm = dirpath.replace("\\", "/") + if "/languages/" in norm + "/" or "/edge-cases/" in norm + "/": + continue + if "manifest.json" in files: + with open(os.path.join(dirpath, "manifest.json"), encoding="utf-8") as f: + manifests.append(json.load(f)) + return manifests + + +def load_coverage(path): + """coverage.csv -> (set of all covered constructs, dict language -> set of constructs).""" + covered = set() + by_lang = {} + with open(path, encoding="utf-8", newline="") as f: + reader = csv.DictReader(f) + rows = 0 + for row in reader: + rows += 1 + construct = row["construct"] + lang = row["language"] + if construct: + covered.add(construct) + by_lang.setdefault(lang, set()).add(construct) + if rows == 0: + die(f"{path} has no data rows -- refusing to prove parity against an empty coverage table") + return covered, by_lang + + +PROV_RE = re.compile(r'^\s*provenance:\s*(.*\S)\s*$') +LANG_RE = re.compile(r'^language:\s*(.*\S)\s*$') +REQ_RE = re.compile(r'^requires:\s*\[(.*)\]\s*$') + + +def _strip_quotes(s): + if len(s) >= 2 and s[0] in "\"'" and s[-1] == s[0]: + return s[1:-1] + return s + + +def load_v2_words(langs_dir, edge_dir): + """Scan every v2 words.yaml. Returns (set of provenance strings, dict language -> requires-list).""" + provenance = set() + lang_requires = {} + for base in (langs_dir, edge_dir): + if not os.path.isdir(base): + continue + for name in sorted(os.listdir(base)): + wy = os.path.join(base, name, "words.yaml") + if not os.path.isfile(wy): + continue + lang = None + requires = None + with open(wy, encoding="utf-8") as f: + for raw in f: + # skip full-line comments so a '#'-commented example provenance never counts + if raw.lstrip().startswith("#"): + continue + m = PROV_RE.match(raw) + if m: + provenance.add(_strip_quotes(m.group(1))) + continue + if lang is None: + lm = LANG_RE.match(raw) + if lm: + lang = _strip_quotes(lm.group(1)) + if requires is None: + rm = REQ_RE.match(raw) + if rm: + inner = rm.group(1).strip() + requires = [t.strip() for t in inner.split(",") if t.strip()] + if lang is None: + die(f"{wy} has no 'language:' field") + if requires is None: + die(f"{wy} has no 'requires:' field") + lang_requires[lang] = requires + return provenance, lang_requires + + +def main(): + root = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) + root = os.path.abspath(root) + coverage_csv = os.path.join(root, "coverage.csv") + constructs_txt = os.path.join(root, "constructs.txt") + langs_dir = os.path.join(root, "languages") + edge_dir = os.path.join(root, "edge-cases") + + for p in (coverage_csv, constructs_txt): + if not os.path.isfile(p): + die(f"missing required input: {p}") + + checklist = load_constructs_txt(constructs_txt) + v2_covered, v2_by_lang = load_coverage(coverage_csv) + v2_provenance, v2_lang_requires = load_v2_words(langs_dir, edge_dir) + v1 = discover_v1(root) + + failures = [] # list of (check, message) + print(f"parity-check: root={root}") + print(f"parity-check: discovered {len(v1)} v1 fixture(s), " + f"{len(v2_by_lang)} v2 grammar(s) in coverage.csv, " + f"{len(v2_provenance)} v2 provenance string(s), {len(checklist)} constructs.txt entries") + + v1_present = len(v1) > 0 + if v1_present and len(v1) != EXPECTED_V1_COUNT: + die(f"expected exactly {EXPECTED_V1_COUNT} v1 fixtures (the frozen set) or 0 (deleted); " + f"found {len(v1)} -- discovery bug or unexpected tree state, refusing to certify") + + # ---- A/B/C: v1 -> v2 floors (only while v1 exists) ---- + if not v1_present: + print("parity-check: no v1 fixtures found -- migration complete; running only absolute check D.") + else: + v1_constructs = set() + v1_provenance = set() + xample_constructs = set() # constructs of requires:[] v1 fixtures + for m in v1: + cons = set(m.get("constructs", [])) + v1_constructs |= cons + if "provenance" in m and m["provenance"]: + v1_provenance.add(m["provenance"]) + if m.get("requires", []) == []: + xample_constructs |= cons + if not v1_constructs: + die("parsed 0 constructs from v1 manifests -- schema/parse bug, refusing to certify") + + # A. provenance exact-value containment + missing_prov = sorted(p for p in v1_provenance if p not in v2_provenance) + if missing_prov: + for p in missing_prov: + failures.append(("A/provenance", f"v1 provenance not found verbatim in any v2 words.yaml: {p!r}")) + print(f"[{'PASS' if not missing_prov else 'FAIL'}] A. provenance: " + f"{len(v1_provenance) - len(missing_prov)}/{len(v1_provenance)} v1 strings carried verbatim") + + # B. per-construct floor + missing_cons = sorted(c for c in v1_constructs if c not in v2_covered) + if missing_cons: + for c in missing_cons: + failures.append(("B/per-construct", f"v1 construct not covered by any v2 word: {c!r}")) + print(f"[{'PASS' if not missing_cons else 'FAIL'}] B. per-construct floor: " + f"{len(v1_constructs) - len(missing_cons)}/{len(v1_constructs)} v1 constructs covered in v2") + + # C. XAmple floor: xample constructs must be covered by a requires:[] v2 grammar + xample_v2_langs = {lang for lang, req in v2_lang_requires.items() if req == []} + xample_v2_covered = set() + for lang in xample_v2_langs: + xample_v2_covered |= v2_by_lang.get(lang, set()) + missing_xample = sorted(c for c in xample_constructs if c not in xample_v2_covered) + if missing_xample: + for c in missing_xample: + failures.append(("C/xample", f"requires:[] v1 construct has no requires:[] v2 home: {c!r}")) + print(f"[{'PASS' if not missing_xample else 'FAIL'}] C. XAmple floor: " + f"{len(xample_constructs) - len(missing_xample)}/{len(xample_constructs)} requires:[] constructs " + f"have a requires:[] v2 home (v2 requires:[] grammars: {sorted(xample_v2_langs)})") + + # ---- D: absolute construct coverage (always) ---- + in_scope = [c for c in checklist if c != OUT_OF_SCOPE] + uncovered = sorted(c for c in in_scope if c not in v2_covered) + if uncovered: + for c in uncovered: + failures.append(("D/absolute", f"constructs.txt construct at zero v2 coverage: {c!r}")) + print(f"[{'PASS' if not uncovered else 'FAIL'}] D. absolute coverage: " + f"{len(in_scope) - len(uncovered)}/{len(in_scope)} in-scope constructs covered " + f"('{OUT_OF_SCOPE}' out of scope by design)") + + print() + if failures: + print(f"parity-check: FAILED with {len(failures)} violation(s):") + for check, msg in failures: + print(f" [{check}] {msg}") + sys.exit(1) + print("parity-check: ALL CHECKS PASSED -- v1 coverage is fully preserved in v2; safe to delete the v1 tree.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/conformance/rules.csv b/conformance/rules.csv new file mode 100644 index 000000000..4a82d200e --- /dev/null +++ b/conformance/rules.csv @@ -0,0 +1,127 @@ +language,rule_id,words +PathologicalDeepOptionalAffixNesting,mrP1,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP10,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP11,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP12,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP2,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP3,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP4,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP5,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP6,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP7,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP8,xxxxxxk;xxxxxxxxxxxxk +PathologicalDeepOptionalAffixNesting,mrP9,xxxxxxk;xxxxxxxxxxxxk +DisjunctiveRecheckProbe,ruleD,pakza;!pakda;pitda +DisjunctiveRecheckProbe,ruleT,!wakta;wokta +LatinDiacriticProbe,rDim,caféta;kelîta;!añota;!gölta +LatinDiacriticProbe,rPl,años;cafés;göls;kelîs +N2-DefaultSymbol,pr1,!bdt +IterativeEpenthesisCascadeProbe,prule4, +StrRepIdentityProbe,ruleObj,ndpat;!ndmat;imat;!ipat +StrRepIdentityProbe,rulePfx,pat;mat;mupat;mumat;!mwpat;mwuta;!muuta;ndpat;imat +TruncateRulesProbe,mruleTruncLead,ag;as;gas +TruncateRulesProbe,mruleTruncOptIns,gbubibi;gas +TruncateRulesProbe,mruleTruncTrail,sa +Kirenti,mrAlphaSuf,satun;setin +Kirenti,mrPast,semitide;semitideler;kutagida;unitide +Kirenti,mrPlural,semitideler +Kirenti,mrPres,semitile;kutagila +Kirenti,prAlphaHighHarmony,satun;setin +Kirenti,prBacknessHarmony,kutagida;kutagila;!untda +Kirenti,prEpenthesis,semitide;semitile;semitideler;kutagida;kutagila;unitide +Kirenti,prGradation,kutagida;kutagila +Kirenti,prVowelMerge,!duii;duy +Kirenti,prVowelMultiMerge,!sueeb;sueb +Kirenti,ruleCollective,!etemik;!esalik +Kirenti,ruleInfixDemo,guan +Tavolan,mrCircum,keadilan +Tavolan,mrComplexMeta,mu+i +Tavolan,mrISuffix,pui +Tavolan,mrInfixUm,sumulat +Tavolan,mrLnk,bahain +Tavolan,mrPerf,katibɯd +Tavolan,mrRedupCV,tutula +Tavolan,mrRedupFull,tulatula +Tavolan,mrSimpleMeta,nui;!niu +Tavolan,mrTrunc,pur +Tavolan,mrUSuffix,mu+i +Tavolan,prNonContig,katibɯd;!katabɯd +Velamba,mrAgr,dabepez;dabez;!kesez +Velamba,mrAppl,andikila;andikishila;andikilisha +Velamba,mrCaus,andikisha;andikishila;andikilisha +Velamba,mrEndH,belh +Velamba,mrEndW,kibw;!belw +Velamba,mrEndZ,xkibz;!yxkibz +Velamba,mrExtX,xkib;yxkib;xkibz +Velamba,mrExtY,yxkib +Velamba,mrFV,andika;andikisha;andikila;andikishila;andikilisha +Velamba,mrNumPl,dabep;dabepez +Velamba,mrRecurSuf,siliz;!siliziz +Velamba,mrRedup,kimbiakimbia +Velamba,mrStemReq,daleh;!daheh +Velamba,mrTense,limape +Velamba,prNasalMutation,mba;!mpa +Velamba,rrPast,limaile;!limapeile +Velamba,rrRRealTest,!zimed;!zamed +Vetruna,mrAblaut,sang +Vetruna,mrCircumfixGeT,gelobt;gelobth +Vetruna,mrCompoundConstrained,mitlav;ficlav;!seclav;!genlav;!mitlavlav +Vetruna,mrCompoundHN,pannox;!panbenox +Vetruna,mrCompoundNH,lexdom;lexbedom +Vetruna,mrEndH,sodh +Vetruna,mrEndW,katw;!sodw +Vetruna,mrEndZ,xpedz;!yxpedz +Vetruna,mrNasalInfix,vinc +Vetruna,mrPast2,!ducit +Vetruna,mrPers1,mano;mino +Vetruna,mrPers2,mans +Vetruna,mrPers3,mint +Vetruna,mrPl2,ducunt +Vetruna,mrPrefixBe1,benox +Vetruna,mrPrefixBe2,lexbedom +Vetruna,mrSuffixS3,lobh;gelobth +Vetruna,mrTense,feres +Vetruna,mrThemeX,xped;yxped;xpedz +Vetruna,mrThemeY,yxped +Vetruna,rrPast,ferid;!feresid +Nuvattak,mrCompoundHN,akutat;!akupitat +Nuvattak,mrCompoundNH,silanuk;silamanuk +Nuvattak,mrDeriv,nunaliq;nunaliqvuq +Nuvattak,mrInfl,nunaliqvuq;!nunavuq;takuvuq +Nuvattak,mrPrefixMa2,silamanuk +Nuvattak,mrPrefixPi1,pitat +Nuvattak,prDelReins,bubu;!buibu +Nuvattak,prMDC1,buuubuuu;!buiibuii +Nuvattak,prMDC2,mau;!matu +Nuvattak,prMultiSeg,kuuukuuu;!kuiikuii +Ketselan,mrAdvA,gabishiyidkal;hogabishiyidkal +Ketselan,mrAdvB,hogabishiyidkal +Ketselan,mrClass,shiyidkal;niyidtan;bishiyidkal;!kal;shiwodkal;gabishiyidkal;hogabishiyidkal +Ketselan,mrModeIntrans,shiwodkal +Ketselan,mrModeTrans,shiyidkal;niyidtan;bishiyidkal;gabishiyidkal;hogabishiyidkal +Ketselan,mrObj,bishiyidkal;!bishiwodkal;gabishiyidkal;hogabishiyidkal +Ketselan,mrSubj,shiyidkal;niyidtan;bishiyidkal;!yidkal;shiwodkal;gabishiyidkal;hogabishiyidkal +Chaqru,aKanta,!kantancha +Chaqru,mrEvidAssum,walaknitan;walaknichiktan;!walakntan +Chaqru,mrEvidConjectural,sipuncha;takincha;!walaknkicha +Chaqru,mrEvidDirect,sipuymi;siputukumi;siputukuchikmi;sipulupami;walaknkichikmi +Chaqru,mrEvidMir,walakniwas;!walaknichikwas +Chaqru,mrEvidReportative,walaknkisi;walaknkishi +Chaqru,mrPerson1,tupay;sipuy;walakni;walaknichik;sipuymi;takiy;kantay;walaknitan;walaknichiktan;walakniwas +Chaqru,mrPerson1PlIncl,siputukumi;siputukuchikmi;!siputuku +Chaqru,mrPerson2,walaknki;walaknkisi;walaknkishi;walaknkichikmi +Chaqru,mrPerson2Pl,sipulupami;!sipulupachikmi +Chaqru,mrPerson3,sipun;sipuncha;takincha +Chaqru,mrPlural,walaknichik;walaknichiktan;siputukuchikmi;walaknkichikmi +Zemreth,mrAspImpf,hilaku +Zemreth,mrAspJuss,hulaka +Zemreth,mrAspPerf,hilaki;hulaki +Zemreth,mrFormII,sapr;!samm +Zemreth,mrPassive,qil +Zemreth,mrPerf1sg,katabit +Zemreth,mrPerf3sg,kataba +Zemreth,prEpenthesis,katabit;!katabt +Zemreth,prGeminate,radda;!rada +Zemreth,prIterFeedingControl,gigugu;gigugi;!gigigi +Zemreth,prProthesis,iktub;!ktub +Zemreth,prSimulFeeding,gigugu;gigugi;!gigigi diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 000000000..781a0d3c3 --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,7 @@ +# Archive + +Historical v1 conformance-suite docs, kept for provenance (not rewritten to match v2): the original +fixture-per-mechanism design plan (`conformance-framework-plan.md`), its implementation scoping note +(`conformance-framework-implementation-notes.md`), and the history-mined regression matrix +(`conformance-HISTORY-MATRIX.md`). Superseded by `docs/conformance-language-suite-plan.md` (current +design) and `docs/conformance-migration-ledger.md` (the v1->v2 provenance bridge). diff --git a/docs/archive/conformance-HISTORY-MATRIX.md b/docs/archive/conformance-HISTORY-MATRIX.md new file mode 100644 index 000000000..a2d593f82 --- /dev/null +++ b/docs/archive/conformance-HISTORY-MATRIX.md @@ -0,0 +1,203 @@ +# History-mined regression matrix (Phase 2, W12 Feeder Path B) + +Every commit on `master` touching `src/SIL.Machine.Morphology.HermitCrab` (74 as of 2026-07-08, +verified via `git log master --oneline -- src/SIL.Machine.Morphology.HermitCrab`), walked +individually: commit diff read in full (not just the message), PR body pulled via `gh pr view + --repo sillsdev/machine` where a `(#NNN)` ref exists, JIRA `LT-NNNNN` key captured where +present. `gh` auth worked throughout (public_repo scope sufficient for `sillsdev/machine`); no +fallback to commit-message-only was needed for any of the 10 numbered-PR commits. + +## Verdict taxonomy (binding, per `rust/docs/phase2-completed/evidence-w12.md` §Feeder path B) + +- **covered** — an existing Rust test/fixture already pins the exact behavior the commit changed. + Cited by file + test name; gesturing at "the corpora pass" is **not** sufficient — the cited + test must exercise the specific mechanism (e.g. the specific C# method/line the commit touched). +- **needs-fixture** — the behavior is unported, under-tested, or only coincidentally not diverging + today. Becomes a Path-A-format fixture (`grammar.xml` + `words.txt` + oracle `expected.tsv`). + These rank **first** in the phase-2 fixture backlog — each one is a behavior someone historically + got wrong, so by construction it is a good adversarial test of the Rust port. +- **N/A-mechanical** — allowed **only** for: null-ref/dispose/GC, thread-safety of shared mutable + state, or .NET-version/API/tooling churn (target-framework bumps, NuGet/csproj edits, CSharpier/ + editorconfig reformatting, mechanical type renames with a verified zero-diff-under-tokens diff). + **Strict rule:** a behavioral fix is *never* N/A-mechanical, even a small one. Every N/A row below + was verified by reading the literal diff (not inferred from the commit message) — the 20 + "Fix formatting" commits were confirmed token-for-token identical modulo whitespace/brace + placement; csproj/target-framework commits were confirmed to touch only build metadata or + preprocessor symbols. +- **superseded** — a later commit replaced the behavior; the later commit carries its own row and + verdict, and this row chains to it by sha. + +Two deliberate scope notes visible in the table: +1. The **root-guesser subsystem** (`Morpher.LexicalGuess`/`GuessRoot`/`MaxUnapplications`/ + `MatchNodesWithPattern`) has **zero** Rust surface (`grep` for `GuessRoot|LexicalGuess|Guessed` + under `rust/` returns nothing). This is not a silent gap — `rust-optimizations-phase2.md` + §Non-goals lists the Guesser API as "absent, on no roadmap — needs an explicit yes/no." Every + commit that only touches this subsystem is marked **needs-fixture** (spec-first capture, in case + the API is greenlit) but flagged `[guesser, non-goal]` so it can be triaged separately from the + Tier-1/W-numbered gaps. +2. The two 2016 **"Merge remote-tracking branch 'hc/master' into Translation"** commits and the + initial **"Reorganized folders"** commit are the bulk import of the original HermitCrab.NET + codebase into this repo — not isolable behavioral regressions. Per plan guidance they are marked + **covered** (the surviving behavior is exercised broadly by the three reference corpora and the + `hc-rules`/`hc-parse` test suites; any specific piece of that substrate that was *later* modified + gets its own row with its own verdict elsewhere in this table). + +## The matrix + +| # | SHA | Date | PR | LT key | Behavior (from the actual diff) | Verdict | Evidence / backlog note | +|---|-----|------|----|--------|----------------------------------|---------|--------------------------| +| 1 | `812aa48e` | 2026-04-10 | #403 | LT-22480 | `NarrowSynthesisRewriteSubruleSpec.Apply`: merge-rule (2→1) node deletion looped by stale positional index (`_targetCount`), not skipping nodes already deleted by a prior application in the same shape — fixed to skip `IsDeleted()` nodes. | **covered** (was needs-fixture; closed P9/W12, 2026-07-10) | Fixture `rust/conformance/rewrite/merge/` (2-seg→1-seg merge) and `rust/conformance/rewrite/multiplemerge/` (3-seg→2-seg merge) were frozen at plan-authoring time as DIVERGES (Rust failed to reverse the merge on analysis). Re-run against current HEAD (`7f46f2ad`) during this audit: **both now MATCH the oracle byte-for-byte**, including the previously-failing rows (`butbut`, `bttbtt`). Each grammar has exactly one lexical entry and zero morphological rules (pure phonology), so a surface accept is unambiguous evidence of a correct analysis-side merge reversal, not a spurious match. Root cause of the fix not bisected to one commit; P10's `GetSkippedOptionalNodes` fold (`63b0a89f`, same session) is the leading circumstantial candidate. Now wired as `merge_matches_oracle`/`multiplemerge_matches_oracle` in `hc-parse/tests/rewrite_conformance.rs`. See both fixtures' README "Re-verified 2026-07-10" sections. | +| 2 | `e755c509` | 2026-02-14 | #384 | — | Update to .NET 10.0 / CSharpier 1.2.6 / NUnit 4.4; dependency + tooling bump across the assembly. | **N/A-mechanical** | .NET-version/tooling churn (explicitly allowed category). Diff on `AnalysisRewriteRule.cs` confirmed as pure reformatting (trailing commas, blank-line removal) — no token change. | +| 3 | `f4589887` | 2026-01-20 | #374 | LT-22353 | Allow phonological merge (2→1) and split (1→2) rules: fixed an untested RHS bug in `NarrowAnalysisRewriteRuleSpec`, and extended it to cover expansion (previously narrowing-only). | **partially covered** (re-assessed P9/W12, 2026-07-10) | Merge half: **covered**, see row 1 (`rewrite/merge` + `rewrite/multiplemerge` now MATCH). Split/expand half: `rust/conformance/rewrite/expand/` (1-seg→2-seg) was frozen as MATCHES (both engines reject all 3 words under the untraced batch oracle — a negative-only result, per the fixture's own "oracle curiosity" note: C#'s `tracing on`/interactive `parse` accepts the same word the untraced `batch` command rejects). Re-run during this audit: Rust now **accepts** `biiiibiiii`, which is the linguistically correct expansion (matches what the C# oracle itself finds under tracing) — i.e. Rust now diverges from the untraced-batch oracle's own internal inconsistency, not from a correct C# answer. Not a Rust defect and not fixed; the residual is deciding which C# mode (traced vs. untraced-batch) is the authoritative parity target for this class of grammar, a candidate follow-up (see `rewrite/expand/README.md` "Re-verified 2026-07-10"). No dedicated single-subrule merge+split-sibling fixture was built (out of scope; the two directions are covered by separate fixtures using the plan's own probe skeleton). | +| 4 | `af3037ac` | 2025-07-18 | #312 | LT-22140 | Add compound-rule productivity restrictions: new `Head/NonHead/OutputProdRestrictionsMprFeatures` `MprFeatureSet`s on `CompoundingRule`, checked via `MprFeatureSet.CompoundMprFeaturesMatch` (nonempty-intersection-or-empty-required). | **covered** | `hc-grammar/src/load.rs:1220-1269` loads all three sets into `CompoundingRuleDef`; `hc-rules/src/morph.rs:1800`'s `compound_match` implements the identical overlap-or-empty semantics; `hc-rules/tests/non_head_root_filter_gate.rs::split_dropped_when_root_found_but_mpr_restriction_unsatisfied` pins exactly this behavior by name (`CompoundMprFeaturesMatch`/`NonHeadProdRestrictionsMprFeatures` cited in the test's own doc comment). | +| 5 | `90dcee64` | 2025-06-19 | #311 | LT-22156 | `Allomorph.CheckAllomorphConstraints`: allomorph/morpheme co-occurrence rules changed from "at least one rule passes" (`.Any(IsWordValid)`) to "every rule must pass" (`foreach ... return false` on first failure) — fixes ad hoc rules on clitics not blocking. | **covered** (was needs-fixture; W6 landed, re-verified P9/W12 2026-07-10) | `MorphemeCoOccurrenceRule`/`AllomorphCoOccurrenceRule` are no longer hard-linted (`hc-grammar/src/load.rs` W6 comment, `AND-not-OR` loop in `hc-rules/src/validity.rs`). Fixture `rust/conformance/cooccurrence/and-semantics-pin/` pins exactly this row's regression (two `MorphemeCoOccurrenceRule`s, one unsatisfiable/one trivially-satisfied — the post-fix AND semantics must still reject); confirmed passing via `hc-parse/tests/cooccurrence_gate.rs::and_semantics_pin_matches_oracle` (plus two sibling fixtures, `allomorph-basic` and `morpheme-adjacency`, all green). | +| 6 | `7ee16798` | 2025-06-05 | #307 | — | Wrap rule-compile exceptions in a new `CompileException` naming the failing template/mrule/prule, for better user diagnostics. | **N/A-mechanical** | Diagnostic/error-message-only; zero effect on any successfully-compiled grammar's parse or synthesis output. TraceManager/compiler-diagnostics parity is an explicit phase-2 non-goal (`rust-optimizations-phase2.md` §Non-goals). | +| 7 | `3a9b17e1` | 2025-01-24 | #285 | — | **Misdiagnosed in the original pass** (re-read the literal diff during P9/W12, 2026-07-10): the commit does **not** disable or touch the `originals.Count < 2` special case at all — that branch is present, unchanged, in current C# (`Word.cs:495-498`). The actual diff is three things: (1) a comment-only fix; (2) **adds** a previously-missing block that propagates the realizational-FS diff onto each re-expanded alternative (`alternative._realizationalFS.Unify(diff, ...)`, cs:515-524) — before this commit alternatives silently lost any realizational-FS delta accumulated since their shared source; (3) fixes a latent-truthy bug, `RootAllomorph != null` (always true once ever set) → `RootAllomorph != Source.RootAllomorph` (the intended comparison), cs:525-526. | **covered** (was needs-fixture on a misdiagnosis; re-diagnosed + confirmed covered P9/W12, 2026-07-10) | `RealizationalRule` is no longer hard-linted (W5 landed, `hc-grammar/src/load.rs` comment; fixtures `rust/conformance/realizational/{realizational-rule,family-blocking,stem-name}/`, all green via `hc-parse/tests/realizational_gate.rs`). The specific `ExpandAlternatives` fix this row describes is ported verbatim in `hc-rules/src/word.rs::expand_alternatives` (lines ~424-433), which cites cs:515-524 and cs:525-526 by line number for exactly the realizational-FS-diff-propagation and `RootAllomorph != Source.RootAllomorph` pieces the actual diff added/fixed. General multi-alternative reconstruction (the `originals.len() >= 2` branch this fix lives in) is exercised end-to-end by `hc-rules/tests/stratum_gate.rs::merge_equivalent_analyses_folds_homophonous_suffixes_and_expand_recovers_both`. Residual: no fixture exercises the realizational-FS-diff propagation specifically in combination with a 2+-alternative merge (the exact shape of this commit's own fix) — the CLI batch oracle's `sig` column doesn't expose internal FS state, so an oracle-verified Path-A fixture for this specific interaction isn't mechanically straightforward; flagged as a small candidate follow-up (would need a unit-level test in the `hc_rules` crate itself, akin to the `stratum_gate.rs` test cited above, not a CLI fixture). | +| 8 | `94336406` | 2025-01-22 | #282 | — | Merge duplicate (shape-equivalent) analyses within `AnalysisStratumRule.Apply`, carrying the folded-away candidate on `Word.Alternatives`; `Morpher`/`Word.ExpandAlternatives` re-expand before synthesis. | **covered** | `hc-rules/src/stratum.rs` ports this with direct C# line citations (`AnalysisStratumRule.cs:150-177`, `Word.cs:491-533`) via `dedup_key`/`expand_alternatives`; `hc-rules/tests/stratum_gate.rs::merge_equivalent_analyses_folds_homophonous_suffixes_and_expand_recovers_both` pins the exact fold-then-recover contract this commit introduced (evidence doc's own "verify a fixture pins it" note for #282 — confirmed yes). | +| 9 | `d13e903e` | 2025-01-13 | — | — | Another `MorphologicalRuleOrder.Linear` bug fix in `AnalysisStratumRule` (permutation-cascade ordering). | **covered** | `hc-rules/tests/stratum_gate.rs::unordered_combination_reaches_root_linear_permutation_misses` and `::linear_stratum_unapplies_suffixes_in_reversed_order` directly test Linear-vs-Unordered permutation-cascade semantics (reversed-index walk, non-commuting suffix order). | +| 10 | `83f73d11` | 2024-12-04 | — | LT-21988 | `MorphologicalRuleOrder.Linear` bug fix (same area as row 9, earlier in the sequence). | **covered** | Same tests as row 9. | +| 11 | `dfbb754b` | 2024-10-23 | #264 | LT-21939 | `SynthesisAffixProcessAllomorphRuleSpec.ApplyRhs`: when a rule produces no new output morph nodes (pure truncation), the allomorph/subsumed-input attribution must still be marked (previously lost, breaking `TruncateRules` parsing). | **covered** (was needs-fixture; W9.1 `MorphStatus::Floating` port landed, re-verified P9/W12 2026-07-10) | `hc-rules/src/morph.rs::attribute_morphs` now implements the full wave-4 fallback model (`MorphStatus::Floating`/`SubsumedChild`/`SubsumedFirst`, doc block at morph.rs:727-771, cites C# `ApplyRhs` cs:137-207 by line). Fixture `rust/conformance/affix-shapes/truncate/` (green via `hc-parse/tests/affix_shapes_conformance.rs::truncate_matches_oracle`) plus the unit-level `hc-parse/tests/csharp_port_affix_process.rs::subsumed_affix_findings` (bubib -> "42 PRES", "43 PRES" — the pure-truncation rule's own morph correctly recorded, not dropped) directly pin this commit's `outputNewMorph == null` branch by gloss-chain assertion. | +| 12 | `73a8d852` | 2024-10-17 | — | — | `Morpher.LexicalGuess`: move a `HashSet` allocation inside the per-pattern loop, fixing cross-pattern suppression of homographs. | **needs-fixture** `[guesser, non-goal]` | Root-guesser subsystem — zero Rust surface (see header note 1). | +| 13 | `4442ce3f` | 2024-09-30 | — | — | "Fix formatting" — `RootAllomorph.cs` line-wrap only. | **N/A-mechanical** | Diff verified: single `if` condition reflowed across 4 lines, identical tokens. | +| 14 | `2dc7d71b` | 2024-09-30 | — | — | Replace `ShapeNode.Iterative` property with `IsIterative()`/`SetIterative()` extension methods backed by `Annotation.Data` (nullable-object storage instead of a first-class bool field). | **N/A-mechanical** | Diff verified as a pure 1:1 storage-representation rename — `IsIterative()` returns `Annotation.Data != null`, `SetIterative(true)` sets it, identical truth table at every call site. Touches only already-separately-tracked unported areas (N3 pattern language, guesser `MatchNodesWithPattern`). | +| 15 | `fc3c9396` | 2024-09-27 | — | — | "Fix formatting" — collapse an auto-property to one line. | **N/A-mechanical** | Diff verified: `{ get; private set; }` reflow only. | +| 16 | `dd7fb650` | 2024-09-27 | — | — | "Fix formatting" — collapse an `if` condition to one line. | **N/A-mechanical** | Diff verified: whitespace/line-wrap only, identical tokens. | +| 17 | `c283891e` | 2024-09-27 | — | — | "Fix formatting" — remove two stray blank lines. | **N/A-mechanical** | Diff verified: blank-line removal only. | +| 18 | `a8c8d2b9` | 2024-09-27 | — | — | "Fix formatting" — reorder two `using` directives. | **N/A-mechanical** | Diff verified: `using` statement order swap only (CSharpier/`.editorconfig` `using` sort), no code change. | +| 19 | `45f75713` | 2024-09-27 | — | — | "Fix formatting" — fix one misaligned closing brace. | **N/A-mechanical** | Diff verified: indentation of a single `}` only. | +| 20 | `bacbaf62` | 2024-09-27 | — | — | "Address Damien's concerns" — root-guesser review feedback across `CharacterDefinitionTable.cs`/`Morpher.cs`/`RootAllomorph.cs`/`Segments.cs`/`XmlLanguageLoader.cs`. | **needs-fixture** `[guesser, non-goal]` | Root-guesser subsystem review-feedback commit (part of the Sept 2024 guesser PR arc alongside rows 25, 24, 26, 39, 40). | +| 21 | `537afad8` | 2024-09-20 | — | — | "Fix formatting" — move a closing paren to its own line. | **N/A-mechanical** | Diff verified: paren placement only. | +| 22 | `c195dd06` | 2024-09-20 | — | — | "Fix formatting" — add a trailing comma / brace reflow. | **N/A-mechanical** | Diff verified: punctuation/line-wrap only. | +| 23 | `0b005ef2` | 2024-09-20 | — | — | "Fix formatting" — reflow an `if`/`return` block, add braces. | **N/A-mechanical** | Diff verified: brace-style only, identical control flow. | +| 24 | `8eacedc3` | 2024-09-20 | — | — | "Fixes issues raised by Damien" — further root-guesser review feedback. | **needs-fixture** `[guesser, non-goal]` | Same guesser PR arc as row 20. | +| 25 | `8c973e0a` | 2024-09-18 | — | — | `Morpher.LexicalGuess`: dedupe guessed roots when multiple FST paths through a lexical pattern (e.g. `([Seg])([Seg])`) produce the same shape string. | **needs-fixture** `[guesser, non-goal]` | Root-guesser subsystem. | +| 26 | `d71bc078` | 2024-09-17 | — | — | "Fix bugs in root guesser." | **needs-fixture** `[guesser, non-goal]` | Root-guesser subsystem. | +| 27 | `ff44d6af` | 2024-09-16 | — | — | "Fix formatting" — brace placement. | **N/A-mechanical** | Diff verified: single-line brace fix only. | +| 28 | `f20ace39` | 2024-09-16 | — | — | "Fix formatting" — split a one-line `if` body onto its own line. | **N/A-mechanical** | Diff verified: whitespace only, identical logic. | +| 29 | `eecae2e1` | 2024-09-16 | — | — | "Fix formatting" — same `if`-body split in two files. | **N/A-mechanical** | Diff verified: whitespace only. | +| 30 | `8bee6c4e` | 2024-09-16 | — | — | `CharacterDefinitionTable.GetShapeNodes` learns the root-allomorph pattern language: `[NatClass]`, `([NatClass])` (optional), `[NatClass]*` (Kleene star) as a fallback when literal characters don't match. | **covered** (was needs-fixture; W2.N3 landed, re-verified P9/W12 2026-07-10) | Fixture `rust/conformance/loader/n3-pattern-shapes/` exercises exactly this: a root allomorph declared with pattern shapes in its `PhoneticShape`, loaded and matched end-to-end. Green via `hc-parse/tests/loader_n3_pattern_shapes_gate.rs` (both `n3_pattern_shapes_grammar_loads_with_the_allomorph_present` and `n3_pattern_shapes_matches_oracle_end_to_end`). | +| 31 | `333a1cf6` | 2024-09-13 | — | — | "Fix formatting" — collapse a multi-line object initializer. | **N/A-mechanical** | Diff verified: whitespace only. | +| 32 | `9bd9f073` | 2024-09-13 | — | — | "Fix formatting" — move closing paren of a method signature. | **N/A-mechanical** | Diff verified: whitespace only. | +| 33 | `a9c89fa1` | 2024-09-13 | — | — | "Fix format" — reflow a long method signature across multiple lines. | **N/A-mechanical** | Diff verified: parameter list reflow, identical signature. | +| 34 | `378181cf` | 2024-09-13 | — | — | "Fix formatting" — collapse an object initializer to one line. | **N/A-mechanical** | Diff verified: whitespace only. | +| 35 | `6dfafa6f` | 2024-09-13 | — | — | "Fix formatting errors and placement of Guessed" — moves the `Guessed` bool property from `RootAllomorph` up to the base `Allomorph` class. | **needs-fixture** `[guesser, non-goal]` | Structural move (widens `Guessed` to all `Allomorph` subclasses) within the root-guesser subsystem — `Guessed` is only ever set by `LexicalGuess`. | +| 36 | `e11422b1` | 2024-09-13 | — | — | "Fix formatting errors" — fix one line's leading-space indentation. | **N/A-mechanical** | Diff verified: whitespace only. | +| 37 | `c1f99b8f` | 2024-09-13 | — | — | "Fix formatting problems" — remove a stray blank line. | **N/A-mechanical** | Diff verified: blank-line removal only. | +| 38 | `cc23686d` | 2024-09-12 | — | — | "Fix formatting" — fix indentation of a method signature and a property. | **N/A-mechanical** | Diff verified: whitespace only. | +| 39 | `74e4c86a` | 2024-09-12 | — | — | "Improve root guesser" (large: 150+/37- across `Morpher.cs`/`RootAllomorph.cs`). | **needs-fixture** `[guesser, non-goal]` | Root-guesser subsystem. | +| 40 | `3b4f8441` | 2024-09-09 | — | — | "Add MaxUnapplications and GuessRoot" — introduces the `GuessRoot` feature itself plus an unapplication-count cap. | **needs-fixture** `[guesser, non-goal]` | Origin commit of the root-guesser subsystem/API. | +| 41 | `4b72d166` | 2024-02-22 | #171 | — | Editor-config/style pass: namespace-scoping, primary constructors, naming enforcement, unused-usings removal. | **N/A-mechanical** | PR body states explicitly: "No Functional change." | +| 42 | `6fa37e91` | 2024-01-30 | — | — | Update to latest CSharpier version; reformat. | **N/A-mechanical** | Formatting-tool version bump; diff is line-wrap/brace-style only across many files. | +| 43 | `0e38305d` | 2024-01-30 | — | — | `AffixProcessAllomorph.CheckAllomorphConstraints`: required-syntactic-FS check changed from `IsUnifiable` to `Subsumes` (a required FS must be entailed by, not merely compatible with, the word's accumulated FS). | **covered** | `hc-rules/src/validity.rs:23-24` explicitly ports this (`RequiredSyntacticFeatureStruct.Subsumes(word.syn)`, cited by C# line number); `hc-rules/tests/validity_gate.rs::required_syntactic_fs_gates_on_the_words_accumulated_syn_fs` pins the exact `Subsumes`-not-`IsUnifiable` distinction (num=sg root + num=pl-requiring suffix must reject; num=pl root must pass). | +| 44 | `85c8eff8` | 2023-03-14 | — | — | "Fix formatting" — reindent two members in `Word.cs` (tabs→spaces artifact). | **N/A-mechanical** | Diff verified: whitespace/indentation only. | +| 45 | `c4b3ae6a` | 2023-03-13 | — | — | Add public `Word.MorphologicalRules` getter (exposes `_mruleApps`). | **covered** | Underlying data (`MorphRecord`, `Word.mrule_apps`) is present and exercised throughout `hc-rules/tests/stratum_gate.rs`; the getter itself is a FieldWorks-facing introspection API (not consumed by any internal parse-decision logic — confirmed via `grep` showing its only other C# use site is inside an `#if OUTPUT_ANALYSES` debug block), not a parse-output behavior and not TSV-fixture-testable. | +| 46 | `abcadfeb` | 2023-02-27 | — | — | Update to CSharpier 0.22.1; reformat. | **N/A-mechanical** | Formatting-tool version bump. | +| 47 | `edb680a7` | 2022-12-19 | — | — | Remove unused `MorphsEqualityComparer` static field from `Morpher` (dead code after an earlier refactor). | **N/A-mechanical** | Dead-code removal, zero behavior change (field was already unreferenced). | +| 48 | `f33fab6a` | 2022-05-23 | — | — | First adoption of CSharpier; whole-assembly reformat. | **N/A-mechanical** | Formatting-tool adoption. | +| 49 | `3a8d21a8` | 2022-05-09 | — | — | Register a new `MorphID` string feature in the internal `HCFeatureSystem`. | **N/A-mechanical** | Purely additive bookkeeping-feature registration; zero standalone behavior until consumed by a later commit (the consuming behavior, e.g. row 11's truncate-rule fix, is assessed on its own merits there). | +| 50 | `2469021f` | 2022-04-14 | — | — | "Add unit test for multiple environments": `Allomorph`'s environment check flips from **conjunctive** (`Where(!valid).ToArray(); if len>0 return false`, i.e. fail if *any* environment is invalid ⇒ pass requires *all* valid) to **disjunctive** (`Count>0 && !Any(valid)`, i.e. fail only if *none* is valid ⇒ pass requires *at least one*). The same AND→OR flip is applied to the two co-occurrence-rule checks; `TraceManager.Failed` now reports the full failing-rule collection instead of just the first; extracts `Word.RootMorphID`. | **covered** (closed by W3 item 4, 2026-07-08: `hc-rules/tests/validity_gate.rs::environments_ok_two_entries_use_or_semantics` pins the OR — two required environments, exactly-one-satisfied passes either way round, neither-satisfied fails) | Not a no-op (caught on review — the boolean transform is a genuine AND→OR semantic change, matching the commit's own title/purpose). The **environment** half is *implemented* in Rust — `hc-rules/src/validity.rs::environments_ok` is `envs.iter().any(...)`, i.e. already on the post-fix OR side — but **unpinned**: every existing `validity_gate.rs` `environments_ok_*` test uses a single-entry `Environments` list (its AND/OR-within-one-environment's left+right sides is a different axis, already covered). No test exercises 2+ list entries where only one is satisfiable. **Fixture:** an allomorph declaring two environments, only one of which the surface form satisfies — must still validate. The **co-occurrence** half is moot in Rust today (`AllomorphCoOccurrenceRule`/`MorphemeCoOccurrenceRule` hard-linted `Unsupported`, row 5) and is superseded in C# itself by `90dcee64` (row 5, #311/LT-22156), which reverts co-occurrence back to conjunctive — so when W6 ports co-occurrence, build it to row 5's spec, not this commit's. | +| 51 | `987be2fd` | 2022-04-13 | — | — | `Allomorph.IsWordValid`: check environments and the disjunctive-allomorph/free-fluctuation rule **per specific morph occurrence** (via `word.GetMorphs(this)`) instead of once per allomorph ID; adds the `GetDisjunctiveAllomorphApplications`-driven per-morph disjunctive re-check. | **covered** (closed by W3.2 + W3.3, 2026-07-08) | Was explicitly deferred in Rust (the old `validity.rs:29-35` scope note). Now ported: the per-morph disjunctive re-check is `validity.rs::allomorphs_valid_impl`'s candidate loop over `MorphRecord::passed_over` (C# `appliedAllomorphIndices`, recorded by `morph.rs::synth_affix`) with the `Range(0, Index)` root fallback, pinned by the oracle fixture `rust/conformance/allomorphy/disjunctive-recheck/` (+ the un-ignored `csharp_port_lex_entry.rs` DisjunctiveAllomorphs negative half, whose W11 "finding" was exactly this gap); the **per specific morph occurrence** half (environments checked at every occurrence) is W3.3's per-contiguous-run records, pinned by `rust/conformance/allomorphy/discontinuous-env/`. | +| 52 | `1ce88e01` | 2022-04-13 | — | — | "Optimize word synthesis" — restructures `Morpher.GenerateWords`/parallel synthesis; **removes** the whole-word `CheckDisjunction` pairwise-allomorph-precedence filter entirely. | **superseded** → `987be2fd` (row 51) | `CheckDisjunction`'s coarse whole-word grouping-by-`AllomorphsInMorphOrder` filter is replaced the same day by `987be2fd`'s finer per-morph/per-allomorph-index disjunctive mechanism (confirmed via commit order: `1ce88e01` then `987be2fd`, both 2022-04-13). The replacement mechanism is itself unported in Rust (row 51, needs-fixture) — this row does not need its own separate fixture. | +| 53 | `606d2f3a` | 2022-04-12 | — | — | "Reduce memory usage while synthesizing words" — switches the analyses collection to a `ConcurrentQueue` for parallel dequeue during `GenerateWords`. | **N/A-mechanical** | Thread-safety/data-structure change for parallel execution; diff verified the candidate/result set itself is unchanged, only how it's iterated. | +| 54 | `e19ca20f` | 2022-04-12 | — | — | "Reduce search space in AnalysisStratumRule" — adds `.Distinct(FreezableEqualityComparer)` to the mrule/template output enumerations before switching on `MorphologicalRuleOrder`. | **covered** | Rust's `hc-rules/src/stratum.rs` dedups every candidate set by `dedup_key` throughout the stratum pipeline (a strictly more thorough mechanism); `stratum_gate.rs`'s exact-set assertions (e.g. "Full unordered set: { akp (seed), ak, a }") implicitly confirm no duplicate candidates survive. | +| 55 | `2cb0edf5` | 2021-12-15 | — | — | csproj: bump package description for TensorFlow NMT model support. | **N/A-mechanical** | csproj-only, 1-line description change. | +| 56 | `057a3c5e` | 2020-10-08 | — | — | csproj: rename "translator" tool references to "machine". | **N/A-mechanical** | csproj-only. | +| 57 | `341fc50b` | 2019-01-24 | — | — | csproj: fix machine API middleware reference. | **N/A-mechanical** | csproj-only, 1-line addition. | +| 58 | `a5a8239f` | 2018-11-28 | — | — | `SynthesisAffixProcessAllomorphRuleSpec.ApplyRhs`: only mark a subsumed allomorph once (`markedAllomorphs` `HashSet` dedup), not once per repeated input-morph occurrence of the same allomorph. | **covered** (was needs-fixture, stale grep; re-verified P9/W12 2026-07-10) | The original `grep` finding is stale — `hc-rules/src/morph.rs::attribute_morphs` now has a `marked: Vec` dedup (lines ~773-836, `marked.contains(&m.allomorph)` guards at every subsumption branch, explicitly citing this row's sha and C#'s `markedAllomorphs` by name in the doc block at morph.rs:758) — a direct, faithful port of the `HashSet`-dedup-once semantics this commit introduced. Pinned at the unit level by `hc-parse/tests/csharp_port_affix_process.rs::subsumed_affix_findings` (`tags`/`tagsv` cases exercise repeated-occurrence dedup via gloss-chain assertion: "47 3SG PAST", not a doubled "47 3SG 3SG PAST"). No dedicated Path-A oracle fixture (grammar.xml/words.txt/expected.tsv) exists for this row specifically — the unit test's grammar (`SUBSUMED_AFFIX_MRULES`) is parse-CLI-compatible and would make a reasonable future oracle fixture, but the existing unit test already exercises the exact dedup mechanism by direct gloss-chain assertion, judged sufficient for this closeout given the shared root allomorph list is not independently oracle-batch-observable beyond what `assert_morphs_eq` already checks. | +| 59 | `97fa7721` | 2018-11-28 | — | — | `SynthesisAffixProcessAllomorphRuleSpec`/`SynthesisCompoundingRule`: "do not allow non-contiguous morph annotations" — when a copied part's nodes aren't contiguous (an intervening deleted/inserted node), split the marking into separate contiguous-run morph annotations instead of one span covering the gap (`MarkMorphs` helper, keeps the longest run as the "primary" morph). | **covered, with one residual noted** (re-verified P9/W12, 2026-07-10 — more complete than previously recorded) | Both halves are now ported, contra the prior note: (1) the contiguous-run split itself — `attribute_morphs`'s `key_runs`/`runs_of` machinery, one `MorphRecord` per run; (2) the **"longest run is primary"** clause — `affix_host_order`/`longest_run_order` (morph.rs:703-724, doc comment: "affix's 'primary' annotation = its longest run, FIRST longest at ties (C# `MarkMorphs`' strict `>` comparison, cs:253)"), which directly feeds the `MarkSubsumedMorph` attachment point (row 58). Fixture `rust/conformance/affix-shapes/noncontiguous/` MATCHES the oracle at the accept/reject + boundary-count `sig` level (`hc-parse/tests/affix_shapes_conformance.rs::noncontiguous_matches_oracle`), and `csharp_port_affix_process.rs::subsumed_affix_findings` independently exercises the primary/longest-run + gap-adjacent-morph interaction at the gloss-chain level for a different grammar. **Residual:** no single fixture combines "gap in the exact grammar shape this commit's own C# test used" with full morph/gloss-order assertion via the CLI oracle — the `noncontiguous` fixture's own README flags that the batch `sig` column can't distinguish a `97fa7721`-style attribution bug from a correct one at that granularity. Closing this fully would need a `hc-rules`-crate unit test (not a CLI fixture) built on the noncontiguous fixture's exact grammar; small, flagged as a candidate follow-up, not done here (bounded scope). | +| 60 | `e118c14e` | 2018-05-15 | — | — | csproj: target .NET 4.6.1. | **N/A-mechanical** | csproj-only. | +| 61 | `04935b09` | 2018-04-23 | — | — | csproj: switch to the SIL.Core NuGet package (drop a project reference). | **N/A-mechanical** | csproj-only. | +| 62 | `20107e52` | 2018-03-12 | — | — | csproj: switch to the .NET Standard QuickGraph package. | **N/A-mechanical** | csproj-only. | +| 63 | `4fa9d8ea` | 2017-09-20 | — | — | Rename `Span` to `Range` across the SIL.Machine base library (and all HC call sites). | **N/A-mechanical** | Mechanical type rename; diff verified as 1:1 substitution with reflow, zero logic change at every call site inspected. | +| 64 | `7cff19dc` | 2017-09-20 | — | — | Remove the `SpanFactory` constructor parameter from `Morpher` (and downstream `Language.CompileAnalysisRule`/`CompileSynthesisRule`) — spans are now created without an explicit factory. | **N/A-mechanical** | API-plumbing refactor; diff verified as parameter removal + reflow only, no behavior change. | +| 65 | `da1df21f` | 2017-08-16 | — | — | Update to .NET Core 2.0 — drop `#if NET45`/`#elif NETSTANDARD1_3` conditional compilation (single code path). | **N/A-mechanical** | .NET-version churn; diff verified as preprocessor-directive removal only, the surviving code path is unchanged. | +| 66 | `e1ed50a7` | 2017-07-27 | — | — | Update unit test projects to .NET Core — rename preprocessor symbols `NETFX`→`NET45`, `NETSTD`→`NETSTANDARD1_3`. | **N/A-mechanical** | .NET-version churn; symbol rename only. | +| 67 | `2539159e` | 2017-03-09 | — | — | csproj: fix `DEFINE` constants for .NET Standard builds. | **N/A-mechanical** | csproj-only. | +| 68 | `ccd66ba8` | 2017-03-09 | — | — | Update projects to VS2017 project-file format (`.projitems`/`.shproj`→`.csproj`). | **N/A-mechanical** | Build-system/project-file format churn. | +| 69 | `a1f7e22c` | 2016-11-24 | — | — | Update .NET Framework projects to 4.5 — rename preprocessor symbol `NET4`→`NETFX`. | **N/A-mechanical** | .NET-version churn; symbol rename only. | +| 70 | `53477e19` | 2016-08-22 | — | — | Merge remote-tracking branch `hc/master` into `Translation` (bulk upstream HermitCrab.NET merge #1) — brings in the (at the time) current `XmlLanguageLoader`/`XmlLanguageWriter`/DTD/etc. wholesale. | **covered** | Initial engine substrate (see header note 2); surviving behavior exercised broadly by the three reference corpora and the `hc-grammar`/`hc-rules` test suites; anything from this substrate later independently modified has its own row. | +| 71 | `d40f46fe` | 2016-08-22 | — | — | Fix the embedded-DTD resource name in `XmlLanguageLoader`'s `ResourceXmlResolver` after the folder reorg (`SIL.HermitCrab...` → `SIL.Machine.Morphology.HermitCrab...`). | **N/A-mechanical** | Resource-path string fix only, no logic change; a one-off correction of row 74's reorg. | +| 72 | `674e3a8e` | 2016-08-19 | — | — | Add `IMorpheme` interface to HC morphemes; remove the separate `HermitCrabMorphologicalAnalyzer`/`Generator` classes; **introduce `Morpher.AnalyzeWord`/`GenerateWords(WordAnalysis)`/the `WordAnalysis` type and `Morpher.Morphemes`.** | **covered** (was needs-fixture, stale note; W7 landed by the time of this audit, confirmed P9/W12 2026-07-10 — matrix note was already corrected once by P8(b), "the 5 GenerateWords assertions were already ported", this pass verifies the fuller picture) | `hc_parse::{WordAnalysis, GenMorpheme}` and `Morpher::{generate_words, generate_words_from_analysis, analyze_word}` are fully implemented (`hc-parse/src/morpher.rs`). `hc-parse/tests/csharp_port_generation.rs` ports both `MorpherTests.GenerateWords_*` C# tests plus the `CompoundingRuleTests` `GenerateWords` assertion, the two/multi-`NonHead` direct-API cases, the `WordAnalysis` left-side-reversal case, and `MorpherTests.AnalyzeWord_CanAnalyze_ReturnsCorrectAnalysis` (structured `WordAnalysis` equality, not just string signature) — 8 tests total, all green. **Caveat on "oracle-verified":** the C# CLI tool (`hc.dll`) has no `generate` command at all (confirmed: no `case "generate"` in `Program.cs`/`BatchCommand.cs`) — `GenerateWords`/`WordAnalysis` are pure object-API surfaces in C#, never CLI-exposed, so the established Path-A pattern (grammar.xml+words.txt+expected.tsv via `batch`) is not mechanically available for this row. The `csharp_port_generation.rs` tests instead port the C# unit tests' own literal expected values directly from `MorpherTests.cs`/`CompoundingRuleTests.cs` source — the closest available equivalent to oracle verification for an API with no CLI surface. | +| 73 | `1b6d8f42` | 2016-08-18 | — | — | Merge remote-tracking branch `hc/master` into `Translation` (bulk upstream HermitCrab.NET merge #2) — brings in `AffixTemplate`/`Allomorph`/`CharacterDefinitionTable`/the DTD/etc. | **covered** | Same as row 70. | +| 74 | `a856521f` | 2016-06-02 | — | — | "Reorganized folders to follow .NET project structure" — the initial commit adding the HermitCrab engine source under this repo's layout. | **covered** | Pure file relocation forming the initial engine substrate; see row 70's note. | + +## Summary + +**Verdict counts (74 total) — updated P9/W12 closeout, 2026-07-10:** + +| Verdict | Count | +|---|---| +| covered | 20 | +| partially covered | 1 | +| needs-fixture | 8 | +| N/A-mechanical | 44 | +| superseded | 1 | +| **Total** | **74** | + +Of the 44 N/A-mechanical rows: 20 are pure CSharpier/editor-config reformatting (verified +token-identical), 15 are csproj/target-framework/NuGet/preprocessor-symbol churn, 4 are mechanical +type renames or API-plumbing refactors (verified zero-logic-delta), 2 are dead-code/thread-safety- +only changes, 1 is diagnostics-only (`7ee16798`), and 1 is the folder-reorg substrate's companion +fixup (`d40f46fe`) — see the table for the exact split. (Row `2469021f`, initially miscategorized +as a no-op boolean refactor during a first pass, was corrected to needs-fixture on review, then +**closed** W3-item-4/2026-07-08 — see row 50.) + +**No commit was unclassifiable.** All 10 PR-numbered commits' bodies were fetched successfully via +`gh pr view --repo sillsdev/machine`; `gh auth status` reported valid `public_repo` scope for the +whole session, so no row fell back to commit-message-only evidence. + +### P9/W12 closeout pass (2026-07-10): 9 of the original 19 needs-fixture rows closed this pass + +Starting point for this pass: 19 needs-fixture rows, of which 2 (rows 50, 51) were already closed +by P10 (2026-07-08, same matrix). Of the remaining **17**, this pass closed **9**, leaving **8** +(the entire root-guesser Tier-2 cluster, unchanged — see below) plus row 3 downgraded to +**partially covered** (not fully closed — one half open, see its row). Net: **10 rows moved out of +needs-fixture this pass** (9 fully covered + row 3 partially). + +**Closed this pass (all re-verified against current HEAD, `7f46f2ad`, and/or corrected on +misdiagnosis):** + +1. Row 1 (`812aa48e`/#403, merge-rule stale-index) — **covered.** Frozen fixtures + `rewrite/merge`/`rewrite/multiplemerge` were recorded as DIVERGES at freeze time; re-running them + now shows both MATCH the oracle (Rust now correctly reverses the 2-seg/3-seg merges). Newly + wired as passing tests in `rewrite_conformance.rs`. +2. Row 3 (`f4589887`/#374, merge+split round trip) — **partially covered.** Merge half closes via + row 1's fixtures. Split half (`rewrite/expand`) surfaced a new, subtle wrinkle: Rust now finds + the split-analysis the C# oracle itself only finds in traced/interactive mode, diverging from + the (self-inconsistent) untraced-batch oracle output. Not a Rust defect; not fixed; flagged as a + candidate follow-up to pick the authoritative oracle mode. +3. Row 5 (`90dcee64`/#311, co-occurrence AND-not-OR) — **covered.** W6 landed; fixture + `cooccurrence/and-semantics-pin` (+2 siblings) green. +4. Row 7 (`3a9b17e1`/#285) — **covered, on a corrected diagnosis.** The original row misdescribed + the actual diff (it does not touch the `originals.Count < 2` branch at all); re-read the literal + C# diff and found the real fix is a realizational-FS-diff-propagation addition + a `!= null` → + `!= Source.X` comparison bug fix, both already ported verbatim in `word.rs::expand_alternatives` + with matching C# line citations. +5. Row 11 (`dfbb754b`/#264, truncate-rule morph attribution) — **covered.** W9.1's + `MorphStatus::Floating` port landed; `affix-shapes/truncate` fixture + `subsumed_affix_findings` + unit test both pin it. +6. Row 30 (`8bee6c4e`, root-allomorph pattern language) — **covered.** W2.N3 landed; fixture + `loader/n3-pattern-shapes` green. +7. Row 58 (`a5a8239f`, subsumed-allomorph double-marking dedup) — **covered.** The `grep` this row's + original note relied on was stale; `attribute_morphs`'s `marked` dedup vec is a direct port, + pinned at the unit level by `subsumed_affix_findings`. +8. Row 59 (`97fa7721`, non-contiguous morph annotations) — **covered, with one residual noted.** + Both halves (contiguous-run split AND the "longest run is primary" tie-break) are ported, not + just the first half as previously recorded; a fully combined oracle-level fixture (gap-in-copied- + material + full gloss-order assertion) remains a small candidate follow-up. +9. Row 72 (`674e3a8e`, Generation API origin) — **covered.** `WordAnalysis`/`GenerateWords`/ + `AnalyzeWord` are fully implemented and ported test-for-test from the C# unit tests in + `csharp_port_generation.rs` (8 tests, green). No CLI `generate` command exists in the C# oracle + tool, so a Path-A batch fixture isn't mechanically possible for this row — the ported-unit-test + approach is the best available substitute. + +**Unchanged — genuine gaps, correctly documented already (8 rows, all `[guesser, non-goal]`, +Tier 2):** `3b4f8441`, `74e4c86a`, `d71bc078`, `8c973e0a`, `bacbaf62`, `8eacedc3`, `73a8d852`, +`6dfafa6f` — the whole root-guesser subsystem (`GuessRoot`/`LexicalGuess`/`MaxUnapplications`) +still has **zero** Rust surface, re-confirmed this pass (`grep -rniE +"GuessRoot|LexicalGuess|MaxUnapplications" crates` returns only comment references to an unrelated +`guessRoot=false` parameter and an unrelated per-stratum candidate cap borrowing the same name). No +fixture is buildable for a feature with no Rust implementation; these correctly remain deferred +pending the product yes/no the plan's §Non-goals already calls for. Not attempted this pass, and +not counted as "left to do" in the normal sense — they are already correctly triaged as +spec-first-if-greenlit, not silently gapped. diff --git a/docs/archive/conformance-framework-implementation-notes.md b/docs/archive/conformance-framework-implementation-notes.md new file mode 100644 index 000000000..e68fbdb59 --- /dev/null +++ b/docs/archive/conformance-framework-implementation-notes.md @@ -0,0 +1,222 @@ +# Conformance framework F0-F3 — grounded implementation scoping (post-recon) + +**Postscript (2026-07-10, review-fix pass):** several mechanics this note describes as planned or +provisional have since changed and this historical record is left as-is rather than rewritten — +see `conformance/PROTOCOL.md` and `conformance/README.md` for current behavior. In short: crash +fixtures are pinned via manifest-declared `expectCrash` (not inferred from an empty `expected.tsv`); +`category: pathological` is the sole authoritative signal for pathological gating (the manifest's +separate `pathological` boolean, mentioned below as the planned mechanism, is now a redundant +cross-check the harness FAILs on if it disagrees); the per-word write-up file is `words.yaml`, not +`words.md`; and `manifest.json.requires` is mechanically re-derived from `grammar.xml` and enforced +by the harness on every run, not just checked once at authoring time. + +**Status of this note:** written after actually doing F0 and the F1 pure-move (both committed — +see commits `81677820` and `0d8f90f6`), per an explicit instruction to stop and produce a reviewed +scoping checkpoint before continuing the bulk of implementation. Nothing below has been implemented +yet except what those two commits already did. + +**Post-write correction (same day): the branch this work sits on was rebuilt on top of +`origin/master` instead of the `rust` branch** — per John, the Rust port will never land in this +repo, so the framework must not be built on top of it. `81677820` (F0) cherry-picked cleanly onto +`master` unchanged. `0d8f90f6` (F1)'s `git mv` could NOT replay onto `master` (master never had a +`rust/` directory to move from) — it was redone as a cross-branch content copy instead (new commit +`b5e4aa07`: same end-state file content, verified byte-identical, but via `git checkout + -- rust/conformance` + `git mv` into place, not a same-branch rename). This +note's step 2 below and the "no content changes" / "rename-tracked" wording describe the original +(now-superseded) commit; the content and BatchCommand verification are unaffected and still hold — +re-verified against the new commit before writing this correction. + +## What's actually done (committed, verified) + +1. **F0 — `BatchCommand` ported.** `git show b9b77bfd:.../BatchCommand.cs` copied verbatim into + `src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs`, registered in `Program.cs`'s + `commands` array. `dotnet build src/SIL.Machine.Morphology.HermitCrab.Tool` succeeds clean on + both the original `rust`-based commit and after cherry-picking onto `master`. + Verified by running `batch` against `conformance/rewrite/simultaneous-epenthesis/` and diffing + word/status/signature columns (ms and the `STARTED` sentinel line excluded from comparison, same + convention as b9b77bfd's own verification note and `rust/tools/parse_compare.py`) — exact match, + re-confirmed again after the F1 redo at the new `conformance/` path. +2. **F1 — cross-branch copy done** (see correction above — no longer a "pure move", since this + branch and `rust` share no history for this content). All 38 fixture directories + + `HISTORY-MATRIX.md` now live at `conformance/` on this `master`-based branch, byte-identical to + `rust`'s copy. Nothing inside any fixture has been touched yet: no `script.txt` backfilled, no + `manifest.json`/`words.md` written, no `PROTOCOL.md` written, no Rust test path fixes applied yet + (and now that this branch doesn't contain `rust/` at all, those Rust test path fixes are moot for + THIS branch — they'd only matter wherever the Rust port ends up consuming `conformance/` from, + which is now an explicitly separate, deferred concern per §9 of the plan doc, not something to + fix here). + +## Concrete facts learned during recon that refine/correct the original prompt's assumptions + +- **`BatchCommand`'s real CLI shape differs from the design doc's stated adapter protocol.** + Design doc §4.3 says the protocol is one command: ` batch + `. That's exactly what `hc-rs`'s CLI does + (`rust/crates/hc-cli/src/main.rs` — `batch` subcommand takes 3 positionals: grammar, words, + output). **C#'s `hc.dll` does not** — grammar loading is a *global* `Program.Main` flag + (`-i `), separate from the `batch` subcommand, which only takes 2 positionals + (` `) plus `--start=N`. The normal invocation is + `hc.dll -i grammar.xml -s script.txt` where `script.txt` contains the line + `batch words.txt expected.tsv` (this is exactly what the existing 19 fixtures' checked-in + `script.txt` files already look like — confirmed by reading + `conformance/allomorphy/discontinuous-env/script.txt`). **Decision needed, not blocking F0/F1**: + PROTOCOL.md (§4.3) should document the *engine-agnostic* contract as the design doc states + (single command, 3 positional args) since that's what `hc-rs` already conforms to and what any + third engine should implement — and should separately note, as an aside, that C#'s own + reference tool needs a one-line script-file wrapper to conform to that same contract (or, F2's + self-check mode simply never shells out to `hc.dll` at all — it calls the `Morpher` API + in-process, sidestepping this mismatch entirely, which is what the design doc already specifies + for self-check mode). Recommend: PROTOCOL.md documents the 3-arg single-command contract as + canonical; a short note explains C#'s tool needs a trivial wrapper script to act as an + `--adapter` if anyone ever wants to point `--adapter` at `hc.dll` itself (not required by F0-F3's + acceptance criteria, which only requires self-check-mode + `--adapter` against `hc-rs`). + +- **Exactly 19 of 38 fixtures lack `script.txt`** (confirmed by direct listing, not estimate): + all 4 of `affix-shapes/*` (`circumfix`, `infix`, `noncontiguous`, `truncate`) and all 15 of + `rewrite/*` (`deletion-reinsertion`, `disjunctive`, `expand`, `longdistance`, `merge`, + `multiplemerge`, `multiplesegment`, `multiplesegment-deletion-composition`, `quantifier`, + `required-pos-subrule`, `simultaneous-epenthesis`, `simultaneous-epenthesis-cascade`, + `simultaneous-feeding`, `simultaneous-feeding-control-iterative`, `word-initial-epenthesis`). + The other 19 (`allomorphy/*`, `compounding/*`, `cooccurrence/*`, `loader/*`, `metathesis/*`, + `mpr-groups/*`, `realizational/*`) already have one, in the form + `batch /words.txt /expected.tsv` — **note these existing ones use absolute + paths baked in from whatever worktree generated them** (e.g. + `discontinuous-env/script.txt` reads + `batch C:\Users\johnm\...\phase2-w3\rust\conformance\allomorphy\discontinuous-env\words.txt ...` + — a path that no longer exists anywhere, let alone at the new location). Backfilled/rewritten + `script.txt` files should use **relative paths** (`batch words.txt expected.tsv`, run with cwd + set to the fixture dir, or paths relative to it) so they stay valid across moves — this is a + fix-forward improvement over the existing convention, not a faithful copy of it, and should be + called out as such (existing 19 should probably be normalized to relative paths too while + touching them, for consistency, rather than leaving 19 fixtures on an absolute-path convention + that's already stale). + +- **`manifest.json`'s `constructs` vocabulary must be pinned once, up front**, using §6's exact + key strings verbatim (e.g. `"AffixProcessRule: prefix/suffix/circumfix/infix"`, `"RewriteRule + Simultaneous"` — copy the literal table-cell text from the design doc, not a paraphrase), because + the coverage report (F2) does an exact cross-reference against that checklist. If this pass + writes 38 manifests with inconsistent phrasing, the coverage report is wrong from day one. + +- **Rust test path fix — out of scope for THIS branch, but recorded here for whoever updates the + `rust` branch itself.** Since `conformance-framework` is now based on `master` and carries no + `rust/` directory at all, there is nothing on this branch for these 14 files to be fixed in. The + fix below still needs to happen eventually, just on the `rust` branch (or wherever the Rust port + ends up), once it's ready to point its own tests at the new `conformance/` location instead of its + own `rust/conformance/` copy — a separate, later coordination step, not part of this PR. + Mechanical but touches 14 files, not "a couple." Grep confirms 14 + files under `rust/crates/hc-parse/tests/` reference `../../conformance` (relative to + `CARGO_MANIFEST_DIR` = `rust/crates/hc-parse`, so `../../conformance` = `rust/conformance`). + Moving to repo-root `conformance/` means every one of those needs `../../../conformance` + (one more level up). Full list already collected via Grep: + `simultaneous_conformance.rs`, `strrep_identity_gate.rs`, `rewrite_conformance.rs`, + `mpr_groups_gate.rs`, `realizational_gate.rs`, `loader_n3_pattern_shapes_gate.rs`, + `metathesis_conformance.rs`, `loader_n1_isactive_gate.rs`, `loader_n2_default_symbol_gate.rs`, + `disjunctive_recheck_gate.rs`, `discontinuous_env_gate.rs`, `compounding_conformance.rs`, + `cooccurrence_gate.rs`, `affix_shapes_conformance.rs`. This is a pure string substitution + (`../../conformance` → `../../../conformance`) across these 14 files, doable as one sed-style + pass, followed by `cargo test --workspace` to confirm no regression. Doc comments in these files + that *mention* `rust/conformance/...` as prose (not as a path literal) are stale after the move + but harmless to leave as historical citations, OR could be updated to `conformance/...` for + accuracy — low priority, cosmetic. + +- **No step-count/rule-stats mechanism exists on this branch** (confirmed: `--rule-stats` was + explicitly stripped from `BatchCommand` per b9b77bfd/this port, because it depends on + `Morpher.AccumulateRuleStats` etc. which only exist on unmerged perf-optimization PRs). The F3 + pathological fixture's manifest must use a **wall-clock bound only**, not a step-count ceiling — + the design doc permits this ("wall-clock with a generous margin is fine"). + +- **38 fixtures, not "about 38"** — confirmed exact count via `find conformance -name grammar.xml + | wc -l` = 38, matching the design doc's number. + +## Revised step-by-step plan (small, ordered, independently verifiable, each its own commit) + +**F1 remainder (fixture-content work):** + +1. Write `conformance/PROTOCOL.md` (design doc §4.3), documenting: the 5-column `expected.tsv` + signature format precisely (column meanings, the `STARTED` sentinel-line convention, the + sort-order/`;`-join signature algorithm — cite `BatchCommand.cs`'s `BuildSignature` by + paragraph, not by reproducing code), the single-command 3-positional-arg adapter contract + (` batch [--start N]`), and the aside about C#'s + own tool needing a script-file wrapper to conform (see discrepancy above). One commit. +2. Pin the `constructs` vocabulary: extract §6's table as a literal string list (a small constant, + either inline in this note or as a `conformance/CONSTRUCTS.md` companion — recommend the latter + so F2's coverage report and any future contributor can both point at one file). One commit. +3. Write ONE fixture's `manifest.json` + `words.md` by hand as the template + (recommend `conformance/rewrite/simultaneous-epenthesis/` — already has a rich README with a + confirmed live-oracle bug worth describing accurately in `words.md`). Commit this alone so it + can be reviewed as the pattern before the other 37 follow it. +4. Backfill `script.txt` (relative-path form) + write `manifest.json` + `words.md` for the + remaining 37 fixtures, grouped by category directory (9 groups: `affix-shapes`, `allomorphy`, + `compounding`, `cooccurrence`, `loader`, `metathesis`, `mpr-groups`, `realizational`, + `rewrite` — `rewrite` is the largest at 15). Each group is its own commit. Every `words.md` + entry must be traceable to something read (the fixture's `README.md` and/or `grammar.xml`) — + no fabricated per-word descriptions; if a README genuinely doesn't say enough, that fixture's + entry says so explicitly rather than guessing, and gets flagged for follow-up in the final + report rather than silently invented. +5. Self-check-verify all 38 (see F2 step 2 below — this can't fully happen until the harness's + self-check mode exists, so step 5 is really "re-run after F2 lands," not a separate F1 step). + +**F2 (harness project):** + +1. Scaffold `src/SIL.Machine.Morphology.HermitCrab.Conformance/` as a new C# console project, + referencing `SIL.Machine.Morphology.HermitCrab`; wire into the solution file. Verify + `dotnet build` succeeds with a trivial `Main` before adding logic. One commit. +2. Implement self-check mode: for each fixture under `--fixtures `, load `grammar.xml` via + `XmlLanguageLoader`, build a `Morpher`, parse every word in `words.txt` in-process (same logic + `BatchCommand` uses — consider factoring `BatchCommand`'s `BuildSignature`/`ParseOneWord` into + a small shared internal helper referenced by both, rather than copy-pasting, since they must + stay byte-identical), diff against `expected.tsv` using parse_compare.py's comparison semantics + (read `rust/tools/parse_compare.py` first to port the exact rules: reorder-only and + dup-count-only differences are NOT failures, real signature/status differences ARE). Run + against all 38 migrated fixtures; this is the actual verification that F1's migration didn't + corrupt anything. One commit, plus fixing any fixture the self-check reveals as broken. +3. Implement `--adapter ""` mode: shell out per fixture, same diff logic. Verify + against `hc-rs batch` (need to locate/build the `hc-cli` binary — `rust/crates/hc-cli`). One + commit. +4. Implement coverage report (plain text, per design doc — no report UI): parse every + `manifest.json`, cross-reference `constructs` against the pinned checklist from F1-step-2, + report per-construct fixture counts + negative/cross-cutting flags + zero-coverage constructs. + One commit. +5. Implement `pathological` skip-by-default + `--include-pathological` + per-fixture + timeout/wall-clock bound read from manifest. One commit (can land before F3's pathological + fixture exists, just needs the flag to be inert until then). +6. Fix the 14 Rust test files' path constants (`../../conformance` → `../../../conformance`), + `cargo test --workspace`. One commit, separate from the C# harness commits. + +**F3 (one fixture per new category):** + +1. Negative fixture: design a small grammar + a word that fails one specific constraint (not + random gibberish). Generate `expected.tsv` for real via the now-working `BatchCommand`/harness + self-check (build the fixture, run it through the live oracle, confirm it reports zero parses + for the target word — don't hand-write "0 parses" and assume). `manifest.json` + (`category: negative`) + `words.md` + `script.txt`. One commit. +2. Cross-cutting fixture: per advisor input during recon, prefer an interaction where **both + halves already have a working single-feature fixture to crib grammar structure from** (lower + risk than reduplication×compounding, since reduplication has zero existing fixtures/no proven + grammar skeleton to start from) — e.g. a disjunctive affix-template slot combined with + Simultaneous-mode epenthesis (both already have single-feature fixtures: `rewrite/disjunctive` + and `rewrite/simultaneous-epenthesis`/`-cascade`). Read both constructs' C# source together + before designing the grammar (per the original task brief), generate `expected.tsv` against the + live oracle. `manifest.json` (`category: cross-cutting`) + `words.md` + `script.txt`. One + commit. (Open decision to flag in the final report either way — reduplication×compounding is + also acceptable per the design doc; this note records the lower-risk choice as the + recommendation, not a unilateral final decision.) +3. Pathological fixture: pick one real complexity driver (deeply nested optional affixes is the + cheapest to build convincingly). Manifest records a wall-clock bound (generous margin) instead + of a parse signature, `pathological: true`. Verify the harness's `--include-pathological` flag + actually gates it (runs excluded by default, included when passed). One commit. +4. Final pass: update `docs/conformance-framework-plan.md` §8 acceptance criteria with DONE status + per item + evidence citations (file:line style, matching `rust-optimizations-phase2.md`'s + convention). One commit. + +## Open items / discrepancies to flag when reporting back (not resolved yet) + +- PROTOCOL.md's contract-vs-C#'s-actual-CLI-shape mismatch (see above) — recommend documenting the + 3-arg single-command contract as canonical and noting C#'s wrapper-script caveat, but this is a + judgment call worth a second opinion before writing PROTOCOL.md. +- Whether to normalize the 19 existing `script.txt` files' absolute paths to relative while + touching them (recommended: yes, for consistency and because the existing absolute paths are + already dangling/stale). +- `words.md` vs `words.yaml` — design doc already defaults to YAML-in-Markdown (`words.md` + containing YAML); no new information changes that default. +- Cross-cutting fixture choice (reduplication×compounding vs. disjunctive-slot×Simultaneous + epenthesis) — recommendation above, not yet decided. diff --git a/docs/archive/conformance-framework-plan.md b/docs/archive/conformance-framework-plan.md new file mode 100644 index 000000000..a121b7141 --- /dev/null +++ b/docs/archive/conformance-framework-plan.md @@ -0,0 +1,493 @@ +# HermitCrab Conformance Framework — design & implementation plan + +**Status:** approved by John 2026-07-10, implementation starting on branch `conformance-framework`. + +## 1. Motivation + +The Rust port of `SIL.Machine.Morphology.HermitCrab` built up 38 oracle-generated conformance +fixtures during Phase 2 (`rust/conformance/`), each a hand-minimized grammar + word list + a +signature-format `expected.tsv` generated by running the *live C# engine* as ground truth. This +worked well, but: + +- It lives under `rust/`, which is likely to be extracted to its own repo. The fixtures — and the + oracle that generates them — are not Rust-specific; they should outlive that split. +- Coverage was built two ways: mining C# commit history (`rust/conformance/HISTORY-MATRIX.md`) and + fixtures born from whichever bug someone happened to hit while porting a feature. There is no + systematic walk of HermitCrab's actual grammar-model surface, and no mechanism that tells you + *what's still missing* — an inventory pass for this plan found reduplication has zero fixtures, + affix-template slot semantics are untested in isolation, and several `MorphologicalOutputAction` + variants are only incidentally exercised. +- The oracle-generating tool itself (`BatchCommand`, in `SIL.Machine.Morphology.HermitCrab.Tool`) + doesn't exist on `master` — only in a worktree tied to an unmerged perf-optimization PR. +- Every fixture today is a single-feature positive case. There are no deliberate negative fixtures + (words that must NOT parse), no fixtures designed to exercise more than one mechanism at once + (cross-cutting), and no fixtures whose purpose is to stress the engine's complexity/performance + characteristics rather than pin one rule's correctness. + +## 2. Goals + +1. A **general-purpose, engine-agnostic conformance test suite** for HermitCrab, covering the full + scope of grammar constructs the engine supports — not just the three reference corpora + (Sena/Indonesian/Amharic) — with systematically-tracked coverage, not ad hoc/history-mined + coverage. +2. Structured so **any engine can be validated against it interchangeably** (the Rust port today; + anything else tomorrow) via a documented, minimal CLI adapter contract — not a shared library + dependency. +3. **Lives in `Machine`** (this repo), independent of where the Rust port ends up. +4. Every word (and every individual parse of a word with more than one) carries a **human-readable + description of what it exercises and why it's in the suite** — not just a fixture-level README. +5. The C# side can **run itself and produce a coverage report**, cross-referencing fixtures against + an explicit checklist of grammar-model constructs, so gaps are visible and actionable rather than + discovered by accident. +6. Coverage includes **negative fixtures** (words that correctly fail to parse), **cross-cutting + fixtures** (deliberate interactions between multiple constructs, not just isolated mechanisms), + and a clearly separated **pathological/stress category** — complexity-focused, not + feature-focused, and excluded from default runs since some are deliberately slow. +7. **XAmple-ready, not XAmple-run.** XAmple (SIL's AMPLE-family parser — the other engine + FieldWorks can drive from the same lexical data) does morphotactics but not HermitCrab's + phonological rules. Fixtures therefore declare the engine capabilities they *require*, engines + declare the capabilities they *support*, and the runner filters on the intersection: a fixture + whose grammar uses no phonological rules must produce the **same set of analyses** from + HermitCrab and XAmple. Per John, the near-term deliverable is only the *readiness*: every + fixture accurately tagged phonology-or-not, so XAmple **could** be validated against the suite + at a future date — actually running XAmple (F6) is deferred indefinitely and nothing in this + plan depends on it happening. See §4.6. +8. **Cheap to grow, and grown deliberately.** Adding a word to an existing fixture — or a whole new + minimal fixture — should be a minutes-not-hours task, and the documented policy (§8) is that + every bug fix in a conforming engine lands together with a fixture or new words reproducing the + bug, so the suite accretes regression pins at the same rate the engines accrete fixes. + +The long-range framing, per John: this is an **"all-language parser oracle," intended to be usable +outside of Machine**. The suite's ambition is that *passing this oracle means you can parse any +language* — or it will mean that, as the suite grows. Each fixture is a minimal, documented pin of +one thing some real language's morphology can demand of a parser; the corpus of fixtures, not any +engine's feature list, is the definition of conformance. HermitCrab supplies the seed construct +vocabulary (§6) and its C# engine is the founding oracle that generates ground truth, but neither +is the boundary: the capability-profile mechanism (§4.3) exists precisely so engines of different +scope — XAmple without phonology today, engines with entirely different grammar formalisms later — +can each be measured against the subset they claim, inside Machine's repo or far from it. Design +decisions below should be read with that portability in mind: nothing in `conformance/` may depend +on Machine's build, layout, or C# — the harness is a convenience that lives here; the suite is the +product. + +## 3. Non-goals (for this pass) + +- Not migrating the Rust port itself, or deciding the future-repo split's mechanics — this plan only + needs the framework to be *ready* to be pulled in by whatever ends up consuming it. +- Not attempting literal 100% combinatorial coverage of every construct-pair interaction — see §7 for + what "coverage-complete enough" means in practice. +- Not replacing `rust/parity-out/`'s golden corpus-level regression diffing (Sena/Indonesian/Amharic + full-corpus parity) — that stays as-is; this framework is the systematic, per-mechanism layer + underneath it, not a replacement for real-corpus regression testing. + +## 4. Architecture + +### 4.1 Location and base branch + +A new top-level directory, `conformance/`, at the repo root — sibling to `src/`, `tests/`. **This +branch is based on `origin/master`, not `rust`** — per John: the Rust port will never land in this +repo, so the conformance framework must not be built on top of it. That means the fixture migration +described below is a **cross-branch content copy**, not a same-branch `git mv`: `rust/conformance/` +only exists in the `rust` branch's own history, which this branch doesn't share, so there is nothing +to rename from on this side. The copy is byte-identical to what's on `rust`; it just doesn't carry +that branch's line-history forward into `master`'s lineage (the `rust` branch keeps its own history +for that content regardless). When the Rust port is eventually extracted to its own repo, +`conformance/` stays in `Machine` and gets pulled in by whatever consumes it (git submodule or sparse +checkout pinned to a tagged commit — a mechanics decision for later, not blocking this plan). + +### 4.2 Fixture format (extends the existing one, doesn't replace it) + +Every fixture directory keeps the proven pieces: +- `grammar.xml` — the `HermitCrabInput` XML grammar. +- `words.txt` — one word per line (unchanged; still what an adapter is fed). +- `expected.tsv` — the existing 5-column `BatchCommand` signature format + (`idx\tword\tms\tstatus\tsignature`), engine-agnostic, order-independent. **This is the only file + an engine adapter's output is diffed against** — descriptions and metadata never leak into the + engine-facing contract. +- `script.txt` — the literal oracle-invocation script (already standard on newer fixtures; backfilled + onto older ones during migration). + +Two new files per fixture: + +- **`manifest.json`** — machine-readable metadata, the input to the coverage report: + ```json + { + "id": "rewrite/simultaneous-epenthesis", + "category": "single-feature", + "constructs": ["RewriteRule.Simultaneous", "Epenthesis"], + "tags": ["phonology"], + "requires": ["phonology"], + "pathological": false, + "oracle": { "worktree": ".worktrees/parse-opt", "commit": "ccf750e6" }, + "provenance": "P13, rust-optimizations-phase2.md" + } + ``` + `category` is one of `single-feature` | `negative` | `cross-cutting` | `pathological`. + `constructs` is a list of entries from the checklist in §6 — this is the field the coverage report + keys on. + `requires` is the list of **engine capabilities** the fixture depends on (the multi-engine + filtering mechanism, §4.6). Absent/empty means pure morphotactics — runnable by any conforming + engine. The initially defined capability is `phonology` (the fixture's grammar contains + `RewriteRule`s or `MetathesisRule`s); the vocabulary can grow finer-grained as real engine + differences surface. + +- **`words.yaml`** (the open question in §10 — `words.md` vs `.yaml` — was resolved to YAML) — + **the per-word/per-parse description mandate**. One entry per word in `words.txt`, and where a + word has more than one expected parse, one description per parse: + ```yaml + - word: "buibui" + exercises: > + Simultaneous-mode epenthesis inserts a vowel after every high vowel in one pass; the two + insertion sites (after each of the root's two high vowels) must not feed each other the way + an Iterative pass would. + parses: + - signature: "19|b(u)(b)(u)" + exercises: "the only valid analysis: unapplying both epenthesis sites in a single non-feeding pass yields root 19" + - word: "buuibui" + exercises: "negative case for the same rule -- this shape is NOT reachable by any unapplication order" + expect_fail: true + ``` + This is metadata for humans, never part of the engine-adapter contract — keeping the adapter + contract (§4.3) minimal is what makes "pull in another engine" cheap. It is not read by the + coverage report either; that report keys purely on `manifest.json.constructs` against §6's + checklist (see `CoverageReport.Build`). + +### 4.3 The adapter protocol (the interchangeability mechanism) + +Documented in `conformance/PROTOCOL.md`. Any engine becomes conformance-testable by implementing: +``` + batch [--start N] +``` +producing the existing 5-column TSV with the existing signature algorithm (sorted, +`;`-joined `morph+morph|shape` tuples per distinct analysis). This is exactly what `hc-rs batch` +and C#'s `BatchCommand` already do today — the protocol doc formalizes what's currently only +implicit in `BatchCommand.cs`'s source, so a third engine could implement it without reading anyone's +code. + +Two generalizations for multi-engine support (both documented in PROTOCOL.md): + +- **Capability profiles.** An engine registration declares the capability set it supports (e.g. + HermitCrab/hc-rs: `["phonology"]`; XAmple: `[]`). The runner skips — and reports as *skipped, not + passed* — any fixture whose `manifest.json.requires` lists a capability the engine doesn't + declare. This keeps one suite serving engines of different scope without forking it. +- **Per-engine grammar representation.** The first positional argument is *the engine's grammar + input for the fixture*, not necessarily `grammar.xml`. `grammar.xml` (HermitCrabInput XML) is the + default and the reference representation; an engine that can't consume it names an alternate + per-fixture grammar file/directory in its registration (XAmple's case — see §4.6). `words.txt` + and `expected.tsv` are always shared: one word list, one ground truth, regardless of engine. + +### 4.4 The harness project + +`src/SIL.Machine.Morphology.HermitCrab.Conformance/` (new C# project): +- **Runner**: given `--fixtures --adapter ""`, runs every fixture through + the adapter, diffs against `expected.tsv`, reports pass/fail per fixture and per word. +- **Self-check mode** (no `--adapter`): runs fixtures against the live C# engine directly (via the + now-mainline `BatchCommand`, §5 F0) — this is what catches a fixture going stale when the oracle + itself changes, the lesson LT-22613 taught (the oracle can be wrong and need correcting too). +- **Coverage report**: cross-references every fixture's `manifest.json.constructs` against the full + checklist (§6), and reports, per construct: how many fixtures touch it, whether any are negative, + whether any are cross-cutting, and — for zero/thin-coverage constructs — flags them explicitly. + This is the mechanism John asked for: *"should be able to produce its own coverage report, and + that should be used to actively add grammars and words toward complete coverage"* — the report is + meant to be read by whoever (human or subagent) picks the next fixture to build, not just archived. +- **Pathological fixtures are excluded by default** (`category: pathological` in the manifest); an + explicit `--include-pathological` flag opts in. The runner should also support a per-fixture + timeout/step-cap read from the manifest for this category (see §4.5), so a pathological fixture + reports "within expected complexity budget" rather than hanging the whole suite. + +### 4.5 The four fixture categories + +- **`single-feature`** — today's existing pattern: one mechanism, isolated, minimal grammar. +- **`negative`** — a word that must correctly produce **zero parses** under the grammar. Just as + important as positive fixtures (an engine that over-generates is as wrong as one that + under-generates) and currently has zero dedicated coverage — every existing fixture only pins + words that *do* parse. +- **`cross-cutting`** — deliberately combines two or more constructs in one grammar (e.g. + reduplication interacting with a compounding rule, or a disjunctive affix template slot combined + with Simultaneous-mode epenthesis) — the interactions HISTORY-MATRIX shows are where C#'s own bugs + actually clustered historically, so this category is where new fixtures are highest-value. +- **`pathological`** — per John: **not** about pinning any specific feature's correctness. These + measure the engine's behavior under high combinatorial/complexity load — e.g. deep nested optional + affixes creating exponential candidate counts, overlapping Simultaneous rewrite match sites, + disjunctive-slot combinatorics, a compounding grammar with many co-occurring roots. Each carries in + its manifest an expected complexity budget (step-count ceiling and/or wall-clock bound) rather than + a specific parse signature — the assertion is "stays within budget" (or, for a deliberate + pathological *mis-parse* case, "still produces the mathematically correct answer despite the + adversarial shape," which is a real assertion, just an expensive one to check). Excluded from the + default run; a separate, explicitly-invoked slow-suite job. + +### 4.6 Second engine: XAmple (non-phonology subset) + +XAmple is not in this repo and never will be — it participates purely through the adapter protocol +(§4.3), which is exactly the interchangeability the framework was designed for. The premise: in +FieldWorks, HermitCrab and XAmple are alternative parsers over the same lexical data, so for a +grammar that uses **no phonological rules** (no `RewriteRule`, no `MetathesisRule` — morphotactics, +affix templates, allomorph environment conditioning, compounding, co-occurrence constraints only), +both engines must produce the same set of analyses for the same word. That parity claim is what +XAmple-eligible fixtures pin. + +What this requires, concretely: + +- **Capability filtering** (§4.3): every fixture whose grammar contains phonological rules carries + `requires: ["phonology"]` in its manifest and is skipped for XAmple. Determined mechanically from + `grammar.xml`'s contents, not by hand-tagging. If in practice some morphotactic construct turns + out to have no faithful XAmple equivalent, that becomes a new capability value — the mechanism + already handles it; nothing needs redesigning. +- **A per-fixture XAmple grammar.** XAmple cannot read HermitCrabInput XML — it consumes + AMPLE-style control/dictionary files, and FieldWorks generates each engine's input from its own + lexicon model (there is no HC-XML→XAmple converter, and writing one is out of scope). So each + XAmple-eligible fixture grows a parallel `grammar.xample/` directory encoding the *same* lexicon + and morphotactics. Authoring discipline: morpheme IDs/glosses must match `grammar.xml`'s exactly, + because the signature format compares morpheme names — a naming mismatch would be a false + failure. `expected.tsv` remains the single ground truth (still generated by the C# HermitCrab + oracle); XAmple must reproduce it, not get its own expected file. +- **A thin XAmple adapter** — a wrapper that invokes XAmple on `grammar.xample/` + `words.txt` and + maps its analyses into the 5-column signature TSV. It can live under `conformance/adapters/` if + that's convenient, but it's deliberately outside the engine-facing contract: to the runner it's + just another `--adapter` command. + +This is phase F6 (below) — it needs the XAmple binary and dual-grammar authoring, so it's a +follow-up to the F0-F3 PR, but everything F0-F3 builds (manifest `requires`, capability filtering, +per-engine grammar selection in PROTOCOL.md) is specified now so F6 is additive, not a redesign. + +## 5. Phased implementation + +- **F0 — Canonicalize the oracle CLI.** Merge `BatchCommand` into mainline + `src/SIL.Machine.Morphology.HermitCrab.Tool` on this branch (it currently only exists in + `.worktrees/parse-opt`, tied to an unmerged PR). Blocks everything else — every fixture's + provenance depends on this tool being buildable without checking out a side branch. +- **F1 — Stand up `conformance/` and migrate.** Cross-branch copy of the 38 existing fixtures from + the `rust` branch's `rust/conformance/` into this (`master`-based) branch's top-level `conformance/` + — not a same-branch `git mv`, since this branch has no `rust/` directory to rename from (see §4.1). + Backfill `script.txt` on the older fixtures that lack it. Write `manifest.json` for every migrated + fixture (`category: + single-feature` for all of them at this point — that's an accurate statement about what exists + today). Write `conformance/PROTOCOL.md`. +- **F2 — Build the harness** (`SIL.Machine.Morphology.HermitCrab.Conformance`): runner, self-check + mode, coverage report, and capability filtering (`manifest.json.requires` vs. the engine's + declared capability set, with skipped fixtures reported as skipped). + **Correction (F0-F3 closeout):** this bullet originally also said to wire + `rust/hc-parse/tests/*_conformance.rs` to read fixtures from the new `conformance/` path. That + contradicts §4.1's own premise (this branch is based on `origin/master`, not `rust`, specifically + *because* the Rust port will never land in this repo) — there is no `rust/` directory here to wire + anything in. Struck as inapplicable to this repo; whichever repo the Rust port ends up in is + responsible for pointing its own tests at wherever it pulls `conformance/` in from (§5 F5's "pull-in + process" documentation, not this framework's own F0-F3 work). +- **F3 — Worked examples of the three new categories.** Not full coverage yet — one or two fixtures + per new category, chosen to establish the pattern clearly: one `negative` fixture, one + `cross-cutting` fixture (a real interaction flagged as thin in the checklist, e.g. reduplication × + compounding), one `pathological` fixture (e.g. deeply nested optional affixes). Each with a full + `words.yaml` per-word/per-parse write-up, so later fixture-writing passes have a concrete template. +- **F4 — Run the coverage report, act on it.** With F0-F3 done, run the harness's coverage report + for real and use its output to drive the next fixture-writing pass(es) — reduplication, + affix-template slot semantics, individual `MorphologicalOutputAction` variants, natural-class + precision, boundary-marker edge cases, and negative/cross-cutting variants of already-covered + constructs. This is explicitly an ongoing, iterative process (per John: coverage reports "should + be used to actively add grammars and words towards complete coverage"), not a single pass — F4 + is the first iteration, not the last. +- **F5 — CI wiring.** Run the full corpus (minus `pathological/`) against the C# oracle itself in + Machine's CI — a real regression gate on HermitCrab, independent of any other engine. Document the + pull-in process for whatever eventually consumes `conformance/` (Rust-repo-to-be or otherwise). +- **F6 — XAmple enablement** (§4.6). Write the XAmple adapter wrapper; author `grammar.xample/` + parallels for an initial batch of capability-eligible fixtures (starting with the morphotactic + areas XAmple exercises hardest: affix templates, allomorph environments, compounding, + co-occurrence); run the suite against the XAmple binary and triage any analysis-set differences — + each one is either a bug in an engine (file it, fix it, keep the fixture) or a genuine capability + gap (record it as a new `requires` value). Needs the XAmple binary (from FieldWorks / + `sillsdev/xample`), so it's gated on that being available where the suite runs. + +This plan's first PR covers **F0-F3**: the foundational skeleton, migration, harness (including the +capability-filtering mechanism XAmple will ride on), and one worked example of each new category. +F4 (the coverage-driven backfill loop), F5 (CI), and F6 (XAmple) are explicitly follow-up passes — +the coverage report from F3's tiny worked set won't yet be "complete," and shouldn't be treated as +such. + +## 6. Grammar-model construct checklist (coverage-report keys) + +Built from a direct read of `src/SIL.Machine.Morphology.HermitCrab/` + `MorphologicalRules/` + +`PhonologicalRules/`. This is the initial key set for `manifest.json.constructs` / +the coverage report; expected to grow as gaps are found, not fixed in stone. + +| Construct | Existing coverage | +|---|---| +| Stratum (`Linear`/`Unordered` rule order) | unit-test only, no Path-A fixture | +| `AffixProcessRule`: prefix/suffix/circumfix/infix | covered (`affix-shapes/`) | +| `AffixProcessRule`: reduplication (`ReduplicationHint`) | **none** | +| `AffixProcessRule`: subtraction/truncation | covered (`affix-shapes/truncate`) | +| `RealizationalAffixProcessRule` | covered (`realizational/`) | +| `CompoundingRule` | covered, thin (2 fixtures) | +| `MorphologicalOutputAction`: `CopyFromInput`/`InsertSegments` | incidental only | +| `MorphologicalOutputAction`: `ModifyFromInput`/`InsertSimpleContext` | **none isolated** | +| `RewriteRule` Iterative — epenthesis/deletion/feature/expansion/merge | covered, deepest area | +| `RewriteRule` Simultaneous | covered (P13, 4 fixtures) | +| `MetathesisRule` | covered (`metathesis/`) | +| Affix template slots (obligatory/disjunctive/ordering) | **none isolated** — only incidental via real corpora | +| `NaturalClass`: `Segments` vs `FeatureNaturalClass`/`SegmentNaturalClass` precision | partially (`strrep-identity`) | +| Boundary markers (`CharacterDefinitionTable`) | covered (`loader/`) | +| `MorphemeCoOccurrenceRule`/`AllomorphCoOccurrenceRule` | covered (`cooccurrence/`) | +| MPR features/groups | covered (`mpr-groups/`) | +| Guesser/`LexicalGuess` | tracked separately (P11), not yet in this framework | +| Disjunctive allomorphs / free-fluctuation | covered (`allomorphy/`) | +| Stem names | covered (`realizational/stem-name`) | +| Tracing (`TraceType`) | out of `expected.tsv`'s scope entirely — separate mechanism (`trace_diff.py`) | + +Every row above currently has **zero negative fixtures and zero cross-cutting fixtures** — even +"covered" rows are covered only in the single-feature-positive sense. + +Capability note (§4.6): the `RewriteRule` (both modes) and `MetathesisRule` rows are `phonology` +capability — out of scope for XAmple. Everything else in the table is morphotactic and in principle +XAmple-eligible; F6 will confirm that per-construct rather than assuming it. + +## 7. What "coverage-complete enough" means + +Not literal 100% combinatorial coverage (the construct-pair space is too large to be worth +exhausting). The practical bar: +1. Every leaf construct in §6 has at least one `single-feature` fixture (positive) and at least one + `negative` fixture where a meaningful negative case exists for that construct. +2. Every construct-pair that HISTORY-MATRIX shows historically interacting badly in C#'s own commit + history gets a `cross-cutting` fixture. +3. A curated (not exhaustive) set of `pathological` fixtures, one per identified major complexity + driver (Simultaneous rewrite overlap, deep optional-affix nesting, compounding combinatorics, + disjunctive-slot explosion, template-slot backtracking). +4. The coverage report shows no construct at zero coverage. + +## 8. Growth policy: fixtures ride along with bug fixes + +The suite is designed to be cheap to extend, and the recommended (documented in +`conformance/README.md`, enforced by review culture rather than tooling) policy is: + +- **Every bug fix in a conforming engine lands with a conformance addition** — either a new minimal + fixture reproducing the bug, or new words in an existing fixture whose grammar already covers the + territory. The `manifest.json.provenance` field records the issue/PR/commit, and the word's + `words.yaml` entry says what the bug was, so the pin stays intelligible years later. This is + exactly how the existing 38 fixtures were born (each from a real divergence hit during the Rust + port) — the policy just makes that reactive pattern a standing expectation for *all* engines, + including C# HermitCrab's own future fixes and, post-F6, XAmple's. +- **The cheap path is deliberately cheap**: adding a word to an existing fixture is `words.txt` + + one `words.yaml` entry + regenerating `expected.tsv` via the oracle (`script.txt` documents the + exact invocation); a new fixture is a directory with the same five files, and the harness's + self-check mode immediately validates it. Neither requires touching the harness. +- Fixtures born from bugs default to `category: single-feature` or `negative`; if the bug was an + interaction, that's a `cross-cutting` fixture — the historical record (HISTORY-MATRIX) says + that's where the next bug is most likely hiding too. + +## 9. Acceptance criteria for the F0-F3 PR + +Verified during final-pass closeout (2026-07-10), against the live build on this branch +(`conformance-framework`, 13 commits ahead of `origin/master` at closeout time). Each item below is +stamped `DONE`, `N/A` (with the reason it doesn't apply to this repo/branch), or a corrected/qualified +status where verification found something to fix first. + +- **DONE** — `BatchCommand` builds on `master`-equivalent mainline + `src/SIL.Machine.Morphology.HermitCrab.Tool`. + Evidence: `src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs` exists and builds; `dotnet + build Machine.sln` succeeds (0 errors, 0 warnings), producing `hc.dll`. + +- **DONE** (one stale fixture found and fixed by this verification pass) — All 38 existing fixtures + live under `conformance/`, each with a `manifest.json`, each verified to still pass self-check mode + against the live C# oracle. + Evidence: `find conformance -name manifest.json | wc -l` → 41 (38 migrated + 3 F3). Self-check run + (`hc-conformance --fixtures conformance`) initially reported `39 passed, 1 failed` — + `rewrite/expand`'s checked-in `expected.tsv` recorded zero parses for `biiiibiiii`. Root cause + (see the fixture's own README for the full account): those frozen rows were generated by the + `.worktrees/parse-opt` oracle at `ccf750e6`, which carries the LT-22613 Gate B/`MaxAnalysisLength` + over-pruning bug in untraced mode; mainline HermitCrab was never affected and has always found the + `u`→`ii` expansion analysis, confirmed identically via both self-check and a real `hc.dll` + subprocess run. Regenerated `expected.tsv` via the documented `script.txt` invocation against + mainline (commit `f86895cc`). Re-run: `40 passed, 0 + failed, 0 skipped (of 40 attempted)` (1 pathological fixture excluded by default, see below); with + `--include-pathological`: `41 passed, 0 failed, 0 skipped (of 41 attempted)`, pathological fixture + at 2091ms against a 15000ms budget. + +- **DONE** — `conformance/PROTOCOL.md` documents the adapter contract precisely enough that a third + party could implement it without reading `BatchCommand.cs`. + Evidence: `conformance/PROTOCOL.md`, 7 sections (contract, TSV format, signature algorithm, + comparison semantics, capability profiles, per-engine grammar representation, the `hc.dll` + CLI-shape aside) — cites `BatchCommand.cs`/`BuildSignature` by section/paragraph, does not + reproduce or depend on its source. + +- **DONE, with the `hc-rs` half N/A** — The harness runs, self-check mode passes on all migrated + fixtures, and `--adapter` mode successfully validates against `hc-rs batch`. + Self-check half: **DONE**, see above (41/41 with `--include-pathological`). + `hc-rs` half: **N/A** — this branch has no `rust/` directory (per §4.1, this branch is based on + `origin/master` specifically because the Rust port will never land in this repo), so there is no + `hc-rs` binary anywhere reachable from this checkout to validate against. To still verify "the + adapter model actually works end-to-end" (the part of this criterion that doesn't depend on `hc-rs` + specifically), added `conformance/adapters/hc-dotnet-wrapper.sh` — the one-line wrapper + `PROTOCOL.md` §7 already anticipated as needed for exactly this case — and ran the full suite + through `--adapter` mode against it (the C# oracle via a real subprocess, not self-check's + in-process path): `40 passed, 0 failed, 0 skipped (of 40 attempted)`, including + `rewrite/simultaneous-epenthesis-cascade` correctly matching a live-oracle crash + (`InfiniteLoopException`) via the manifest-declared `expectCrash: true` contract (see + `FixtureManifest.ExpectCrash`, `EngineCrashException`, `Runner.cs`) — a review pass hardened this + from an earlier zero-rows inference to an explicit manifest field, verified to still FAIL (not + silently PASS) if the adapter can't even be started (a missing/bad `--adapter` binary). This + exercises the same subprocess/tokenize/tempfile/exit-code/diff plumbing `hc-rs` would use; only the + specific claim "the Rust port still conforms" is N/A, since there is no Rust port in this repo to + make that claim about. + +- **DONE** — At least one fixture in each of `negative`/`cross-cutting`/`pathological`, each with a + complete `words.md` (in practice `words.yaml`, per §10's open question, resolved to YAML). + Evidence: `conformance/negative/obligatory-tense-slot/words.yaml`, + `conformance/cross-cutting/disjunctive-tense-simultaneous-epenthesis/words.yaml`, + `conformance/pathological/deep-optional-affix-nesting/words.yaml` — all present and populated. Note: + this criterion, as literally written, only requires the 3 new-category fixtures to have one; the 38 + migrated fixtures deliberately do not have `words.yaml`/`words.md` (a scope decision recorded in + commit `f86f61b1`'s message) — goal 4's "every fixture" aspiration is not yet met for the migrated + 38, tracked as F4 follow-up work, not a gap in this criterion. + +- **DONE** — The coverage report runs and produces a real (if still mostly "gap") report against §6's + checklist. + Evidence: `hc-conformance --fixtures conformance --coverage-report` → 20 constructs tracked, 6 at + zero coverage (`Stratum rule order`, `AffixProcessRule: reduplication`, both + `MorphologicalOutputAction` rows, `Guesser/LexicalGuess`, `Tracing`) — matches §6's own documented + gap list exactly; no `UnknownConstructsInManifests` warnings, so every fixture's `constructs` values + resolve against `conformance/constructs.txt`. + +- **DONE** — Every migrated fixture's `manifest.json` carries an accurate `requires` field (derived + from its grammar's actual contents), and the runner demonstrably filters on it. + Evidence: `RequiresDerivation.Derive` + (`src/SIL.Machine.Morphology.HermitCrab.Conformance/RequiresDerivation.cs`) scans each + `grammar.xml` for `PhonologicalRule`/`MetathesisRule` elements, and `Runner` mechanically + validates every fixture's `manifest.json.requires` against it on every run (not just a one-off + cross-check script), FAILing with a manifest-error on mismatch — 0 mismatches across all 41 + fixtures, 21 deriving to `["phonology"]`. Filter run: `hc-conformance --fixtures conformance + --capabilities ""` → `19 passed, 0 failed, 21 skipped`; all 21 skips are exactly the + phonology-`requires` fixtures (15 `rewrite/*`, 3 `metathesis/*`, `affix-shapes/noncontiguous`, + `loader/n2-default-symbol`, `cross-cutting/disjunctive-tense-simultaneous-epenthesis`), each + reported `SKIP`, not `PASS`. + +- **N/A** — `rust/hc-parse/tests/*_conformance.rs` still pass, reading fixtures from the new path. + This branch has no `rust/` directory (§4.1: this branch is based on `origin/master`, not `rust`, + *because* the Rust port will never land in this repo) — there is no such test file to run or + update here. This criterion, as literally written, presupposes the Rust port living in this repo, + which contradicts §4.1's own stated premise; the corresponding §5 F2 bullet has been corrected + in-place (see above) to strike the same presupposition. Whichever repo eventually consumes + `conformance/` (the Rust port's own future repo, or elsewhere) is responsible for wiring its own + tests to wherever it pulls the directory in from — that's §5 F5's "document the pull-in process" + scope, not F0-F3's. + +## 10. Open questions (flag, don't block on) + +- **Resolved: `words.yaml`.** `words.md` vs `words.yaml` for the per-word/per-parse descriptions — + YAML is more reliably machine-parseable for potential future tooling; Markdown reads better for + humans browsing a fixture. Defaulted to YAML with a comment-friendly structure; all 3 F3 fixtures + ship one (the 38 migrated fixtures don't yet — tracked F4 backfill, see §5's F3 acceptance + criteria). Note this file is metadata only — the coverage report itself never reads it, keying + purely on `manifest.json.constructs` instead (see §4.2). +- Exact distribution mechanism for pulling `conformance/` into a future separate Rust repo (git + submodule vs. sparse checkout vs. tagged-release archive) — deferred per John's "let's not push + right now, we may be starting a new repo instead." +- Whether `manifest.json`'s `constructs` values should be free-text (grows organically) or a closed + enum validated by the harness (stricter, catches typos, but needs updating whenever a new construct + is added) — default to free-text validated against §6's checklist as a soft warning, not a hard + error, so a genuinely new construct can still be recorded before the checklist is updated. +- How F6's `grammar.xample/` parallels get authored: hand-written minimal AMPLE dictionaries/control + files (matches the hand-minimized spirit of the fixtures, but requires AMPLE-format fluency) vs. + round-tripping each fixture through a FieldWorks project and letting FieldWorks generate both + engines' inputs from one lexicon (guarantees the two grammars agree, but is heavyweight and pulls + FLEx into the authoring loop). Deferred to F6; nothing in F0-F3 depends on the answer. +- Whether XAmple analysis output maps losslessly onto the signature format's `morph+morph|shape` + tuples (allomorph shape reporting in particular) — to be settled empirically in F6 by running the + adapter on the first eligible fixture; if shapes can't be reproduced faithfully, the protocol may + need a per-engine "morphs-only signature" comparison level, declared in the engine registration. diff --git a/docs/conformance-language-suite-plan.md b/docs/conformance-language-suite-plan.md new file mode 100644 index 000000000..4980118bf --- /dev/null +++ b/docs/conformance-language-suite-plan.md @@ -0,0 +1,392 @@ +# Conformance suite v2 — typologically-selected synthetic languages + +**Status:** approved direction (John, 2026-07-11): hybrid layout. Roster grew 6 → 8 in response to +John's coverage question ("are all language features covered? should we add 1–5 more?") — see §3 +for the two additions and the rationale; the roster is in review. Supersedes the +fixture-per-mechanism layout of `docs/archive/conformance-framework-plan.md` (kept for history); the +adapter contract in `conformance/PROTOCOL.md` is unchanged. Nothing has shipped — PR #454 is a +draft — so this is a restructure of the same branch, not a migration users can observe. + +## 1. Why restructure + +The F0–F3 suite works (41 fixtures, self-check green, adapter mode green) but has three costs: + +1. **File sprawl.** 252 files; each fixture carries up to 7 (`grammar.xml`, `words.txt`, + `expected.tsv`, `script.txt`, `manifest.json`, `words.yaml`, `README.md`), with each word's + story smeared across four of them. +2. **Isolation is the wrong default.** Every migrated fixture is a hand-minimized single-mechanism + probe, yet `HISTORY-MATRIX.md` shows C#'s real bugs clustered in *interactions*. Cross-cutting + coverage exists only as a special category (1 fixture). +3. **Coverage follows the engine, not languages.** Fixtures are named after HermitCrab class names. + Six constructs sit at zero coverage because no bug ever happened to hit them; nothing about the + taxonomy pulls toward closing them. The suite's stated ambition — an all-language parser oracle — + is not reflected in its structure. + +## 2. The new shape + +A small set of **synthetic languages**, each simulating a named real language family chosen from +morphological typology, each dense enough that ordinary words exercise several constructs at once. +Plus a small **edge-cases** set for things no shared grammar can host. + +``` +conformance/ + README.md # the one doc: project, families, coverage philosophy, growth policy + PROTOCOL.md # adapter contract (unchanged semantics, trimmed prose) + constructs.txt # coverage keys (unchanged role) + coverage.csv # GENERATED: word × language × constructs exercised + languages// # exactly two files each: grammar.xml + words.yaml + edge-cases// # micro-grammars: loader probes, expectCrash, budgeted stress, unembeddable pins +``` + +Two files per fixture, ~40 files total (from 252). + +### 2.1 `words.yaml` is the ground truth + +One YAML per grammar replaces `words.txt` + `expected.tsv` + `script.txt` + `manifest.json` + +`words.yaml` + `README.md`: + +```yaml +language: Veyra # invented name; never a real language's name +inspired_by: ["Turkish (Turkic)", "Finnish (Uralic)"] +sources: ["WALS ch. 20/21", "..."] # the technical sources the simulation is grounded in +requires: [phonology] # capability profile, validated against grammar.xml as today +# edge-cases only: +# budget_ms: 15000 +# expect_crash: true +words: + - word: evlerimde + note: Backness harmony propagates across the whole suffix chain. + parses: + - signature: "EV+PL+1SG+LOC|evlerimde" + gloss: house-PL-1SG-LOC + rules: [mrPlural, mrPoss1sg, mrLocative, prBacknessHarmony] + exercises: + - "RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)" + - "Affix template slots (obligatory/disjunctive/ordering)" + - word: evlarimda # non-parse: the grammar must reject it + note: Harmony violation; must produce zero parses. + expect_fail: true + blocked_by: [prBacknessHarmony] + exercises: ["RewriteRule Iterative (epenthesis/deletion/feature/expansion/merge)"] + # optional on any word: provenance: "LT-XXXXX / PR link" (bug-driven growth policy) + - word: mekterinde # guess-stem parse: root deliberately absent from the lexicon + note: Only a guesser (LexicalGuess) analysis exists; pins guessed-stem behavior. + parses: + - signature: "+PL+LOC|mekterinde" + guess: true + rules: [mrPlural, mrLocative] + exercises: ["Guesser/LexicalGuess"] +``` + +Rules: + +- `parses[].signature` uses the existing signature algorithm — the comparison unit is unchanged. +- Signatures are **authored-and-verified, never blindly regenerated** (the LT-22613 lesson: the + oracle can be wrong). Self-check FAILs on mismatch; a `--propose` mode prints the would-be YAML + patch for a human to accept. No mode ever writes the file. +- `exercises` values are verbatim `constructs.txt` entries (unknown value = soft warning, as today). +- `rules` (per parse, **required**): every grammar rule — by its `grammar.xml` rule id — expected + to fire in that parse. This is the fine-grained coverage dimension under `exercises`' coarse one, + and it is *verified*, not aspirational: self-check runs the oracle with tracing enabled and FAILs + if the traced rule applications don't match the declared list. (Tracing stays engine-internal and + oracle-side only — the adapter contract and `expected.tsv` semantics are untouched.) +- Non-parses: `expect_fail: true` plus word-level `exercises`, and optionally `blocked_by` — the + rule id(s) whose correct behavior forbids the parse — so rejection coverage shows up in the rule + tables too. +- Skips: `expect_skip: true` (mutually exclusive with `expect_fail`) marks a word the oracle *skips* + rather than returning a genuine zero-parse "ok" — it throws `InvalidShapeException` (e.g. an + undeclared segment). The two differ in the adapter-contract status column ("SKIPPED" vs "ok"), so + they are distinct keys: self-check verifies the `InvalidShapeException` actually fired and adapter + mode materializes status "SKIPPED", keeping the two run modes consistent on the distinction. +- Guess-stem parses: `guess: true` marks an analysis produced via `LexicalGuess` (root deliberately + absent from the lexicon). The exact signature rendering of a guessed stem is pinned in G1 from + `BatchCommand`'s actual output and documented in PROTOCOL.md. +- `gloss` is **per parse, and optional**: a gloss describes one analysis, so an ambiguous word + carries one gloss per parse and an `expect_fail` word carries none. (A parse may also lack a + gloss where morpheme-by-morpheme glossing is meaningless, e.g. pathological stress words.) +- Every word needs a `note`; multi-parse words describe each parse. +- **Machine-parsable, human-readable — both are hard requirements** (tables are generated from + these files). Plain YAML 1.2 subset only: no anchors, aliases, merge keys, or custom tags; fixed + key vocabulary (schema-checked by the harness, unknown keys are errors); block style with one + word per entry so diffs and reviews stay line-oriented. +- The old category system dissolves: negative = `expect_fail` word; cross-cutting = the default + nature of a language grammar; pathological = an edge-case with `budget_ms`; single-feature = an + edge-case pin. + +### 2.2 The adapter contract does not change + +`PROTOCOL.md`'s CLI contract (` batch `) stays byte-for-byte. +The harness materializes `words.txt` and `expected.tsv` into a temp directory from `words.yaml` +before invoking an adapter, so existing adapters (including `adapters/hc-dotnet-wrapper.sh`) work +unmodified. The YAML is a repository format, not a wire format. + +## 3. Language selection (the research) + +Candidate pool of 32, drawn from morphological-typology surveys (WALS fusion/exponence/ +prefixing-suffixing/reduplication chapters; the metathesis, subtraction, and polysynthesis +literature), scored against the 20-row construct checklist. Full matrix in §3.1; the picks: + +| Grammar | Simulates | Carries (uniquely bold) | +|---|---|---| +| `templatic-semitic` | Arabic, Amharic | **`ModifyFromInput`/`InsertSimpleContext`** (zero today), stem names, OCP root co-occurrence, `RewriteRule` Simultaneous, epenthesis | +| `agglutinative-turkic` | Turkish, Finnish | Iterative long-distance harmony, gradation allomorphy, obligatory/disjunctive slots, natural-class precision, boundary markers | +| `bantu-verbal` | Swahili, Ndebele, Sena | Realizational rules, MPR features/groups, **verbal reduplication** (zero today), extension-slot ordering, nasal-prefix mutation (Celtic-style mutation coverage rides here) | +| `austronesian-phase` | Tagalog, Indonesian, Rotuman, Leti | Infix, circumfix, CV- + full reduplication, **`MetathesisRule`** (Rotuman phase), truncation (Rotuman deletion-phase; O'odham cited), **isolated `CopyFromInput`/`InsertSegments`** (zero today) | +| `polysynthetic-inuit` | Yup'ik, Kalaallisut | **Stratum ordering** (zero today) via derivation-then-inflection recursion, incorporation-style compounding, **Guesser/`LexicalGuess`** (zero today), seam epenthesis/deletion | +| `suffixing-quechua` | Cuzco Quechua | `requires: []` — the XAmple-eligible grammar: evidential chains, suffix co-occurrence, environment-conditioned allomorphs, obligatory slots | +| `prefixal-athabaskan` | Navajo | Position-class **prefixal** template with fused/discontinuous slot dependencies — the hardest documented template morphology; everything else in the roster is suffix-dominant, so template stress from the left edge is otherwise untested | +| `fusional-latin` | Latin, Russian; German ablaut | Inflection classes + syncretism (one affix, many features; many affixes, one cell), stem alternants as principal parts, ablaut/apophony as **sole** exponent — realizational machinery under fusional load, which `bantu-verbal`'s agglutinative use of the same rules never produces | + +The first six cover every in-scope checklist construct (infixation rides `austronesian-phase` via +Tagalog `-um-`); rows 7–8 exist because checklist coverage is not the same as *stress* coverage — +they re-exercise already-covered constructs under configurations (left-edge position classes, +fusional syncretism, affixless exponence) that no suffix-dominant or agglutinative grammar +produces. Five of the six zero-coverage constructs get natural homes; the sixth (`Tracing`) stays +explicitly out of scope (it never was in `expected.tsv`'s domain). Without `suffixing-quechua`, +XAmple-eligible coverage would drop from 19 fixtures to zero, killing plan-v1 goal 7 (XAmple-ready); +it is the designated morphotactics-pure grammar. + +**Future expansion tier** (documented trigger, not built now): Nilotic stem-internal-only +inflection (Nuer/Dinka — vowel grade + voice quality with almost no affixes), Kayardild/Warlpiri +case stacking (recursive suffix concord), Chukchi (circumfix + incorporation + harmony combined in +one grammar). Each enters as a new `languages/` member the day an engine bug or capability question +touches its territory — same growth policy as everything else. + +Grammars are synthetic: invented lexemes, invented (plausible) segment inventories, no orthographic +claims about any real language — `inspired_by` + `sources` say what phenomenon class each mechanism +simulates. A grammar may cite multiple family members (e.g. Rotuman for metathesis inside an +otherwise Philippine-style grammar) as long as every member is Austronesian-plausible. + +### 3.1 Candidate matrix (32) + +| # | Language (family) | Signature phenomena | Disposition | +|---|---|---|---| +| 1 | Turkish (Turkic) | vowel harmony, long suffix chains | **picked** → `agglutinative-turkic` | +| 2 | Finnish (Uralic) | harmony + consonant gradation | folded into 1 | +| 3 | Hungarian (Uralic) | harmony, definiteness conjugation | class covered by 1 | +| 4 | Kazakh (Turkic) | rounding harmony | class covered by 1 | +| 5 | Arabic (Semitic) | root-and-pattern, OCP root constraints | **picked** → `templatic-semitic` | +| 6 | Hebrew (Semitic) | templatic, weak-root deletions | folded into 5 | +| 7 | Amharic (Ethiosemitic) | templatic + affixal mix (HC reference corpus) | folded into 5 | +| 8 | Swahili (Bantu) | noun classes, verb template | **picked** → `bantu-verbal` | +| 9 | Ndebele (Bantu) | verbal reduplication | folded into 8 | +| 10 | Sena (Bantu) | HC reference corpus | folded into 8 | +| 11 | Tagalog (Austronesian) | infix `-um-`, CV-reduplication | **picked** → `austronesian-phase` | +| 12 | Indonesian (Austronesian) | circumfix `ke-…-an`, full reduplication | folded into 11 | +| 13 | Rotuman (Oceanic) | phase alternation: metathesis/umlaut/deletion | folded into 11 (metathesis + truncation carrier) | +| 14 | Leti (Austronesian) | phrase-conditioned metathesis | folded into 11 | +| 15 | Sierra Miwok (Utian) | templatic stem shapes, metathesis | skipped — covered by 13/14 | +| 16 | Navajo (Athabaskan) | position-class template, extreme fusion | **picked** → `prefixal-athabaskan` | +| 17 | C.A. Yup'ik (Eskimo-Aleut) | recursive derivational suffixation | **picked** → `polysynthetic-inuit` | +| 18 | Kalaallisut (Eskimo-Aleut) | extreme synthesis | folded into 17 | +| 19 | Mohawk (Iroquoian) | noun incorporation | folded into 17 (as compounding) | +| 20 | Chukchi (Chukotko-Kamchatkan) | incorporation + circumfix + harmony | skipped — overlaps 17/12/1 | +| 21 | Georgian (Kartvelian) | circumfixes, version vowels | skipped — circumfix via 12 | +| 22 | Welsh (Celtic) | initial consonant mutation | folded into 8 (nasal-prefix mutation) | +| 23 | Irish (Celtic) | mutation + eclipsis | skipped — same class as 22 | +| 24 | Tohono O'odham (Uto-Aztecan) | subtractive perfective truncation | folded into 11 (deletion-phase; cited in provenance) | +| 25 | Alabama (Muskogean) | subtraction, internal change | skipped — same class as 24 | +| 26 | Cuzco Quechua (Quechuan) | strictly-suffixing, regular, evidentials | **picked** → `suffixing-quechua` | +| 27 | Sanskrit (Indo-Aryan) | sandhi cascades, long-distance retroflexion | folded — long-distance rewrite via 1's harmony | +| 28 | Latin (Italic) | fusional classes, stem alternants, syncretism | **picked** → `fusional-latin` | +| 29 | German (Germanic) | circumfix `ge-…-t`, ablaut, compounding | folded — ablaut into 28, circumfix via 12, compounding via 17 | +| 30 | Warlpiri (Pama-Nyungan) | reduplication, case stacking | future tier — case stacking | +| 31 | Nuer (Nilotic) | stem-internal-only inflection (vowel grade, voice quality) | future tier | +| 32 | Kayardild (Tangkic) | extreme case stacking / suffix concord | future tier | + +## 4. Harness changes + +`SIL.Machine.Morphology.HermitCrab.Conformance` keeps its runner/self-check/adapter/coverage modes; +what changes is the fixture model: + +- **Discovery:** a fixture is any directory under `languages/` or `edge-cases/` containing + `grammar.xml` + `words.yaml`. `FixtureManifest` is replaced by the YAML front matter (parsed with + YamlDotNet, a new test-side dependency). +- **Self-check:** per word, run the engine, build the signature set, compare against the set of + `parses[].signature` (empty set for `expect_fail`). Same order-independent semantics as today. +- **Adapter mode:** materialize `words.txt`/`expected.tsv` per fixture into temp, then the existing + subprocess/diff flow runs unchanged. +- **`requires` validation:** `RequiresDerivation` is unchanged — front-matter `requires` is still + mechanically re-derived from `grammar.xml` every run, mismatch FAILs. +- **Coverage:** emits two generated tables, both committed and regenerable (CI can diff for + freshness): `conformance/coverage.csv` (language, word, parse signature, construct) and + `conformance/rules.csv` (language, rule id, exercising words — including `blocked_by` + attributions). The harness enumerates every rule id in each `grammar.xml` and flags **dead + rules** (rules no word exercises) — a mechanical detector for authoring gaps inside a grammar, + parallel to what `constructs.txt` does across grammars. Console rollup stays. +- **Traced verification of `rules`:** self-check runs the oracle with tracing on and FAILs a parse + whose actual rule applications diverge from its declared `rules` list. +- **`--propose`:** on signature mismatch, print the YAML patch that would reconcile; never write. +- **Edge-case semantics:** `expect_crash` and `budget_ms` move from manifest to front matter, + behavior unchanged (crash contract, post-hoc + enforced timeout budgets, `--include-pathological` + gating keyed on `budget_ms` presence). + +## 5. Migration: the ledger + +Every one of the 41 existing fixtures gets a row in `docs/conformance-migration-ledger.md` +(retained permanently — it is the provenance bridge) recording: old fixture id, its constructs, its +destination (language + word(s), or `edge-cases/`), and how the bug-triggering configuration +was preserved. Hard rules: + +1. **No construct's coverage may regress.** The old suite's construct→fixture map is the floor. +2. **A pin that can't embed faithfully stays a micro-grammar.** Expected residents of `edge-cases/`: + the three `loader/` XML-semantics probes, `rewrite/simultaneous-epenthesis-cascade` + (`expect_crash`), `pathological/deep-optional-affix-nesting` (`budget_ms`), and any allomorphy/ + rewrite pin whose exact rule configuration a naturalistic grammar won't contain (candidates: + `disjunctive-recheck`, `strrep-identity`, `n2-default-symbol`-class cases — the ledger decides + per fixture, embedding is not forced). +3. **Old `manifest.json.provenance` moves into per-word `provenance:` fields.** Nothing about a + bug's history is lost, including `rewrite/expand`'s LT-22613 account. +4. The old fixture tree is deleted only when the ledger is complete and §7's gates pass. + +## 6. Growth policy (restated for v2) + +Unchanged in spirit: **every engine bug fix lands with a conformance addition.** The cheap path is +now even cheaper — one YAML entry (word + note + parses + `provenance`) in the language whose +grammar already covers the territory, verified by self-check. If no language's grammar can express +the trigger, either extend a language grammar (preferred when family-plausible) or add an +`edge-cases/` micro-grammar. New typological territory (the §3 future tier — Nilotic stem-internal +inflection, case stacking, Chukotkan) enters as a whole new `languages/` member with its own +`sources`. + +## 7. Phases and acceptance gates + +- **G1 — Pilot.** Harness rework (YAML fixture model, temp materialization, CSV, `--propose`) + + convert one language end-to-end (`suffixing-quechua`, smallest and phonology-free) + two + edge-case conversions (`loader/n1-isactive`, the `expect_crash` cascade). G1 also pins the + guessed-stem signature rendering (from `BatchCommand`'s real output, documented in PROTOCOL.md) + and implements traced `rules` verification. Gate: self-check and adapter mode green on the pilot; + traced-rules verification demonstrably FAILs on a wrong `rules` list; `--propose` demonstrably + never writes; broken-adapter and mistagged-`requires` negative tests still FAIL. +- **G2 — Author + migrate.** Write the seven remaining grammars (each: hand-derive expected parses + for ≥5 words *before* consulting the oracle, then self-check the rest); complete the ledger; + delete the old tree. Gate: ledger complete with zero coverage regression; all fixtures green in + both modes. +- **G3 — Docs consolidation.** One `conformance/README.md` (project, the six families, coverage + philosophy, growth policy — target ≤120 lines); PROTOCOL.md trimmed (contract untouched); + `conformance-framework-plan.md` + `HISTORY-MATRIX.md` archived under `docs/archive/` (live + content already moved into ledger/provenance fields); `constructs.txt` header updated. +- **G4 — Verify + PR.** Full run both modes; coverage.csv shows every construct except Tracing + covered (including the five newly-closed); rules.csv shows zero dead rules in every grammar; + every language has at least one non-parse word and the suite has at least one guess-stem word; + file count ≤ ~45, HermitCrab test suite still green; + update PR #454 in place (still draft) with the restructure story. +- **G5 — Coverage-driven hardening.** `constructs.txt`/`coverage.csv`/`rules.csv` measure whether + every documented *construct* and grammar *rule* is exercised, but say nothing about how much of + `SIL.Machine.Morphology.HermitCrab`'s actual source the suite reaches. Baseline (2026-07-13, + `dotnet-coverage collect --output-format cobertura` around a full `--include-pathological` + self-check run): 69.2% line coverage (5,056/7,306 lines); excluding `XmlLanguageWriter` (984 + lines, 0% — serialization, never exercised because the harness only loads grammars) it's 79.97% + (5,056/6,322). Two threads close real gaps without expanding the suite's scope: + 1. **New words in existing grammars**, not a new language (per §6: this is closing gaps in + mechanisms the existing roster could already plausibly need, not new typological territory), + exercising four `grammar.xml` mechanisms every current language leaves at zero: + - Syntactic feature agreement (`RequiredHeadFeatures`/`AssignedHeadFeatures`/ + `RequiredFootFeatures` on `MorphologicalRule`/`RealizationalRule`/`CompoundingRule`) — + natural home: `bantu-verbal` (noun-class agreement) or `agglutinative-turkic`. + - Alpha-variable phonological environments (`AlphaVariables`/`VariableFeature`) — natural + home: `agglutinative-turkic` (vowel harmony is the textbook alpha-variable use case). + - Optional-group and Kleene-star `CharacterDefinitionTable` pattern syntax (`([C])`, `[V]*`) + in a NaturalClass shape pattern — any language with an optional coda/onset works. + - Compounding constraints exercising `MaxApplicationCount`, non-head/obligatory syntactic + features, and `Blockable` retry — natural home: `polysynthetic-inuit` or `fusional-latin`, + both of which already have compounding. + 2. **Targeted coverage chase, file by file**, toward as-close-to-100% as the parse-only harness + contract allows, re-measured with the same `dotnet-coverage` invocation after each addition: + - `XmlLanguageLoader` (87.7% baseline, 131/1067 lines uncovered) — closed by the feature- + agreement and alpha-variable additions above, plus any other unreached `grammar.xml` + element/attribute branch a coverage pass turns up. + - `CharacterDefinitionTable` (59.7% baseline) — closed by the optional-group/Kleene-star + addition above; sweep remaining uncovered lines after. + - `SynthesisCompoundingRule` (62.0% baseline) — closed by the compounding-constraints + addition above. + - Any other class the post-addition run still shows with meaningful uncovered lines + (`SynthesisAffixProcessRule`, `SynthesisRealizationalAffixProcessRule`, + `SyntacticFeatureSystem`, `AllomorphEnvironment`, and `MprFeatureSet` were all secondary + gaps at baseline) gets the same treatment: read the uncovered lines, decide whether a word + or grammar addition can reach them, add it, re-measure. + Explicitly out of scope (see §8): `Morpher.GenerateWords`/`AnalyzeWord`/`PermuteRules`/ + `PermuteOtherMorphemes` (a word-generation/analysis API the parse-only harness never calls, + by contract) and `XmlLanguageWriter` (serialization, never exercised by a load-only harness) + — closing these means testing a different capability, not adding fixture data. + Gate: `dotnet-coverage` line coverage on every file named above at or near 100% (any residual + gap is a defensive branch/exception path genuinely unreachable through the `grammar.xml` + + `words.yaml` surface, and is documented inline as such); zero regression in construct/rule + coverage; self-check and adapter mode still green. + + **Status (2026-07-13): first pass done.** All four planned mechanisms landed and verified against + the real oracle (self-check green 16/16, adapter mode green 15/15, construct coverage 23/23 — + four new construct rows added to `constructs.txt` for the mechanisms themselves, `HermitCrab` + test suite still 67/67): + - **Syntactic feature agreement** (`bantu-verbal`, posV6): `RequiredHeadFeatures`/ + `OutputHeadFeatures`/`RequiredFootFeatures`/`OutputFootFeatures` on a new `mrNumPl`→`mrAgr` + chain, plus `AssignedHeadFeatures`/`AssignedFootFeatures` on a negative-control root (`kes`) and + a new top-level `` section (previously declared by zero grammars in the suite). + Also closed `outputObligatoryFeatures` for free (loading the attribute exercises the loader + regardless of runtime effect, which is `Morpher.GenerateWords`-only and out of scope). + - **Alpha-variable phonological environments** (`agglutinative-turkic`, posVAlpha): a new + `prAlphaHighHarmony` rule with its own ``/`` — a genuine + generative-phonology `[αback]...[αback]` agreement rule, isolated from the grammar's existing + (enumerated-subrule) harmony so nothing already-pinned changed. Both predictions (`satun`, + `setin`) matched the oracle on the first run. + - **`CharacterDefinitionTable` optional-group pattern** (`edge-cases/loader-pattern-shapes`): a + second lexical entry `"b([Vowel])t"` alongside the existing bracket-only `"b[Vowel]t"`. + Reconciliation finding: the LOADER builds the optional-annotated shape node regardless (so the + loader branch is exercised either way), but root-allomorph ANALYSIS matching does not actually + honor the Optional annotation at runtime — `"bt"` (vowel absent) gets zero parses rather than + matching, unlike a phonological environment match. Pinned as `expect_fail`, documented as a + genuine engine finding rather than corrected to fit the original guess. + - **`CompoundingRule` constraints** (`fusional-latin`, posCompH/posCompNH): a new + `mrCompoundConstrained` exercising `headProdRestrictionsMprFeatures`, per-subrule + `requiredMPRFeatures`/`excludedMPRFeatures` on `HeadMorphologicalInput`, and a real + `Blockable`/`CheckBlocking` trigger (`famCompBlock`: `GEN` blocked by suppletive `NOV`, the + first time this suite's Blockable path is exercised through a *compound* rather than an + ordinary `MorphologicalRule`). Reconciliation finding: `outputObligatoryFeatures` on a + `CompoundingRule` silently zeroes every parse unless the SAME rule also assigns that feature a + value via `OutputHeadFeatures` (an unset-but-obligatory feature fails a completeness check the + parse-only harness never otherwise surfaces) — the general lesson (confirmed against + `bantu-verbal`'s `mrNumPl`, which self-satisfies the same way) is now recorded here for whoever + uses `outputObligatoryFeatures` next. + + Coverage moved from 69.2%→71.0% line / 62.8%→65.1% branch overall + (`SIL.Machine.Morphology.HermitCrab`, `dotnet-coverage` cobertura, self-check + + `--include-pathological`): `XmlLanguageLoader` 87.7%→91.1%, `CharacterDefinitionTable` + 59.7%→65.1%, `SynthesisCompoundingRule` 62.0%→75.6%. `SyntacticFeatureSystem` held at 44.4% — + traced to `Clear`/`Remove`/the single-arg `Add(Feature)` override/the `HeadFeatures`/`FootFeatures` + enumerable getters, none of which the loader or `Morpher` ever call (the loader always uses the + 2-arg `Add(feature, type)`); this is the same class of genuinely-unreachable API surface as + `CharacterDefinitionTable`'s `ICollection` plumbing (`Clear`/`Remove`/non-generic + `GetEnumerator`/`IsMatch`/`TrySegment`, the last two reachable only from `Morpher.IsWordValid`, + itself `GenerateWords`-only). + + **Remaining for a future pass** (not yet attempted or attempted-and-reverted this round): + `SynthesisCompoundingRule` still has `MaxApplicationCount` (51–62), `NonHeadRequiredSyntacticFeatureStruct` + failure (86–97), the `ExcludedMprFeatures`-failure branch (structurally unreachable from a + 2-subrule required/excluded-on-the-same-feature design — a 3rd subrule gated on a *different* + feature would be needed), and `HeadPattern`/`NonHeadPattern` match-failure tracing (209–224). + `fusional-latin`'s `mitlavlav` word was hand-derived to hit `MaxApplicationCount` via recursive + (compound-of-compound) analysis and correctly gets zero parses, but a coverage check after adding + it showed the `MaxApplicationCount` lines still unreached — most likely analysis-direction + compound-splitting never recurses into a derived (non-lexical) head span at all, so the rejection + happens earlier for an unconfirmed reason; the word is kept as a correct pin but the note no + longer claims `MaxApplicationCount` attribution. `SynthesisAffixProcessRule` (72.6%) and + `SynthesisRealizationalAffixProcessRule` (63.8%) have the same `MaxApplicationCount`/ + `RequiredSyntacticFeatureStruct`/`Blockable` shape as `SynthesisCompoundingRule` did before this + pass and are good candidates for the same treatment. `AllomorphEnvironment` (62.4%) is unexplored. + +Authoring model per John's standing pattern: Sonnet subagents write (G1 harness, G2 grammars, G5 +additions), Fable reviews each gate. + +## 8. Non-goals + +- No change to the adapter protocol, signature algorithm, or comparison semantics. +- No XAmple execution (still deferred; `suffixing-quechua` preserves *readiness*). +- No claim of documentary accuracy about any real language — synthetic grammars simulate phenomenon + classes, with sources cited per grammar. +- Not replacing corpus-level parity testing (Sena/Indonesian/Amharic goldens) wherever the Rust + port lives. +- Not chasing code coverage in `Morpher`'s word-generation/analysis API (`GenerateWords`/ + `AnalyzeWord`/`PermuteRules`/`PermuteOtherMorphemes`) or in `XmlLanguageWriter` (grammar + serialization) — see G5. Both are real capabilities of `SIL.Machine.Morphology.HermitCrab`, but + neither is reachable through `ParseWord`, which is all a parse-only conformance harness calls; + closing that gap would mean testing a different capability, not adding fixture data. diff --git a/docs/conformance-migration-ledger.md b/docs/conformance-migration-ledger.md new file mode 100644 index 000000000..c5a3d8cc0 --- /dev/null +++ b/docs/conformance-migration-ledger.md @@ -0,0 +1,113 @@ +# Conformance v1 → v2 migration ledger + +Every v1 fixture maps to a v2 destination. Rules (spec §5): no construct's coverage may regress; +a pin that can't embed faithfully stays an `edge-cases/` micro-grammar; `manifest.json.provenance` +moves into per-word `provenance:` fields; the v1 tree is deleted only after the G4 parity proof. + +**XAmple floor** (column X): fixtures with `requires: []` in v1. Their construct coverage must land +in a `requires: []` destination — `suffixing-quechua`, `fusional-latin` (deliberately authored +phonology-free: ablaut/stem alternants via `ModifyFromInput`/lexical allomorphs, which derive +morphotactic), or a phonology-free edge-case. Where the primary destination is a phonology grammar, +a second requires-[] home is listed (words are cheap; the grammar is the expensive part). + +| v1 fixture | X | Destination | Notes on faithful embedding | +|---|---|---|---| +| affix-shapes/circumfix | ✓ | austronesian-phase (`ke-…-an`) **+ fusional-latin** (`ge-…-t`) | Latin home preserves XAmple floor | +| affix-shapes/infix | ✓ | austronesian-phase (`-um-`) **+ fusional-latin** (nasal infix `vi-n-c-`) | Latin nasal infix is genuinely attested | +| affix-shapes/noncontiguous | — | austronesian-phase | already phonology; noncontiguous affix + rewrite interplay | +| affix-shapes/truncate | ✓ | austronesian-phase (Rotuman deletion phase) | XAmple floor: no natural requires-[] home → **edge-cases/truncate-morphotactic** duplicate | +| allomorphy/discontinuous-env | ✓ | agglutinative-turkic | env across intervening segments ≈ harmony-adjacent conditioning | +| allomorphy/disjunctive-recheck | ✓ | **edge-cases/disjunctive-recheck** | deferred-recheck engine behavior too specific to embed (spec §5 names it) | +| allomorphy/strrep-identity | ✓ | **edge-cases/strrep-identity** | feature-less-grammar StrRep identity pin (spec §5 names it) | +| compounding/nonhead-not-root | ✓ | polysynthetic-inuit **+ fusional-latin** (Latin/Germanic compounds) | Latin home preserves XAmple floor. Landed in fusional-latin: the compounding join marker "+" must be a `BoundaryDefinition` (CharacterDefinitionTable), not a plain `SegmentDefinition` — declaring it as a segment produced zero parses for every compound word (v1's own grammar.xml already does this correctly; worth checking before polysynthetic-inuit's own compounding lands). | +| compounding/prefix-commute | ✓ | polysynthetic-inuit **+ fusional-latin** | same. Also landed in fusional-latin: if two `CompoundingRule`s coexist in one grammar with unrestricted head/nonhead wildcards, each must be scoped to a disjoint `headPartsOfSpeech`/`nonHeadPartsOfSpeech` pair or they double-count each other's words. | +| cooccurrence/allomorph-basic | ✓ | suffixing-quechua | allomorph co-occurrence in suffix chain | +| cooccurrence/and-semantics-pin | ✓ | suffixing-quechua | AND-not-OR semantics (LT-22156) — provenance must carry | +| cooccurrence/morpheme-adjacency | ✓ | suffixing-quechua | all 5 adjacency kinds across the evidential chain | +| cross-cutting/disjunctive-tense-simultaneous-epenthesis | — | agglutinative-turkic | disjunctive tense slots + simultaneous epenthesis, Turkic-plausible | +| loader/n1-isactive | ✓ | **edge-cases/loader-isactive** (G1, done) | XML-loader semantics, not language facts | +| loader/n2-default-symbol | — | **edge-cases/loader-default-symbol** | same | +| loader/n3-pattern-shapes | ✓ | **edge-cases/loader-pattern-shapes** | same | +| metathesis/simple_rule | — | austronesian-phase | Rotuman/Leti phase alternation | +| metathesis/complex_rule | — | austronesian-phase | fallback edge-case if exact rule shape won't embed | +| metathesis/not_unapplied | — | austronesian-phase | pins non-unapplication; fallback edge-case | +| mpr-groups/output-overwrite | ✓ | bantu-verbal **+ fusional-latin** (conjugation classes as MPR) | Latin home preserves XAmple floor. Landed in fusional-latin: `MorphologicalPhonologicalRuleFeatureGroup` genuinely needs `morphologicalRuleOrder="linear"` to be observable (per v1's own note) — if that same stratum also needs `"unordered"` for some other construct (e.g. a CompoundingRule's head-recursion), split into two `Stratum` elements sharing one `CharacterDefinitionTable` rather than forcing one order on both; worth checking before bantu-verbal's own MPR-groups land alongside its other constructs. | +| mpr-groups/required-all | ✓ | bantu-verbal **+ fusional-latin** | same | +| negative/obligatory-tense-slot | ✓ | agglutinative-turkic | the F3 example is already Turkic-shaped (tun-di/tun-ta) | +| pathological/deep-optional-affix-nesting | ✓ | **edge-cases/deep-optional-affix-nesting** | budget_ms fixture, isolation required | +| realizational/family-blocking | ✓ | fusional-latin | LexFamily blocking is fusional-paradigm territory | +| realizational/realizational-rule | ✓ | bantu-verbal **+ fusional-latin** | core mechanics in both; Latin preserves floor | +| realizational/stem-name | ✓ | templatic-semitic (binyan grades) **+ fusional-latin** (principal parts) | Latin preserves floor | +| rewrite/deletion-reinsertion | — | polysynthetic-inuit | seam deletion under reinsertion | +| rewrite/disjunctive | — | agglutinative-turkic | disjunctive subrules ≈ gradation contexts | +| rewrite/expand | — | templatic-semitic | segment expansion ≈ spreading/gemination; **LT-22613 provenance must carry verbatim** | +| rewrite/longdistance | — | agglutinative-turkic | harmony is the canonical long-distance rule | +| rewrite/merge | — | agglutinative-turkic | vowel coalescence at seams (Uralic-plausible); history row 1 provenance | +| rewrite/multiplemerge | — | agglutinative-turkic | same | +| rewrite/multiplesegment | — | polysynthetic-inuit | multi-segment seam alternations | +| rewrite/multiplesegment-deletion-composition | — | polysynthetic-inuit | group-capture composition pin; fallback edge-case | +| rewrite/quantifier | — | agglutinative-turkic | quantified contexts = harmony transparency spans | +| rewrite/required-pos-subrule | — | bantu-verbal | verb-only phonology is Bantu-plausible | +| rewrite/simultaneous-epenthesis | — | templatic-semitic | Arabic-style epenthesis, Simultaneous mode | +| rewrite/simultaneous-epenthesis-cascade | — | **edge-cases/simultaneous-epenthesis-cascade** (G1, done) | expect_crash isolation | +| rewrite/simultaneous-feeding | — | templatic-semitic | paired Simultaneous/Iterative contrast — embed as two rules on distinct segments; fallback: keep pair as one edge-case | +| rewrite/simultaneous-feeding-control-iterative | — | templatic-semitic | control sibling of the above, same fallback | +| rewrite/word-initial-epenthesis | — | templatic-semitic | Arabic prothesis (ʔi- before clusters) — perfect family fit | + +## Cross-cutting findings from G2a (agglutinative-turkic, fusional-latin) + +Two engine/harness facts surfaced while authoring the first two multi-construct v2 grammars that +will affect every language still to come (templatic-semitic, bantu-verbal, austronesian-phase, +polysynthetic-inuit, prefixal-athabaskan) — not fixture-specific, so recorded here rather than on +any one row: + +1. **Any phonological subrule (or `ModifyFromInput`) that CHANGES a segment's identity needs a + fully-specified `PhonologicalFeatureSystem`** where every segment in the `CharacterDefinitionTable` + has an explicit value for every declared feature, and every segment's combination is unique. A bare + `SegmentNaturalClass`/literal `Segment` target or output for such a subrule is analysis-unreachable + in self-check (confirmed by isolated probing: always zero parses, regardless of anchoring or an + explicit `Environment`); a partially-specified feature table renders ambiguous bracketed character + classes even for segments no rule ever touches. A `SegmentNaturalClass`/literal segment remains fine + for IDENTITY subrules (no actual change) and for `Environment` conditions (never the position being + changed). See `agglutinative-turkic/words.yaml`'s header and `fusional-latin/grammar.xml`'s + `PhonologicalFeatureSystem` comment for the full derivation. +2. **Self-check's traced `rules:` verification originally could not distinguish "this phonological + rule fired with effect on this word" from "this phonological rule's stratum pass merely ran"** — + confirmed by an isolated 2-rule probe where a word containing neither rule's target segment still + traced both rules as applied. **Fixed in the G2a review pass:** the engine's `TraceManager` records + both applied (`FailureReason.None`) and not-applied (`FailureReason` set) attempts as + `PhonologicalRuleSynthesis` trace nodes, and `TraceRuleAttributor` was counting both kinds; + filtering to `FailureReason.None` makes per-word phonological-rule attribution exact. + `agglutinative-turkic`'s `rules:` lists were narrowed to the truthful per-word sets, which matched + its header's hand derivations exactly. Later grammars declare only the rules that actually fire. + +## Floor summary (G4 parity check keys) + +- Per-construct: every construct with ≥1 v1 fixture must have ≥1 v2 word exercising it. +- XAmple floor: every construct covered by a requires-[] v1 fixture must be exercised by ≥1 word in + a requires-[] v2 grammar (quechua, latin, athabaskan, or a phonology-free edge-case). +- Provenance: every v1 `manifest.json.provenance` string must appear in some v2 `provenance:` field + (mechanically greppable). + +## G2e parity proof (mechanical, run before deleting the v1 tree) + +`conformance/parity-check.py` mechanically enforces all three floors above plus an absolute +construct-coverage check, and exits nonzero on any violation. It self-validates that it discovered +exactly 41 v1 fixtures before certifying. Run it any time with `python conformance/parity-check.py`. +The passing run that gated the v1 deletion: + +``` +parity-check: discovered 41 v1 fixture(s), 15 v2 grammar(s) in coverage.csv, 41 v2 provenance string(s), 20 constructs.txt entries +[PASS] A. provenance: 41/41 v1 strings carried verbatim +[PASS] B. per-construct floor: 14/14 v1 constructs covered in v2 +[PASS] C. XAmple floor: 11/11 requires:[] constructs have a requires:[] v2 home +[PASS] D. absolute coverage: 19/19 in-scope constructs covered ('Tracing (TraceType)' out of scope by design) +parity-check: ALL CHECKS PASSED -- v1 coverage is fully preserved in v2; safe to delete the v1 tree. +``` + +The proof caught one real gap during authoring: the G1 pilot `suffixing-quechua` (authored before the +provenance discipline) had never carried the three `cooccurrence/*` fixtures' provenance and did not +genuinely exercise their sub-behaviours; it was enriched (AllomorphCoOccurrenceRule, all five +adjacency kinds, the LT-22156 two-rule AND pin — all morphotactic, `requires: []` preserved) until the +proof went green. After deletion the v1→v2 checks (A/B/C) report "migration complete" and only the +absolute check D runs; D is the permanent G4 mechanism (also emitted by `--coverage-report`). diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/AdapterEngine.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/AdapterEngine.cs new file mode 100644 index 000000000..c354a0296 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/AdapterEngine.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Runs a fixture against an external engine adapter process, per the contract in +/// conformance/PROTOCOL.md section 1: <engine-binary> batch <grammar> <words.txt> +/// <output.tsv>. The command template is supplied by the caller (--adapter) with +/// {grammar}/{words}/{output} placeholders, e.g.: +/// "C:\...\hc-rs.exe batch {grammar} {words} {output}". +/// +public class AdapterEngine : IEngine +{ + private readonly List _templateTokens; + private readonly int _timeoutMs; + + public AdapterEngine(string commandTemplate, IReadOnlySet capabilities, int timeoutMs = 5 * 60 * 1000) + { + // Tokenize + validate the template once here, not per fixture run: a bad/empty template is + // a configuration error that should fail fast, not surface 41 times over. Tokenization + // itself is shared with the Tool project's own command-line parsing (see + // Program.SplitCommandLine's doc comment) rather than a second, divergent copy. + _templateTokens = new List(SIL.Machine.Morphology.HermitCrab.Program.SplitCommandLine(commandTemplate)); + if (_templateTokens.Count == 0) + throw new InvalidOperationException("empty --adapter command template"); + _timeoutMs = timeoutMs; + Capabilities = capabilities; + } + + public string Name => "adapter"; + + public IReadOnlySet Capabilities { get; } + + public List Run(MaterializedFixture fixture) + { + string outputPath = Path.Combine(Path.GetTempPath(), "hc-conformance-" + Guid.NewGuid().ToString("N") + ".tsv"); + try + { + var tokens = new List(_templateTokens.Count); + foreach (string token in _templateTokens) + { + tokens.Add( + token + .Replace("{grammar}", Path.GetFullPath(fixture.GrammarPath)) + .Replace("{words}", Path.GetFullPath(fixture.WordsPath)) + .Replace("{output}", outputPath) + ); + } + + var psi = new ProcessStartInfo + { + FileName = tokens[0], + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + for (int i = 1; i < tokens.Count; i++) + psi.ArgumentList.Add(tokens[i]); + + using Process process = + Process.Start(psi) + ?? throw new InvalidOperationException($"failed to start adapter process '{tokens[0]}'"); + + // Start draining both streams asynchronously BEFORE waiting for exit: reading them + // synchronously (ReadToEnd) first would block forever on a child that fills its + // stderr/stdout buffer before exiting, making the timeout below unreachable. + Task stdoutTask = process.StandardOutput.ReadToEndAsync(); + Task stderrTask = process.StandardError.ReadToEndAsync(); + + // A fixture's own manifest budget (pathological fixtures) takes over the process + // timeout for that one run instead of the default 5 minutes, so a runaway adapter gets + // killed at the same wall-clock bound MaterializedRunner enforces, not five minutes later. + int effectiveTimeoutMs = + fixture.Manifest.Budget != null ? (int)fixture.Manifest.Budget.WallClockMs : _timeoutMs; + + bool exited = process.WaitForExit(effectiveTimeoutMs); + if (!exited) + { + try + { + process.Kill(entireProcessTree: true); + } + catch (InvalidOperationException) { } + throw new TimeoutException( + $"adapter process for fixture '{fixture.Id}' did not exit within {effectiveTimeoutMs}ms" + ); + } + + string stdout = stdoutTask.GetAwaiter().GetResult(); + string stderr = stderrTask.GetAwaiter().GetResult(); + + if (process.ExitCode != 0) + { + // The engine under test itself failed (as opposed to a harness-level problem like + // a bad --adapter template, a hung process, or exiting 0 with no output) -- see + // EngineCrashException's doc comment for the PASS/FAIL contract this feeds MaterializedRunner. + throw new EngineCrashException( + $"adapter process exited {process.ExitCode} for fixture '{fixture.Id}'.\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); + } + if (!File.Exists(outputPath)) + { + throw new InvalidOperationException( + $"adapter process for fixture '{fixture.Id}' did not produce its output file.\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); + } + return SignatureTsv.ReadFile(outputPath); + } + finally + { + try + { + if (File.Exists(outputPath)) + File.Delete(outputPath); + } + catch (IOException) { } + } + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/CoverageReport.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/CoverageReport.cs new file mode 100644 index 000000000..9b7e000cd --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/CoverageReport.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Emits the two coverage tables described in docs/conformance-language-suite-plan.md section 4: +/// conformance/coverage.csv (language, word, parse signature, construct -- one row per +/// parse×construct, expect_fail words getting an empty signature) and conformance/rules.csv +/// (language, rule id, exercising words -- semicolon-joined, blocked_by attributions marked +/// with a "!" prefix), plus dead-rule detection: every rule id finds +/// in a grammar.xml that no word's "rules:"/"blocked_by" ever names. +/// +public static class CoverageReport +{ + public class DeadRule + { + public string FixtureId = ""; + public string Language = ""; + public string RuleId = ""; + } + + public class CoverageResult + { + public List DeadRules { get; } = new(); + + /// Every construct string any word exercises (the distinct construct column + /// of coverage.csv). Diffed against constructs.txt by the caller to report absolute + /// construct coverage. + public HashSet CoveredConstructs { get; } = new(StringComparer.Ordinal); + } + + public static List LoadConstructChecklist(string path) + { + var constructs = new List(); + foreach (string rawLine in File.ReadAllLines(path)) + { + string line = rawLine.Trim(); + if (line.Length == 0 || line.StartsWith("#")) + continue; + constructs.Add(line); + } + return constructs; + } + + public static CoverageResult WriteCsvs(List fixtures, string coverageCsvPath, string rulesCsvPath) + { + var result = new CoverageResult(); + var coverageRows = new List<(string language, string word, string signature, string construct)>(); + var ruleRows = new List<(string language, string ruleId, string words)>(); + + foreach (Fixture fixture in fixtures) + { + string language = fixture.Words.Language; + GrammarRuleIndex ruleIndex = GrammarRuleIndex.Load(fixture.GrammarPath); + + // rule id -> exercising word tokens ("word" for a positive rules: hit, "!word" for a + // blocked_by attribution on an expect_fail word). + var exercisingWords = new Dictionary>(StringComparer.Ordinal); + void AddExercise(string ruleId, string token) + { + if (!exercisingWords.TryGetValue(ruleId, out List list)) + { + list = new List(); + exercisingWords[ruleId] = list; + } + list.Add(token); + } + + foreach (WordEntry word in fixture.Words.Words) + { + if (word.ExpectFail || word.ExpectSkip) + { + // expect_fail and expect_skip words both carry word-level exercises and no parses; + // only expect_fail words carry blocked_by (a skip is malformed input, not a + // rule-forbidden analysis), but iterating the empty list is harmless either way. + foreach (string construct in word.Exercises) + { + coverageRows.Add((language, word.Word, "", construct)); + result.CoveredConstructs.Add(construct); + } + foreach (string ruleId in word.BlockedBy) + AddExercise(ruleId, "!" + word.Word); + } + else + { + foreach (ParseEntry parse in word.Parses) + { + IEnumerable constructs = parse + .Exercises.Concat(word.Exercises) + .Distinct(StringComparer.Ordinal); + foreach (string construct in constructs) + { + coverageRows.Add((language, word.Word, parse.Signature, construct)); + result.CoveredConstructs.Add(construct); + } + foreach (string ruleId in parse.Rules) + AddExercise(ruleId, word.Word); + } + } + } + + foreach ( + string ruleId in ruleIndex + .AllRuleIds.Distinct(StringComparer.Ordinal) + .OrderBy(x => x, StringComparer.Ordinal) + ) + { + if (exercisingWords.TryGetValue(ruleId, out List words)) + { + ruleRows.Add((language, ruleId, string.Join(";", words.Distinct(StringComparer.Ordinal)))); + } + else + { + ruleRows.Add((language, ruleId, "")); + result.DeadRules.Add( + new DeadRule + { + FixtureId = fixture.Id, + Language = language, + RuleId = ruleId, + } + ); + } + } + } + + WriteCsv( + coverageCsvPath, + new[] { "language", "word", "signature", "construct" }, + coverageRows.Select(r => new[] { r.language, r.word, r.signature, r.construct }) + ); + WriteCsv( + rulesCsvPath, + new[] { "language", "rule_id", "words" }, + ruleRows.Select(r => new[] { r.language, r.ruleId, r.words }) + ); + + return result; + } + + private static void WriteCsv(string path, string[] header, IEnumerable rows) + { + var sb = new StringBuilder(); + sb.AppendLine(string.Join(",", header.Select(CsvField))); + foreach (string[] row in rows) + sb.AppendLine(string.Join(",", row.Select(CsvField))); + File.WriteAllText(path, sb.ToString()); + } + + private static string CsvField(string value) + { + value ??= ""; + if (value.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0) + return "\"" + value.Replace("\"", "\"\"") + "\""; + return value; + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/Diff.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Diff.cs new file mode 100644 index 000000000..a1e674437 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Diff.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// Per-word outcome of diffing one fixture's actual output against its expected.tsv. +public class WordResult +{ + public string Word = ""; + public bool Passed; + public string Detail = ""; +} + +public class FixtureDiff +{ + public List WordResults { get; } = new(); + public bool AllPassed => WordResults.All(w => w.Passed); +} + +/// +/// Word/status/signature-only diffing between two batch-TSV row sets, per conformance/PROTOCOL.md +/// section 4. Rows are matched by word (idx/ms are never compared); a signature's ";"-separated +/// entries are compared as a multiset via . +/// +public static class Diff +{ + public static FixtureDiff Compare(List expected, List actual) + { + var diff = new FixtureDiff(); + Dictionary> expByWord = GroupByWord(expected); + Dictionary> actByWord = GroupByWord(actual); + + IEnumerable allWords = expByWord + .Keys.Union(actByWord.Keys, StringComparer.Ordinal) + .OrderBy(w => w, StringComparer.Ordinal); + + foreach (string word in allWords) + { + bool hasExpected = expByWord.TryGetValue(word, out List expRows); + bool hasActual = actByWord.TryGetValue(word, out List actRows); + + if (!hasExpected) + { + diff.WordResults.Add( + new WordResult + { + Word = word, + Passed = false, + Detail = "present in adapter output but not in expected.tsv", + } + ); + continue; + } + if (!hasActual) + { + diff.WordResults.Add( + new WordResult + { + Word = word, + Passed = false, + Detail = "missing from adapter/self-check output", + } + ); + continue; + } + + List expCanon = Canonicalize(expRows); + List actCanon = Canonicalize(actRows); + bool passed = expCanon.SequenceEqual(actCanon, StringComparer.Ordinal); + diff.WordResults.Add( + new WordResult + { + Word = word, + Passed = passed, + Detail = passed + ? "ok" + : $"expected [{string.Join(" | ", expCanon)}] got [{string.Join(" | ", actCanon)}]", + } + ); + } + + return diff; + } + + private static Dictionary> GroupByWord(List rows) + { + var byWord = new Dictionary>(); + foreach (TsvRow row in rows) + { + if (!byWord.TryGetValue(row.Word, out List list)) + { + list = new List(); + byWord[row.Word] = list; + } + list.Add(row); + } + return byWord; + } + + // One canonical "status|sorted-signature-entries" string per row, sorted across the word's + // rows (usually just one) so occurrence order/duplication is handled as a multiset. + private static List Canonicalize(List rows) + { + return rows.Select(r => r.Status + "|" + string.Join(";", SignatureTsv.SplitSignature(r.Signature))) + .OrderBy(s => s, StringComparer.Ordinal) + .ToList(); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/EngineCrashException.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/EngineCrashException.cs new file mode 100644 index 000000000..604d3941c --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/EngineCrashException.cs @@ -0,0 +1,21 @@ +using System; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Thrown by an when the ENGINE UNDER TEST itself failed while loading a +/// grammar or parsing a word -- a genuine engine crash, as opposed to a harness-level failure +/// (the adapter process couldn't be started, timed out, or exited 0 without producing an output +/// file; see ). treats this exception specially: a +/// fixture whose manifest declares expectCrash: true (see ) +/// PASSes when this is thrown instead of the normal signature diff; any other fixture FAILs, +/// showing the carried diagnostics. +/// +public class EngineCrashException : Exception +{ + public EngineCrashException(string message) + : base(message) { } + + public EngineCrashException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/Fixture.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Fixture.cs new file mode 100644 index 000000000..c1068eec4 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Fixture.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// One fixture directory: exactly grammar.xml + words.yaml, under +/// conformance/languages/<name>/ or conformance/edge-cases/<name>/, per +/// docs/conformance-language-suite-plan.md section 2. Discovery is scoped to only those two roots. +/// +public class Fixture(string id, string directory, WordsYaml words) +{ + public string Id { get; } = id; + public string Directory { get; } = directory; + public WordsYaml Words { get; } = words; + + public string GrammarPath => Path.Combine(Directory, "grammar.xml"); + + public static List DiscoverAll(string fixturesRoot) + { + var fixtures = new List(); + foreach (string category in new[] { "languages", "edge-cases" }) + { + string categoryRoot = Path.Combine(fixturesRoot, category); + if (!System.IO.Directory.Exists(categoryRoot)) + continue; + + foreach (string dir in System.IO.Directory.EnumerateDirectories(categoryRoot)) + { + string grammarPath = Path.Combine(dir, "grammar.xml"); + string wordsYamlPath = Path.Combine(dir, "words.yaml"); + if (!File.Exists(grammarPath) || !File.Exists(wordsYamlPath)) + continue; + + string name = Path.GetFileName(dir); + string id = $"{category}/{name}"; + WordsYaml words = WordsYamlLoader.Load(wordsYamlPath); + fixtures.Add(new Fixture(id, dir, words)); + } + } + fixtures.Sort((a, b) => string.CompareOrdinal(a.Id, b.Id)); + return fixtures; + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/FixtureManifest.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/FixtureManifest.cs new file mode 100644 index 000000000..b4f379491 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/FixtureManifest.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Deserialized shape of a fixture's manifest.json (see conformance/README.md and +/// docs/conformance-framework-plan.md section 4.2). Field names are camelCase in the JSON file; +/// pins each mapping explicitly rather than relying on a +/// naming-policy convention, so the on-disk contract stays obvious from this class alone. +/// +public class FixtureManifest +{ + [JsonPropertyName("id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("category")] + public string Category { get; set; } = "single-feature"; + + [JsonPropertyName("constructs")] + public List Constructs { get; set; } = new(); + + [JsonPropertyName("tags")] + public List Tags { get; set; } = new(); + + [JsonPropertyName("requires")] + public List Requires { get; set; } = new(); + + // Nullable so Runner can distinguish "field absent" (bool default false would look identical + // to an explicit "false") from an explicit value to cross-check against Category above. + [JsonPropertyName("pathological")] + public bool? Pathological { get; set; } + + [JsonPropertyName("expectCrash")] + public bool ExpectCrash { get; set; } + + [JsonPropertyName("budget")] + public FixtureBudget Budget { get; set; } + + [JsonPropertyName("oracle")] + public JsonElement Oracle { get; set; } + + [JsonPropertyName("provenance")] + public string Provenance { get; set; } = ""; + + private static readonly JsonSerializerOptions Options = new() { ReadCommentHandling = JsonCommentHandling.Skip }; + + public static FixtureManifest Load(string path) + { + string json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, Options) + ?? throw new InvalidDataException($"manifest.json at '{path}' deserialized to null"); + } +} + +/// +/// A pathological fixture's complexity budget: a wall-clock bound only (no step-count ceiling +/// exists on this branch -- see conformance framework plan notes on BatchCommand's stripped +/// --rule-stats option). Absent/null for non-pathological fixtures. +/// +public class FixtureBudget +{ + [JsonPropertyName("wallClockMs")] + public long WallClockMs { get; set; } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/FixtureMaterializer.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/FixtureMaterializer.cs new file mode 100644 index 000000000..d40964ed6 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/FixtureMaterializer.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Linq; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Materializes a fixture (grammar.xml + words.yaml) into the on-disk adapter shape +/// (grammar.xml + words.txt + expected.tsv) in a temp directory, plus an in-memory +/// , so it can be run through + +/// for adapter mode, per docs/conformance-language-suite-plan.md +/// section 2.2 ("The harness materializes words.txt and expected.tsv into a temp directory from +/// words.yaml before invoking an adapter, so existing adapters ... work unmodified"). This is the +/// only code path that touches AdapterEngine/MaterializedRunner. +/// +public static class FixtureMaterializer +{ + /// + /// Builds the temp directory and returns a pointing at it. + /// Caller owns cleanup (delete the returned fixture's + /// when done). + /// + public static MaterializedFixture Materialize(Fixture fixture) + { + string tempDir = Path.Combine(Path.GetTempPath(), "hc-conformance-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + + File.Copy(fixture.GrammarPath, Path.Combine(tempDir, "grammar.xml")); + + // Words with any guess:true parse are self-check-only (PROTOCOL.md section 3: BatchCommand + // has no guessRoot opt-in, so no contract-compliant adapter can ever produce a guess parse) + // and are omitted from adapter materialization entirely rather than materialized with + // expectations no adapter can meet. + var adapterWords = fixture.Words.Words.Where(w => w.Parses.All(p => !p.Guess)).ToList(); + + string wordsPath = Path.Combine(tempDir, "words.txt"); + File.WriteAllLines(wordsPath, adapterWords.Select(w => w.Word)); + + string expectedPath = Path.Combine(tempDir, "expected.tsv"); + using (var writer = new StreamWriter(expectedPath)) + { + if (fixture.Words.ExpectCrash) + { + // See conformance/PROTOCOL.md's "STARTED sentinel" section: only the STARTED + // sentinel for the one word that crashes the oracle, no real result row -- + // MaterializedRunner never reads this content for an expectCrash fixture (its + // crash-handling catch block bypasses the diff entirely), but + // SignatureTsv.ReadFile still needs *a* file to exist. + WordEntry first = fixture.Words.Words[0]; + writer.WriteLine($"0\t{first.Word}\tSTARTED"); + } + else + { + int i = 0; + foreach (WordEntry word in adapterWords) + { + string signature = BuildExpectedSignature(word); + // expect_skip words carry status "SKIPPED" (the oracle throws InvalidShapeException), + // matching the expected.tsv row shape "N word 0 SKIPPED -"; everything else is "ok". + string status = word.ExpectSkip ? "SKIPPED" : "ok"; + writer.WriteLine($"{i}\t{word.Word}\t0\t{status}\t{signature}"); + i++; + } + } + } + + var manifest = new FixtureManifest + { + Id = fixture.Id, + Category = fixture.Words.BudgetMs.HasValue ? "pathological" : "single-feature", + Requires = fixture.Words.Requires, + ExpectCrash = fixture.Words.ExpectCrash, + Budget = fixture.Words.BudgetMs.HasValue + ? new FixtureBudget { WallClockMs = fixture.Words.BudgetMs.Value } + : null, + Provenance = string.Join( + "; ", + fixture.Words.Words.Where(w => !string.IsNullOrEmpty(w.Provenance)).Select(w => w.Provenance) + ), + }; + + return new MaterializedFixture(tempDir, manifest); + } + + public static void Cleanup(MaterializedFixture materialized) + { + try + { + if (Directory.Exists(materialized.Directory)) + Directory.Delete(materialized.Directory, recursive: true); + } + catch (IOException) { } + } + + /// The signature string materialized into expected.tsv column 5: "-" for a word with zero expected parses (expect_fail), else the ";"-joined, ordinally-sorted set of parses[].signature -- the same shape BatchCommand.BuildSignature itself produces. + public static string BuildExpectedSignature(WordEntry word) + { + if (word.ExpectFail || word.ExpectSkip || word.Parses.Count == 0) + return "-"; + return string.Join(";", word.Parses.Select(p => p.Signature).OrderBy(s => s, StringComparer.Ordinal)); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/GrammarRuleIndex.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/GrammarRuleIndex.cs new file mode 100644 index 000000000..83e61b862 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/GrammarRuleIndex.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Linq; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Independently re-parses a fixture's grammar.xml (not via , +/// whose runtime rule objects lose the XML id attribute for several rule kinds -- see below) +/// to answer two questions the harness needs for the "rules:" coverage dimension: (1) every rule id +/// the grammar declares (for dead-rule detection), and (2) how to translate a *runtime* rule +/// identity (what or a node's +/// Source actually expose at parse time) back to that XML id attribute, so a traced +/// rule application can be compared against a words.yaml "rules:" entry. +/// +/// Why this indirection is needed (see XmlLanguageLoader.TryLoadAffixProcessRule / +/// TryLoadCompoundingRule / LoadRewriteRule / LoadMetathesisRule): the XML +/// id attribute on a MorphologicalRule/RealizationalRule element is used only +/// as the loader's own lookup key while building the graph -- the *runtime* +/// object's is loaded from the separate <MorphemeId> child +/// element instead (the short code that appears in a parse's morph-chain signature, e.g. "PL"). +/// A CompoundingRule, PhonologicalRule, and MetathesisRule element has no +/// MorphemeId at all; their runtime objects expose only (from the +/// element's <Name> child). None of the four runtime rule kinds retains the XML +/// id attribute itself anywhere the harness can read it back after loading. This class +/// rebuilds that lost mapping straight from the XML. +/// +public class GrammarRuleIndex +{ + /// Every rule id declared by the grammar (MorphologicalRule/RealizationalRule/CompoundingRule/PhonologicalRule/MetathesisRule ids, plus the co-occurrence-rule pseudo-ids below), in document order. + public List AllRuleIds { get; } = new(); + + // MorphemeId text -> XML id attribute, for MorphologicalRule/RealizationalRule (AffixProcessRule + // /RealizationalAffixProcessRule at runtime): these are the only two kinds whose runtime object + // exposes a stable per-morpheme string (Morpheme.Id) that this class can translate back. + private readonly Dictionary _ruleIdByMorphemeId = new(StringComparer.Ordinal); + + // text -> XML id attribute, for CompoundingRule/PhonologicalRule/MetathesisRule: the only + // identity these three retain at runtime is IHCRule.Name. + private readonly Dictionary _ruleIdByRuleName = new(StringComparer.Ordinal); + + /// + /// MorphemeCoOccurrenceRule/AllomorphCoOccurrenceRule elements are EMPTY per the DTD -- they + /// have no id attribute of their own at all, only primaryMorpheme/ + /// primaryAllomorph IDREFs. This harness's convention (documented here -- it is + /// coverage/dead-rule bookkeeping, internal to the harness, not part of PROTOCOL.md's wire + /// adapter contract) is: such a rule's "rule id", for both + /// blocked_by and dead-rule detection purposes, IS the referenced primary morpheme/ + /// allomorph id. is that set, so dead-rule detection can + /// tell a real dead co-occurrence rule from an id that was never declared at all. + /// + public HashSet CoOccurrencePseudoIds { get; } = new(StringComparer.Ordinal); + + public static GrammarRuleIndex Load(string grammarPath) + { + var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; + using XmlReader reader = XmlReader.Create(grammarPath, settings); + XDocument doc = XDocument.Load(reader); + + var index = new GrammarRuleIndex(); + foreach (XElement el in doc.Descendants()) + { + switch (el.Name.LocalName) + { + case "MorphologicalRule": + case "RealizationalRule": + { + string id = (string)el.Attribute("id"); + string morphemeId = (string)el.Element("MorphemeId"); + if (id == null) + continue; + index.AllRuleIds.Add(id); + if (morphemeId != null) + { + index._ruleIdByMorphemeId[morphemeId] = id; + } + else + { + // Conservative fallback for a MorphologicalRule/RealizationalRule with no + // child at all (Morpheme.Id then null, so the morpheme-id path + // above can never resolve it) -- register its text the same way + // CompoundingRule/PhonologicalRule/MetathesisRule already are below, but only + // in this no-MorphemeId case, so every existing grammar (which assigns + // MorphemeId to every rule) is byte-for-byte unaffected: this dict only ever + // gains an entry when the morpheme-id path was never going to work anyway. + // Discovered authoring edge-cases/disjunctive-recheck + edge-cases/strrep- + // identity (both assign no MorphemeId at all, a deliberate choice -- adding + // one would change the "+|wokta"-style signatures those fixtures pin) -- see + // those fixtures' words.yaml headers. + string name = (string)el.Element("Name"); + if (name != null) + index._ruleIdByRuleName[name] = id; + } + break; + } + case "CompoundingRule": + case "PhonologicalRule": + case "MetathesisRule": + { + string id = (string)el.Attribute("id"); + string name = (string)el.Element("Name"); + if (id == null) + continue; + index.AllRuleIds.Add(id); + if (name != null) + index._ruleIdByRuleName[name] = id; + break; + } + case "MorphemeCoOccurrenceRule": + case "AllomorphCoOccurrenceRule": + { + string primary = (string)(el.Attribute("primaryMorpheme") ?? el.Attribute("primaryAllomorph")); + if (primary == null) + continue; + index.AllRuleIds.Add(primary); + index.CoOccurrencePseudoIds.Add(primary); + break; + } + } + } + return index; + } + + /// + /// Translates a fired (from , + /// or a RealizationalAffixProcessRule taken from a MorphologicalRuleSynthesis trace node -- see + /// ) back to its grammar.xml rule id. Returns null (rather than + /// throwing) for a rule kind this index cannot attribute. + /// + public string ResolveMorphologicalRuleId(IMorphologicalRule rule) + { + if ( + rule is Morpheme morpheme + && morpheme.Id != null + && _ruleIdByMorphemeId.TryGetValue(morpheme.Id, out string byMorphemeId) + ) + { + return byMorphemeId; + } + if (rule.Name != null && _ruleIdByRuleName.TryGetValue(rule.Name, out string byName)) + return byName; + return null; + } + + /// Translates a fired phonological rule (from a trace node's Source) back to its grammar.xml rule id, by IHCRule.Name -- see the class doc comment. + public string ResolvePhonologicalRuleId(IPhonologicalRule rule) + { + return rule.Name != null && _ruleIdByRuleName.TryGetValue(rule.Name, out string id) ? id : null; + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/IEngine.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/IEngine.cs new file mode 100644 index 000000000..69902bad1 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/IEngine.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Something that can produce batch-TSV rows for a fixture: either the live C# oracle running +/// in-process () or an external adapter process +/// (), per conformance/PROTOCOL.md. +/// +public interface IEngine +{ + /// A short, human-readable name for reporting (e.g. "self-check", "adapter"). + string Name { get; } + + /// + /// The capability set this engine declares support for. Self-check mode always reports the + /// full set (it IS the reference C# oracle); an adapter's set comes from the --capabilities + /// flag. + /// + IReadOnlySet Capabilities { get; } + + /// Runs every word in the fixture's words.txt, returning the resulting TSV rows. + List Run(MaterializedFixture fixture); +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/MaterializedFixture.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/MaterializedFixture.cs new file mode 100644 index 000000000..9e61ab030 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/MaterializedFixture.cs @@ -0,0 +1,20 @@ +using System.IO; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// A fixture materialized onto disk in the on-disk adapter shape (grammar.xml + words.txt + +/// expected.tsv + an in-memory ), so it can be handed to +/// and (self-check or an external adapter +/// process). Built by from a . +/// +public class MaterializedFixture(string directory, FixtureManifest manifest) +{ + public string Id { get; } = manifest.Id; + public string Directory { get; } = directory; + public FixtureManifest Manifest { get; } = manifest; + + public string GrammarPath => Path.Combine(Directory, "grammar.xml"); + public string WordsPath => Path.Combine(Directory, "words.txt"); + public string ExpectedPath => Path.Combine(Directory, "expected.tsv"); +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/MaterializedRunner.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/MaterializedRunner.cs new file mode 100644 index 000000000..28d05293f --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/MaterializedRunner.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +public enum FixtureOutcome +{ + Passed, + Failed, + Skipped, +} + +public class FixtureResult +{ + public string FixtureId = ""; + public FixtureOutcome Outcome; + public string Reason = ""; + public long ElapsedMs; + public List WordResults = new(); +} + +public class MaterializedRunReport +{ + public List Results { get; } = new(); + public int ExcludedPathologicalCount { get; set; } + + public int Passed => Results.Count(r => r.Outcome == FixtureOutcome.Passed); + public int Failed => Results.Count(r => r.Outcome == FixtureOutcome.Failed); + public int Skipped => Results.Count(r => r.Outcome == FixtureOutcome.Skipped); +} + +/// +/// Runs a list of s through an , applying +/// category exclusion (pathological) and capability filtering (manifest "requires" vs. the engine's +/// declared capabilities), and diffs results against each fixture's expected.tsv. Called by +/// against fixtures writes to disk. +/// +public static class MaterializedRunner +{ + public static MaterializedRunReport Run( + List fixtures, + IEngine engine, + bool includePathological + ) + { + var report = new MaterializedRunReport(); + + foreach (MaterializedFixture fixture in fixtures) + { + // category:pathological is the authoritative signal (the only thing docs ever mention); + // the manifest's own "pathological" boolean is redundant and, if present, must agree + // with it -- a mismatch is a manifest bug, not a runtime condition to silently resolve. + bool isPathological = fixture.Manifest.Category == "pathological"; + var result = new FixtureResult { FixtureId = fixture.Id }; + if (fixture.Manifest.Pathological.HasValue && fixture.Manifest.Pathological.Value != isPathological) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = + $"manifest error: pathological={fixture.Manifest.Pathological.Value.ToString().ToLowerInvariant()} " + + $"disagrees with category '{fixture.Manifest.Category}'"; + report.Results.Add(result); + continue; + } + + if (isPathological && !includePathological) + { + report.ExcludedPathologicalCount++; + continue; + } + + // manifest.requires must mechanically match what grammar.xml actually contains (see + // PROTOCOL.md section 5 / RequiresDerivation) -- a hand-authored drift here is a + // manifest bug, caught the same way the pathological/category mismatch above is. + List derivedRequires = RequiresDerivation.Derive(fixture.GrammarPath); + if ( + !new HashSet(derivedRequires, StringComparer.Ordinal).SetEquals( + new HashSet(fixture.Manifest.Requires, StringComparer.Ordinal) + ) + ) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = + $"manifest error: requires [{string.Join(",", fixture.Manifest.Requires)}] does not match " + + $"grammar-derived [{string.Join(",", derivedRequires)}]"; + report.Results.Add(result); + continue; + } + + List missingCapabilities = fixture + .Manifest.Requires.Where(req => !engine.Capabilities.Contains(req)) + .ToList(); + if (missingCapabilities.Count > 0) + { + result.Outcome = FixtureOutcome.Skipped; + result.Reason = + $"requires [{string.Join(",", fixture.Manifest.Requires)}], " + + $"engine declares [{string.Join(",", engine.Capabilities.OrderBy(c => c))}]; " + + $"missing [{string.Join(",", missingCapabilities)}]"; + report.Results.Add(result); + continue; + } + + // Read once, before the try: the old code re-read expected.tsv from inside the catch + // block too, so a genuinely missing file could throw from inside exception handling. + List expected = SignatureTsv.ReadFile(fixture.ExpectedPath); + + var sw = Stopwatch.StartNew(); + try + { + List actual = RunWithBudget(engine, fixture); + sw.Stop(); + result.ElapsedMs = sw.ElapsedMilliseconds; + + // A handful of fixtures deliberately pin a CRASH as their ground truth (e.g. + // rewrite/simultaneous-epenthesis-cascade's InfiniteLoopException): the manifest + // says so explicitly (expectCrash: true) rather than the harness inferring it from + // an empty expected.tsv. If the engine returns normally here, it did not reproduce + // that crash, which is itself a conformance failure. + if (fixture.Manifest.ExpectCrash) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = "expected crash, engine produced output"; + } + else + { + FixtureDiff diff = Diff.Compare(expected, actual); + result.WordResults = diff.WordResults; + + // Enforced whenever a budget is present, regardless of category: today only + // pathological fixtures carry one, but nothing about the check is category- + // specific. + bool overBudget = + fixture.Manifest.Budget != null && result.ElapsedMs > fixture.Manifest.Budget.WallClockMs; + + if (overBudget) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = + $"exceeded wall-clock budget ({result.ElapsedMs}ms > {fixture.Manifest.Budget.WallClockMs}ms)"; + } + else if (diff.AllPassed) + { + result.Outcome = FixtureOutcome.Passed; + } + else + { + result.Outcome = FixtureOutcome.Failed; + int failCount = diff.WordResults.Count(w => !w.Passed); + result.Reason = $"{failCount}/{diff.WordResults.Count} word(s) mismatched"; + } + } + } + catch (EngineCrashException ex) + { + sw.Stop(); + result.ElapsedMs = sw.ElapsedMilliseconds; + + if (fixture.Manifest.ExpectCrash) + { + result.Outcome = FixtureOutcome.Passed; + result.Reason = $"engine crashed as expected (manifest expectCrash: true): {ex.Message}"; + } + else + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = $"engine crashed: {ex.Message}"; + } + } + catch (TimeoutException ex) when (fixture.Manifest.Budget != null) + { + sw.Stop(); + result.ElapsedMs = sw.ElapsedMilliseconds; + + result.Outcome = FixtureOutcome.Failed; + result.Reason = $"exceeded budget {fixture.Manifest.Budget.WallClockMs}ms: {ex.Message}"; + } + catch (Exception ex) + { + sw.Stop(); + result.ElapsedMs = sw.ElapsedMilliseconds; + + // Any other exception is a plain harness error (bad --adapter template, process + // couldn't start, timed out, etc.) -- never a pass, regardless of expectCrash. + result.Outcome = FixtureOutcome.Failed; + result.Reason = $"harness error: {ex.Message}"; + } + + report.Results.Add(result); + } + + return report; + } + + /// + /// Runs the engine for one fixture. If the manifest carries a budget, engine.Run happens on a + /// background thread and this waits up to budget + a small margin; AdapterEngine independently + /// plumbs the same budget into its own process timeout (so a subprocess adapter gets killed), + /// but self-check's in-process call has no external process to kill, so this watchdog is what + /// keeps a runaway self-check parse from hanging the whole harness run. If the watchdog expires, + /// a TimeoutException is thrown and the background thread is simply abandoned -- this is a + /// short-lived CLI, so a leaked runaway thread on the rare budget-blown fixture is an acceptable + /// trade for keeping this shape simple. + /// + private static List RunWithBudget(IEngine engine, MaterializedFixture fixture) + { + FixtureBudget budget = fixture.Manifest.Budget; + if (budget == null) + return engine.Run(fixture); + + Task> task = Task.Run(() => engine.Run(fixture)); + long marginMs = Math.Max(budget.WallClockMs / 5, 1000); + bool completed; + try + { + completed = task.Wait((int)(budget.WallClockMs + marginMs)); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + // Unwrap: rethrow the engine's own exception so the caller's EngineCrashException/ + // generic catch blocks see the real exception type, not Task's wrapper. + throw ex.InnerException; + } + if (!completed) + throw new TimeoutException("watchdog timed out waiting for engine.Run"); + return task.GetAwaiter().GetResult(); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/Program.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Program.cs new file mode 100644 index 000000000..a139c8a45 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Program.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +internal class Program +{ + private static int Main(string[] args) + { + string fixturesRoot = null; + string adapterTemplate = null; + string capabilitiesArg = null; + bool capabilitiesProvided = false; + bool includePathological = false; + bool coverageReport = false; + string constructsPath = null; + bool propose = false; + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--fixtures": + if (!TryGetNextArg(args, ref i, "--fixtures", out fixturesRoot)) + return 2; + break; + case "--adapter": + if (!TryGetNextArg(args, ref i, "--adapter", out adapterTemplate)) + return 2; + break; + case "--capabilities": + if (!TryGetNextArg(args, ref i, "--capabilities", out capabilitiesArg)) + return 2; + capabilitiesProvided = true; + break; + case "--include-pathological": + includePathological = true; + break; + case "--coverage-report": + coverageReport = true; + break; + case "--constructs": + if (!TryGetNextArg(args, ref i, "--constructs", out constructsPath)) + return 2; + break; + case "--propose": + propose = true; + break; + case "-h": + case "--help": + PrintUsage(); + return 0; + default: + Console.Error.WriteLine($"unrecognized argument: {args[i]}"); + PrintUsage(); + return 2; + } + } + + if (string.IsNullOrEmpty(fixturesRoot)) + { + Console.Error.WriteLine("--fixtures is required"); + PrintUsage(); + return 2; + } + + List fixtures; + try + { + fixtures = Fixture.DiscoverAll(fixturesRoot); + } + catch (WordsYamlException ex) + { + Console.Error.WriteLine($"words.yaml error: {ex.Message}"); + return 2; + } + Console.WriteLine($"discovered {fixtures.Count} fixture(s) under '{fixturesRoot}'"); + + if (fixtures.Count == 0) + { + Console.Error.WriteLine($"no fixtures discovered under '{fixturesRoot}'"); + return 2; + } + + if (coverageReport) + { + constructsPath ??= Path.Combine(fixturesRoot, "constructs.txt"); + RunCoverageReport(fixtures, fixturesRoot, constructsPath); + return 0; + } + + IEngine engine; + if (adapterTemplate != null) + { + HashSet capabilities = ParseCapabilities(capabilitiesArg ?? ""); + engine = new AdapterEngine(adapterTemplate, capabilities); + } + else + { + // Self-check mode implies phonology support (it IS the reference engine) unless the + // caller explicitly overrides --capabilities, which is how the capability-filtering + // mechanism itself gets exercised without needing a second engine (see conformance + // framework verification notes). + IReadOnlySet capabilities = capabilitiesProvided ? ParseCapabilities(capabilitiesArg) : null; + engine = new SelfCheckEngine(capabilities); + } + + RunReport report = + adapterTemplate != null + ? Runner.RunAdapter(fixtures, engine, includePathological) + : Runner.RunSelfCheck(fixtures, includePathological, engine.Capabilities, propose, Console.Out); + PrintRunReport(report, engine); + + bool anyRan = report.Passed > 0 || report.Failed > 0; + if (!anyRan) + { + Console.Error.WriteLine("no fixtures actually ran (all excluded or skipped) -- treating this as an error"); + return 2; + } + + return report.Failed > 0 ? 1 : 0; + } + + /// Returns the argument following , or prints usage and returns + /// false if was the last token (no bounds-check crash on a truncated + /// command line). + private static bool TryGetNextArg(string[] args, ref int i, string flag, out string value) + { + if (i + 1 >= args.Length) + { + Console.Error.WriteLine($"{flag} requires a value"); + PrintUsage(); + value = null; + return false; + } + value = args[++i]; + return true; + } + + private static HashSet ParseCapabilities(string arg) + { + return arg.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToHashSet(StringComparer.Ordinal); + } + + private static void PrintRunReport(RunReport report, IEngine engine) + { + Console.WriteLine(); + Console.WriteLine( + $"engine: {engine.Name}, capabilities: [{string.Join(",", engine.Capabilities.OrderBy(c => c))}]" + ); + if (report.ExcludedPathologicalCount > 0) + { + Console.WriteLine( + $"{report.ExcludedPathologicalCount} pathological (budget_ms) fixture(s) excluded by default (--include-pathological to run)" + ); + } + Console.WriteLine(); + + foreach (FixtureResult result in report.Results) + { + string status = result.Outcome switch + { + FixtureOutcome.Passed => "PASS", + FixtureOutcome.Failed => "FAIL", + FixtureOutcome.Skipped => "SKIP", + _ => "?", + }; + Console.WriteLine($"[{status}] {result.FixtureId} ({result.ElapsedMs}ms) {result.Reason}"); + if (result.Outcome == FixtureOutcome.Failed) + { + foreach (WordResult w in result.WordResults.Where(w => !w.Passed)) + Console.WriteLine($" word '{w.Word}': {w.Detail}"); + } + } + + Console.WriteLine(); + Console.WriteLine( + $"totals: {report.Passed} passed, {report.Failed} failed, {report.Skipped} skipped (of {report.Results.Count} attempted)" + ); + } + + // "Tracing" is the one construct the suite deliberately never covers (it was never in + // expected.tsv's domain) -- see docs/conformance-language-suite-plan.md sections 3 and 7. + private const string OutOfScopeConstruct = "Tracing (TraceType)"; + + private static void RunCoverageReport(List fixtures, string fixturesRoot, string constructsPath) + { + string coverageCsvPath = Path.Combine(fixturesRoot, "coverage.csv"); + string rulesCsvPath = Path.Combine(fixturesRoot, "rules.csv"); + CoverageReport.CoverageResult result = CoverageReport.WriteCsvs(fixtures, coverageCsvPath, rulesCsvPath); + + Console.WriteLine(); + Console.WriteLine("coverage report"); + Console.WriteLine("==============="); + Console.WriteLine($"wrote {coverageCsvPath}"); + Console.WriteLine($"wrote {rulesCsvPath}"); + + // Absolute construct-coverage check against constructs.txt: every construct except Tracing + // must be covered. + if (File.Exists(constructsPath)) + { + List checklist = CoverageReport.LoadConstructChecklist(constructsPath); + List uncovered = checklist + .Where(c => !string.Equals(c, OutOfScopeConstruct, StringComparison.Ordinal)) + .Where(c => !result.CoveredConstructs.Contains(c)) + .ToList(); + Console.WriteLine(); + int inScope = checklist.Count(c => !string.Equals(c, OutOfScopeConstruct, StringComparison.Ordinal)); + Console.WriteLine( + $"construct coverage: {inScope - uncovered.Count}/{inScope} in-scope constructs covered " + + $"(Tracing out of scope by design)" + ); + if (uncovered.Count > 0) + { + Console.WriteLine($"*** {uncovered.Count} CONSTRUCT(S) AT ZERO COVERAGE ***"); + foreach (string c in uncovered) + Console.WriteLine($" {c}"); + } + } + + if (result.DeadRules.Count > 0) + { + Console.WriteLine(); + Console.WriteLine($"*** {result.DeadRules.Count} DEAD RULE(S) (exercised by zero words) ***"); + foreach (CoverageReport.DeadRule dead in result.DeadRules) + Console.WriteLine($" {dead.FixtureId}: rule '{dead.RuleId}'"); + } + else + { + Console.WriteLine("0 dead rules across all grammars."); + } + } + + private static void PrintUsage() + { + Console.WriteLine( + """ + usage: hc-conformance --fixtures [options] + + options: + --adapter "" Run fixtures through an external engine adapter. + Template placeholders: {grammar} {words} {output}. + Omit to run the harness's self-check mode (in-process + C# oracle) instead. + --capabilities Capability set the engine under test declares (e.g. + "phonology", or "" for none). Defaults to the full set + in self-check mode, empty in --adapter mode. + --include-pathological Also run category:pathological fixtures (excluded by + default). + --coverage-report Print a coverage report instead of running fixtures. + --constructs Construct checklist file for --coverage-report + (default: /constructs.txt). + --propose Self-check only: on a signature mismatch, print + the words.yaml patch that would reconcile it. Never + writes any file. + """ + ); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/ProposePatchWriter.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/ProposePatchWriter.cs new file mode 100644 index 000000000..9ec3e113e --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/ProposePatchWriter.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.IO; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// --propose: on a self-check signature mismatch, prints the words.yaml patch that +/// would reconcile the fixture with what the oracle actually produced, per +/// docs/conformance-language-suite-plan.md ("a `--propose` mode prints the would-be YAML patch for +/// a human to accept. No mode ever writes the file."). This class only ever writes to the +/// it is given -- it has no file-write call anywhere, by construction, so +/// there is no code path here that could accidentally start writing words.yaml back out. +/// +public static class ProposePatchWriter +{ + public static void WriteSignatureProposal( + TextWriter output, + Fixture fixture, + WordEntry word, + List actualEntries + ) + { + output.WriteLine(); + output.WriteLine($"# --propose: {fixture.Id}/words.yaml -- word '{word.Word}' signature mismatch"); + output.WriteLine("# Proposed replacement entry (rules:/gloss: left as-is; fill in by hand):"); + if (actualEntries.Count == 0) + { + output.WriteLine($" - word: {word.Word}"); + output.WriteLine($" note: {word.Note}"); + output.WriteLine(" expect_fail: true"); + } + else + { + output.WriteLine($" - word: {word.Word}"); + output.WriteLine($" note: {word.Note}"); + output.WriteLine(" parses:"); + foreach (string entry in actualEntries) + { + output.WriteLine($" - signature: \"{entry}\""); + output.WriteLine(" rules: [] # TODO: fill in from the trace"); + } + } + output.WriteLine(); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/RequiresDerivation.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/RequiresDerivation.cs new file mode 100644 index 000000000..4fccaf13e --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/RequiresDerivation.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Mechanically derives a fixture's "requires" capability list from its grammar.xml, per +/// conformance/PROTOCOL.md section 5: today the only defined capability is "phonology", meaning +/// the grammar contains at least one PhonologicalRule or MetathesisRule element. +/// validates every fixture's words.yaml "requires" (and +/// the materialized manifest.json equivalent) against this +/// derivation, so the field can never silently drift from what the grammar actually contains +/// (conformance/README.md's own growth-policy instructions already say to determine this by +/// inspecting the grammar, not by hand-guessing -- this makes that mechanical, not just advisory). +/// +public static class RequiresDerivation +{ + public static List Derive(string grammarPath) + { + // DtdProcessing.Ignore: this only needs element names, not the HermitCrabInput.dtd itself + // (which XmlLanguageLoader resolves via an embedded-resource resolver elsewhere) -- ignoring + // it here avoids taking on that dependency for a check this narrow. + var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; + using XmlReader reader = XmlReader.Create(grammarPath, settings); + XDocument doc = XDocument.Load(reader); + + bool hasPhonology = doc.Descendants() + .Any(e => e.Name.LocalName == "PhonologicalRule" || e.Name.LocalName == "MetathesisRule"); + + return hasPhonology ? new List { "phonology" } : new List(); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/Runner.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Runner.cs new file mode 100644 index 000000000..1cd3d4324 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/Runner.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +public class RunReport +{ + public List Results { get; } = new(); + public int ExcludedPathologicalCount { get; set; } + + public int Passed => Results.Count(r => r.Outcome == FixtureOutcome.Passed); + public int Failed => Results.Count(r => r.Outcome == FixtureOutcome.Failed); + public int Skipped => Results.Count(r => r.Outcome == FixtureOutcome.Skipped); +} + +/// +/// Runs fixtures under conformance/languages and conformance/edge-cases. Self-check mode is a +/// custom, in-process loop (it needs per-word signature-SET comparison against words.yaml and +/// traced-rules verification, neither of which / +/// does). Adapter mode instead materializes each fixture onto disk and hands it to +/// + -- see . +/// +public static class Runner +{ + public static RunReport RunSelfCheck( + List fixtures, + bool includePathological, + IReadOnlySet engineCapabilities, + bool propose, + TextWriter proposeOutput + ) + { + var report = new RunReport(); + foreach (Fixture fixture in fixtures) + { + FixtureResult result = RunOneSelfCheck( + fixture, + includePathological, + engineCapabilities, + propose, + proposeOutput + ); + if (result == null) + report.ExcludedPathologicalCount++; + else + report.Results.Add(result); + } + return report; + } + + private static FixtureResult RunOneSelfCheck( + Fixture fixture, + bool includePathological, + IReadOnlySet engineCapabilities, + bool propose, + TextWriter proposeOutput + ) + { + var result = new FixtureResult { FixtureId = fixture.Id }; + + // Mechanical requires-check against the words.yaml front matter (see PROTOCOL.md section 5 / + // RequiresDerivation) -- a hand-authored drift here is a words.yaml bug. + List derivedRequires = RequiresDerivation.Derive(fixture.GrammarPath); + if ( + !new HashSet(derivedRequires, StringComparer.Ordinal).SetEquals( + new HashSet(fixture.Words.Requires, StringComparer.Ordinal) + ) + ) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = + $"words.yaml error: requires [{string.Join(",", fixture.Words.Requires)}] does not match " + + $"grammar-derived [{string.Join(",", derivedRequires)}]"; + return result; + } + + bool isPathological = fixture.Words.BudgetMs.HasValue; + if (isPathological && !includePathological) + return null; + + List missingCapabilities = fixture.Words.Requires.Where(r => !engineCapabilities.Contains(r)).ToList(); + if (missingCapabilities.Count > 0) + { + result.Outcome = FixtureOutcome.Skipped; + result.Reason = + $"requires [{string.Join(",", fixture.Words.Requires)}], missing [{string.Join(",", missingCapabilities)}]"; + return result; + } + + var sw = Stopwatch.StartNew(); + try + { + RunWithBudget(fixture, result, propose, proposeOutput); + sw.Stop(); + result.ElapsedMs = sw.ElapsedMilliseconds; + + if (fixture.Words.ExpectCrash) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = "expected crash, engine produced output"; + } + else + { + bool overBudget = fixture.Words.BudgetMs.HasValue && result.ElapsedMs > fixture.Words.BudgetMs.Value; + if (overBudget) + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = $"exceeded wall-clock budget ({result.ElapsedMs}ms > {fixture.Words.BudgetMs}ms)"; + } + else if (result.WordResults.All(w => w.Passed)) + { + result.Outcome = FixtureOutcome.Passed; + } + else + { + result.Outcome = FixtureOutcome.Failed; + int failCount = result.WordResults.Count(w => !w.Passed); + result.Reason = $"{failCount}/{result.WordResults.Count} word(s) mismatched"; + } + } + } + catch (Exception ex) + { + sw.Stop(); + result.ElapsedMs = sw.ElapsedMilliseconds; + if (fixture.Words.ExpectCrash) + { + result.Outcome = FixtureOutcome.Passed; + result.Reason = $"engine crashed as expected (words.yaml expect_crash: true): {ex.Message}"; + } + else + { + result.Outcome = FixtureOutcome.Failed; + result.Reason = $"engine crashed: {ex.Message}"; + } + } + + return result; + } + + // Mirrors MaterializedRunner.RunWithBudget's watchdog shape (see that method's doc comment) -- a + // budgeted fixture gets the same "background thread + Task.Wait" timeout enforcement. + private static void RunWithBudget(Fixture fixture, FixtureResult result, bool propose, TextWriter proposeOutput) + { + if (!fixture.Words.BudgetMs.HasValue) + { + RunAllWords(fixture, result, propose, proposeOutput); + return; + } + + long budgetMs = fixture.Words.BudgetMs.Value; + Task task = Task.Run(() => RunAllWords(fixture, result, propose, proposeOutput)); + long marginMs = Math.Max(budgetMs / 5, 1000); + bool completed; + try + { + completed = task.Wait((int)(budgetMs + marginMs)); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + throw ex.InnerException; + } + if (!completed) + throw new TimeoutException("watchdog timed out waiting for self-check engine"); + } + + private static void RunAllWords(Fixture fixture, FixtureResult result, bool propose, TextWriter proposeOutput) + { + Language language = XmlLanguageLoader.Load(fixture.GrammarPath); + var traceManager = new TraceManager { IsTracing = true }; + var morpher = new Morpher(traceManager, language); + GrammarRuleIndex ruleIndex = GrammarRuleIndex.Load(fixture.GrammarPath); + + foreach (WordEntry word in fixture.Words.Words) + { + bool guessRoot = word.Parses.Any(p => p.Guess); + List results; + object trace; + bool wasSkipped = false; + try + { + results = morpher.ParseWord(word.Word, out trace, guessRoot).ToList(); + } + catch (InvalidShapeException) + { + // The engine SKIPS this word (an undeclared segment, etc.) rather than returning a + // zero-parse "ok" -- the adapter contract's "SKIPPED" status. Distinguished from a + // genuine 0-parse below so expect_fail (ok) and expect_skip (SKIPPED) each verify the + // status the materialized expected.tsv (and any real adapter) will actually diff on. + wasSkipped = true; + results = new List(); + trace = null; + } + + string actualSignature = SignatureFormat.BuildSignature(results); + string expectedSignature = FixtureMaterializer.BuildExpectedSignature(word); + List actualEntries = SignatureTsv.SplitSignature(actualSignature); + List expectedEntries = SignatureTsv.SplitSignature(expectedSignature); + + bool signatureMatches = actualEntries.SequenceEqual(expectedEntries, StringComparer.Ordinal); + if (!signatureMatches) + { + result.WordResults.Add( + new WordResult + { + Word = word.Word, + Passed = false, + Detail = + $"expected [{string.Join(" | ", expectedEntries)}] got [{string.Join(" | ", actualEntries)}]", + } + ); + if (propose) + ProposePatchWriter.WriteSignatureProposal(proposeOutput, fixture, word, actualEntries); + continue; + } + + if (word.ExpectSkip) + { + // Declared SKIPPED: PASS iff the engine actually threw InvalidShapeException. A word + // that instead produced a genuine empty parse set (status "ok") is mis-declared -- it + // would diff as ok-vs-SKIPPED against a real adapter, so fail it here too. + result.WordResults.Add( + new WordResult + { + Word = word.Word, + Passed = wasSkipped, + Detail = wasSkipped + ? "ok (skipped: InvalidShapeException)" + : "declared expect_skip but engine produced a zero-parse 'ok' without skipping", + } + ); + continue; + } + + if (word.ExpectFail) + { + // Declared genuine zero-parse "ok": PASS iff the engine did NOT skip. A word that was + // actually SKIPPED (InvalidShapeException) is mis-declared and should be expect_skip -- + // it would diff as SKIPPED-vs-ok against a real adapter (adapter mode materializes "ok" + // for expect_fail). Keeping self-check strict here keeps the two modes consistent. + result.WordResults.Add( + new WordResult + { + Word = word.Word, + Passed = !wasSkipped, + Detail = !wasSkipped + ? "ok (0 parses)" + : "declared expect_fail (status ok) but engine SKIPPED it (InvalidShapeException) -- use expect_skip", + } + ); + continue; + } + + // Signature set matched; now verify the traced "rules:" list per parse. Group actual + // results by their own individual signature so an ambiguous word's N declared parses + // can each be matched to the one result that produced it (signatureMatches above already + // proved this is a 1:1 correspondence at the SET level; duplicate signature strings + // across parses are not expected and, if they occurred, would just re-check the same + // result against every parse sharing that signature). + Dictionary> actualBySignature = results + .GroupBy(w => SignatureFormat.BuildSignature(new[] { w })) + .ToDictionary(g => g.Key, g => g.ToList()); + HashSet wordLevelRuleIds = TraceRuleAttributor.WordLevelRuleIds(trace, ruleIndex); + + bool wordRulesOk = true; + var ruleDetails = new List(); + foreach (ParseEntry parse in word.Parses) + { + if (!actualBySignature.TryGetValue(parse.Signature, out List matches) || matches.Count == 0) + continue; // can't happen given signatureMatches, but stay defensive + foreach (Word match in matches) + { + HashSet actualRuleIds = TraceRuleAttributor.MorphologicalRuleIds(match, ruleIndex); + actualRuleIds.UnionWith(wordLevelRuleIds); + var declaredRuleIds = new HashSet(parse.Rules, StringComparer.Ordinal); + if (!actualRuleIds.SetEquals(declaredRuleIds)) + { + wordRulesOk = false; + ruleDetails.Add( + $"parse '{parse.Signature}': declared rules [{string.Join(",", parse.Rules)}] " + + $"!= traced [{string.Join(",", actualRuleIds.OrderBy(x => x, StringComparer.Ordinal))}]" + ); + } + } + } + + result.WordResults.Add( + new WordResult + { + Word = word.Word, + Passed = wordRulesOk, + Detail = wordRulesOk ? "ok" : string.Join("; ", ruleDetails), + } + ); + } + } + + // ---- Adapter mode: materialize onto disk + reuse MaterializedRunner/AdapterEngine, unmodified. ---- + + public static RunReport RunAdapter(List fixtures, IEngine engine, bool includePathological) + { + var report = new RunReport(); + foreach (Fixture fixture in fixtures) + { + MaterializedFixture materialized = FixtureMaterializer.Materialize(fixture); + try + { + MaterializedRunReport oneReport = MaterializedRunner.Run( + new List { materialized }, + engine, + includePathological + ); + report.ExcludedPathologicalCount += oneReport.ExcludedPathologicalCount; + foreach (FixtureResult r in oneReport.Results) + { + r.FixtureId = fixture.Id; // already fixture.Id (came from the synthetic manifest); kept explicit for clarity + report.Results.Add(r); + } + } + finally + { + FixtureMaterializer.Cleanup(materialized); + } + } + return report; + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/SIL.Machine.Morphology.HermitCrab.Conformance.csproj b/src/SIL.Machine.Morphology.HermitCrab.Conformance/SIL.Machine.Morphology.HermitCrab.Conformance.csproj new file mode 100644 index 000000000..eb6d394a1 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/SIL.Machine.Morphology.HermitCrab.Conformance.csproj @@ -0,0 +1,30 @@ + + + + Exe + net10.0 + SIL.Machine.Morphology.HermitCrab.Conformance + hc-conformance + Engine-agnostic conformance test harness for the HermitCrab grammar model (see conformance/PROTOCOL.md). + SIL.Machine.Morphology.HermitCrab.Conformance + true + true + true + $(NoWarn);CS1591;CS1573 + + + + + + + + + + + + + + + diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/SelfCheckEngine.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/SelfCheckEngine.cs new file mode 100644 index 000000000..5a786bb9e --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/SelfCheckEngine.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Runs a fixture against the live C# HermitCrab engine, in-process: loads grammar.xml via +/// , builds a , parses every word in +/// words.txt, and computes the exact same signature that +/// BatchCommand's oracle uses -- byte-identical, since it's the same shared code, not a +/// reimplementation. This is what catches a fixture going stale when the oracle itself changes +/// (the lesson the plan doc cites from LT-22613). +/// +/// +/// Lets a caller override the reference engine's normal full-capability declaration -- e.g. +/// --capabilities "", which is how the harness's own capability-filtering skip path gets +/// exercised against the live oracle without needing a second, capability-limited engine on hand. +/// Null (the default) means the full set. +/// +public class SelfCheckEngine(IReadOnlySet capabilities = null) : IEngine +{ + private static readonly IReadOnlySet DefaultCapabilities = new HashSet { "phonology" }; + + public string Name => "self-check"; + + // The reference C# engine implements every capability the suite currently knows about, unless + // overridden above. + public IReadOnlySet Capabilities { get; } = capabilities ?? DefaultCapabilities; + + public List Run(MaterializedFixture fixture) + { + Language language; + Morpher morpher; + try + { + language = XmlLanguageLoader.Load(fixture.GrammarPath); + morpher = new Morpher(new TraceManager(), language); + } + catch (Exception ex) + { + throw new EngineCrashException( + $"self-check engine crashed loading grammar for fixture '{fixture.Id}': {ex.Message}", + ex + ); + } + + string[] words = SignatureFormat.LoadWords(fixture.WordsPath); + + var rows = new List(words.Length); + for (int i = 0; i < words.Length; i++) + { + string word = words[i]; + try + { + // SignatureFormat.ParseOneWord only catches InvalidShapeException itself (yielding + // "SKIPPED"); anything else it lets propagate is a genuine engine crash, wrapped + // here the same way grammar-load failures are above. + (string status, long elapsedMs, string signature) = SignatureFormat.ParseOneWord(morpher, word); + rows.Add(new TsvRow(i, word, elapsedMs, status, signature)); + } + catch (Exception ex) + { + throw new EngineCrashException( + $"self-check engine crashed parsing word '{word}' in fixture '{fixture.Id}': {ex.Message}", + ex + ); + } + } + return rows; + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/SignatureTsv.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/SignatureTsv.cs new file mode 100644 index 000000000..0ab0088e5 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/SignatureTsv.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// One well-formed 5-column row of the batch TSV format (see conformance/PROTOCOL.md). +public readonly struct TsvRow(int idx, string word, long ms, string status, string signature) +{ + public readonly int Idx = idx; + public readonly string Word = word; + public readonly long Ms = ms; + public readonly string Status = status; + public readonly string Signature = signature; +} + +/// +/// Parsing and comparison for the batch TSV format, per conformance/PROTOCOL.md sections 2-4: +/// only word/status/signature are compared; idx and ms are ignored; any line that isn't a +/// well-formed 5-column row (in particular the "STARTED" 3-column sentinel) is skipped; a +/// signature's ";"-separated entries are compared as a multiset, not a plain string. +/// +public static class SignatureTsv +{ + /// Reads a batch TSV file, silently skipping malformed/sentinel lines. + public static List ReadFile(string path) + { + var rows = new List(); + foreach (string line in File.ReadAllLines(path)) + { + if (TryParseRow(line, out TsvRow row)) + rows.Add(row); + } + return rows; + } + + public static bool TryParseRow(string line, out TsvRow row) + { + row = default; + if (string.IsNullOrEmpty(line)) + return false; + string[] cols = line.Split('\t'); + // A well-formed row has exactly 5 columns (idx, word, ms, status, signature). The + // 3-column "STARTED" sentinel and any other malformed line is not comparison data. + if (cols.Length != 5) + return false; + if (!int.TryParse(cols[0], out int idx)) + return false; + if (!long.TryParse(cols[2], out long ms)) + return false; + // A well-formed row's status is always "ok" or "SKIPPED" (see PROTOCOL.md section 2); any + // other value means the line isn't actually a comparison-worthy result row. + if (cols[3] != "ok" && cols[3] != "SKIPPED") + return false; + row = new TsvRow(idx, cols[1], ms, cols[3], cols[4]); + return true; + } + + /// The ";"-separated signature entries as a multiset (sorted list, for equality/diff display). + public static List SplitSignature(string signature) + { + if (signature == "-") + return new List(); + return signature.Split(';').OrderBy(s => s, StringComparer.Ordinal).ToList(); + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/TraceRuleAttributor.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/TraceRuleAttributor.cs new file mode 100644 index 000000000..dc75d8bbd --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/TraceRuleAttributor.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using SIL.Machine.Morphology.HermitCrab.MorphologicalRules; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Extracts, for one successful parse () returned by Morpher.ParseWord, +/// the set of grammar rule ids the plan's "rules:" field is verified against, per +/// docs/conformance-language-suite-plan.md section 2.1 ("self-check runs the oracle with tracing +/// enabled and FAILs if the traced rule applications don't match the declared list"). +/// +/// Two different sources, because no single API gives both: +/// +/// Morphological rules (AffixProcessRule/CompoundingRule) come straight off +/// -- populated once during analysis-direction unapplication +/// and carried forward unchanged through synthesis, so it is exact and per-result, no ambiguity. +/// Phonological rules (RewriteRule/MetathesisRule) have no equivalent per-Word list -- +/// only records their application as nodes +/// (), via . +/// Unlike , that call does NOT update +/// Word.CurrentTrace (see TraceManager.cs), so a phonological-rule trace node is a *sibling* +/// of the branch that produced a given final Word, not an ancestor reachable by walking +/// CurrentTrace upward. This class instead walks the ENTIRE trace tree for the word (the +/// trace out-parameter of ParseWord) and collects every +/// node system-wide. +/// +/// +/// Documented limitation (ambiguous words): when a surface word has more than one successful +/// analysis, this global trace walk cannot attribute a specific phonological rule firing to one +/// analysis branch over another -- it reports the UNION of every phonological rule that fired +/// anywhere while producing any of the word's results. applies this same +/// phonological-rule set to every one of that word's declared parses, so a false PASS is possible +/// in principle for an ambiguous word whose two parses use different phonological rules but the +/// same declared "rules:" set does not distinguish them. This does not affect any G1 fixture (the +/// pilot grammar has zero phonological rules by construction; the one ambiguous pilot word's +/// parses differ only in a morphological rule, which IS attributed exactly per-result). A future +/// language with both ambiguity and phonological rules would need per-branch trace correlation +/// (e.g. threading a distinguishing marker through ITraceManager.PhonologicalRuleApplied) to +/// close this gap -- flagged here rather than silently shipped. +/// +/// RealizationalAffixProcessRule applications are attributed word-level too: +/// Word.MorphologicalRuleUnapplied deliberately excludes them from the _mruleApps +/// list that backs (see Word.cs), so they cannot come from +/// the per-result source above -- but SynthesisRealizationalAffixProcessRule does report +/// successful applications through , which +/// records a node. The same trace walk that +/// collects phonological rules therefore also collects realizational rules (with the same +/// union-across-results ambiguity caveat), keyed off the node's Source type. +/// +public static class TraceRuleAttributor +{ + public static HashSet MorphologicalRuleIds(Word result, GrammarRuleIndex index) + { + var ids = new HashSet(StringComparer.Ordinal); + foreach (IMorphologicalRule rule in result.MorphologicalRules) + { + string id = index.ResolveMorphologicalRuleId(rule); + if (id != null) + ids.Add(id); + } + return ids; + } + + /// The word-level (not per-result) set of phonological and realizational rule ids fired anywhere in the trace tree rooted at -- see the class doc comment for why these two kinds are word-level, not per-result. + public static HashSet WordLevelRuleIds(object trace, GrammarRuleIndex index) + { + var ids = new HashSet(StringComparer.Ordinal); + if (trace is not Trace root) + return ids; + Walk(root); + return ids; + + void Walk(Trace node) + { + // TraceManager records BOTH outcomes of a synthesis-direction rule attempt as + // *RuleSynthesis nodes: *RuleApplied (FailureReason.None, Output set) and + // *RuleNotApplied (FailureReason set, no Output). Only the applied nodes count as + // "this rule fired" -- without this filter, every phonological rule in a stratum shows + // up in every word's traced set merely because its pass ran. + if (node.FailureReason == FailureReason.None) + { + if (node.Type == TraceType.PhonologicalRuleSynthesis && node.Source is IPhonologicalRule prule) + { + string id = index.ResolvePhonologicalRuleId(prule); + if (id != null) + ids.Add(id); + } + else if ( + node.Type == TraceType.MorphologicalRuleSynthesis + && node.Source is RealizationalAffixProcessRule rrule + ) + { + string id = index.ResolveMorphologicalRuleId(rrule); + if (id != null) + ids.Add(id); + } + } + foreach (Trace child in node.Children) + Walk(child); + } + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/WordsYaml.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/WordsYaml.cs new file mode 100644 index 000000000..8bf33dce6 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/WordsYaml.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// In-memory model of one words.yaml file, per +/// docs/conformance-language-suite-plan.md section 2.1. Parsed by , +/// which enforces the strict, fixed key vocabulary documented there -- this class only carries +/// already-validated data, it does not itself validate anything. +/// +public class WordsYaml +{ + public string Language = ""; + public List InspiredBy { get; } = new(); + public List Sources { get; } = new(); + public List Requires { get; } = new(); + + /// Edge-cases only: a wall-clock budget in milliseconds (materialized as manifest.json's budget.wallClockMs). + public long? BudgetMs; + + /// Edge-cases only: this fixture's ground truth is a crash, not a signature (materialized as manifest.json's expectCrash). + public bool ExpectCrash; + + public List Words { get; } = new(); +} + +public class WordEntry +{ + public string Word = ""; + public string Note = ""; + public string Provenance = ""; + public bool ExpectFail; + + /// + /// The oracle SKIPS this word rather than returning a genuine zero-parse "ok": it throws + /// (e.g. the surface contains a segment undeclared in the + /// grammar's CharacterDefinitionTable). Distinct from (well-formed input, + /// zero valid analyses) because the two produce different adapter-contract status columns -- + /// "SKIPPED" vs "ok" -- and BatchCommand/expected.tsv distinguish them (materialized row: "N word 0 SKIPPED -"). + /// Both modes verify it: self-check confirms an was actually + /// thrown; adapter mode materializes status "SKIPPED". Like expect_fail, a skip word has no parses. + /// + public bool ExpectSkip; + + public List BlockedBy { get; } = new(); + public List Exercises { get; } = new(); + public List Parses { get; } = new(); +} + +public class ParseEntry +{ + public string Signature = ""; + public string Gloss = ""; + public bool Guess; + public List Rules { get; } = new(); + public List Exercises { get; } = new(); +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/WordsYamlLoader.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/WordsYamlLoader.cs new file mode 100644 index 000000000..32fe5fc0b --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/WordsYamlLoader.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.RepresentationModel; + +namespace SIL.Machine.Morphology.HermitCrab.Conformance; + +/// +/// Strict parser for a fixture's words.yaml, per +/// docs/conformance-language-suite-plan.md section 2.1: a fixed key vocabulary (unknown keys are +/// hard errors) and "plain YAML 1.2 subset only: no anchors, aliases, merge keys, or custom tags". +/// +public static class WordsYamlLoader +{ + // The complete, fixed key vocabulary at each of the three mapping levels. Anything else + // encountered is a hard parse error -- this is the mechanism behind "unknown keys are errors". + private static readonly HashSet FrontMatterKeys = new(StringComparer.Ordinal) + { + "language", + "inspired_by", + "sources", + "requires", + "budget_ms", + "expect_crash", + "words", + }; + private static readonly HashSet WordKeys = new(StringComparer.Ordinal) + { + "word", + "note", + "provenance", + "expect_fail", + "expect_skip", + "blocked_by", + "exercises", + "parses", + }; + private static readonly HashSet ParseKeys = new(StringComparer.Ordinal) + { + "signature", + "gloss", + "guess", + "rules", + "exercises", + }; + + public static WordsYaml Load(string path) + { + string text = File.ReadAllText(path); + try + { + RejectNonPlainConstructs(text, path); + + var yamlStream = new YamlStream(); + using (var reader = new StringReader(text)) + yamlStream.Load(reader); + + if (yamlStream.Documents.Count != 1) + throw new WordsYamlException(path, "expected exactly one YAML document"); + + if (yamlStream.Documents[0].RootNode is not YamlMappingNode root) + throw new WordsYamlException(path, "top-level YAML node must be a mapping"); + + return ParseFrontMatter(root, path); + } + catch (YamlException ex) + { + throw new WordsYamlException(path, $"YAML syntax error at {ex.Start}-{ex.End}: {ex.Message}", ex); + } + } + + /// + /// Scans the raw parser event stream (not the composed node tree -- by the time + /// composes a tree, an alias has already been resolved into a shared + /// node reference and is no longer visible as such) for anchors, aliases, and non-core tags, + /// per the plan's "no anchors, aliases, merge keys, or custom tags" rule. Merge keys ("<<") + /// are a mapping-key convention layered on plain scalars, not a distinct event type, so those + /// are rejected in instead, where key strings are actually read. + /// + private static void RejectNonPlainConstructs(string text, string path) + { + var parser = new Parser(new StringReader(text)); + while (parser.MoveNext()) + { + ParsingEvent evt = parser.Current; + if (evt is AnchorAlias) + throw new WordsYamlException(path, "YAML aliases (*name) are not allowed in words.yaml"); + + if (evt is NodeEvent nodeEvent) + { + if (!nodeEvent.Anchor.IsEmpty) + throw new WordsYamlException(path, "YAML anchors (&name) are not allowed in words.yaml"); + if ( + !nodeEvent.Tag.IsEmpty + && !nodeEvent.Tag.Value.StartsWith("tag:yaml.org,2002:", StringComparison.Ordinal) + ) + { + throw new WordsYamlException( + path, + $"custom YAML tag '{nodeEvent.Tag.Value}' is not allowed in words.yaml" + ); + } + } + } + } + + private static WordsYaml ParseFrontMatter(YamlMappingNode root, string path) + { + var doc = new WordsYaml(); + Dictionary map = RequireMapping(root, FrontMatterKeys, path, "front matter"); + + doc.Language = RequireScalar(map, "language", path, "front matter"); + AppendScalarList(map, "inspired_by", doc.InspiredBy, path, "front matter"); + AppendScalarList(map, "sources", doc.Sources, path, "front matter"); + AppendScalarList(map, "requires", doc.Requires, path, "front matter"); + if (map.TryGetValue("budget_ms", out YamlNode budgetNode)) + doc.BudgetMs = long.Parse(RequireScalarNode(budgetNode, path, "front matter.budget_ms")); + if (map.TryGetValue("expect_crash", out YamlNode crashNode)) + doc.ExpectCrash = ParseBool(RequireScalarNode(crashNode, path, "front matter.expect_crash"), path); + + if (!map.TryGetValue("words", out YamlNode wordsNode) || wordsNode is not YamlSequenceNode wordsSeq) + throw new WordsYamlException(path, "front matter must have a 'words' sequence"); + if (wordsSeq.Children.Count == 0) + throw new WordsYamlException(path, "'words' must contain at least one word entry"); + + foreach (YamlNode wordNode in wordsSeq.Children) + doc.Words.Add(ParseWord(wordNode, path)); + + return doc; + } + + private static WordEntry ParseWord(YamlNode node, string path) + { + if (node is not YamlMappingNode mapNode) + throw new WordsYamlException(path, "each 'words' entry must be a mapping"); + Dictionary map = RequireMapping(mapNode, WordKeys, path, "word"); + + var entry = new WordEntry + { + Word = RequireScalar(map, "word", path, "word"), + Note = RequireScalar(map, "note", path, "word"), + }; + if (map.TryGetValue("provenance", out YamlNode provNode)) + entry.Provenance = RequireScalarNode(provNode, path, $"word '{entry.Word}'.provenance"); + if (map.TryGetValue("expect_fail", out YamlNode failNode)) + entry.ExpectFail = ParseBool(RequireScalarNode(failNode, path, $"word '{entry.Word}'.expect_fail"), path); + if (map.TryGetValue("expect_skip", out YamlNode skipNode)) + entry.ExpectSkip = ParseBool(RequireScalarNode(skipNode, path, $"word '{entry.Word}'.expect_skip"), path); + AppendScalarList(map, "blocked_by", entry.BlockedBy, path, $"word '{entry.Word}'"); + AppendScalarList(map, "exercises", entry.Exercises, path, $"word '{entry.Word}'"); + + if (entry.ExpectFail && entry.ExpectSkip) + { + throw new WordsYamlException( + path, + $"word '{entry.Word}': 'expect_fail' and 'expect_skip' are mutually exclusive " + + "(a word is either a genuine zero-parse 'ok' or a SKIPPED InvalidShapeException, not both)" + ); + } + if (entry.BlockedBy.Count > 0 && !entry.ExpectFail) + { + throw new WordsYamlException( + path, + $"word '{entry.Word}': 'blocked_by' is only meaningful on an 'expect_fail' word" + ); + } + + bool hasParses = map.TryGetValue("parses", out YamlNode parsesNode); + if (entry.ExpectFail || entry.ExpectSkip) + { + if (hasParses) + { + throw new WordsYamlException( + path, + $"word '{entry.Word}': expect_fail/expect_skip words must not have 'parses'" + ); + } + } + else + { + if (!hasParses || parsesNode is not YamlSequenceNode parsesSeq || parsesSeq.Children.Count == 0) + { + throw new WordsYamlException( + path, + $"word '{entry.Word}': non-expect_fail words must have at least one entry under 'parses'" + ); + } + foreach (YamlNode parseNode in parsesSeq.Children) + entry.Parses.Add(ParseParse(parseNode, entry.Word, path)); + } + + return entry; + } + + private static ParseEntry ParseParse(YamlNode node, string word, string path) + { + if (node is not YamlMappingNode mapNode) + throw new WordsYamlException(path, $"word '{word}': each 'parses' entry must be a mapping"); + Dictionary map = RequireMapping(mapNode, ParseKeys, path, $"word '{word}'.parses[]"); + + var parse = new ParseEntry { Signature = RequireScalar(map, "signature", path, $"word '{word}'.parses[]") }; + if (map.TryGetValue("gloss", out YamlNode glossNode)) + parse.Gloss = RequireScalarNode(glossNode, path, $"word '{word}'.parses[].gloss"); + if (map.TryGetValue("guess", out YamlNode guessNode)) + parse.Guess = ParseBool(RequireScalarNode(guessNode, path, $"word '{word}'.parses[].guess"), path); + if (!map.ContainsKey("rules")) + throw new WordsYamlException(path, $"word '{word}': every parse requires a 'rules' list (may be empty [])"); + AppendScalarList(map, "rules", parse.Rules, path, $"word '{word}'.parses[]"); + AppendScalarList(map, "exercises", parse.Exercises, path, $"word '{word}'.parses[]"); + + return parse; + } + + /// + /// Reads a mapping node's keys into a dictionary, hard-failing on any key not in + /// (the "unknown keys are hard errors" rule) and on any + /// non-scalar key (which would indicate a merge key or other non-plain construct). + /// + private static Dictionary RequireMapping( + YamlMappingNode node, + HashSet allowedKeys, + string path, + string context + ) + { + var map = new Dictionary(StringComparer.Ordinal); + foreach (KeyValuePair entry in node.Children) + { + if (entry.Key is not YamlScalarNode keyScalar || keyScalar.Value == null) + throw new WordsYamlException(path, $"{context}: mapping keys must be plain scalars"); + string key = keyScalar.Value; + if (key == "<<") + throw new WordsYamlException(path, $"{context}: YAML merge keys ('<<') are not allowed"); + if (!allowedKeys.Contains(key)) + { + throw new WordsYamlException( + path, + $"{context}: unknown key '{key}' (allowed: {string.Join(", ", allowedKeys.OrderBy(k => k))})" + ); + } + if (map.ContainsKey(key)) + throw new WordsYamlException(path, $"{context}: duplicate key '{key}'"); + map[key] = entry.Value; + } + return map; + } + + private static string RequireScalar(Dictionary map, string key, string path, string context) + { + if (!map.TryGetValue(key, out YamlNode node)) + throw new WordsYamlException(path, $"{context}: missing required key '{key}'"); + return RequireScalarNode(node, path, $"{context}.{key}"); + } + + private static string RequireScalarNode(YamlNode node, string path, string context) + { + if (node is not YamlScalarNode scalar || scalar.Value == null || scalar.Value.Length == 0) + throw new WordsYamlException(path, $"{context}: expected a non-empty scalar value"); + return scalar.Value; + } + + private static void AppendScalarList( + Dictionary map, + string key, + List target, + string path, + string context + ) + { + if (!map.TryGetValue(key, out YamlNode node)) + return; + if (node is not YamlSequenceNode seq) + throw new WordsYamlException(path, $"{context}.{key}: expected a sequence"); + foreach (YamlNode child in seq.Children) + target.Add(RequireScalarNode(child, path, $"{context}.{key}[]")); + } + + private static bool ParseBool(string value, string path) + { + return value switch + { + "true" => true, + "false" => false, + _ => throw new WordsYamlException(path, $"expected 'true' or 'false', got '{value}'"), + }; + } +} + +public class WordsYamlException(string path, string message, Exception inner = null) + : Exception($"{path}: {message}", inner) { } diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs new file mode 100644 index 000000000..64ffbfc3d --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/BatchCommand.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.IO; +using ManyConsole; + +namespace SIL.Machine.Morphology.HermitCrab; + +/// +/// Parses every word in a word list and records a per-word result signature plus elapsed time, so two +/// runs (e.g. before/after an engine change on this branch) can be diffed to confirm parse results are +/// unchanged. Flushed per word and crash-resumable via --start, since some corpus words are expensive +/// enough (this branch is plain master, with none of the hc-rustify or parse-optimization speedups, so a +/// handful of words can take minutes) that losing partial progress on a long corpus run is unacceptable. +/// +/// Ported from the parse-opt worktree's BatchCommand.cs, with the --rule-stats and --parallel options +/// removed: those depend on Morpher.AccumulateRuleStats / AnalysisRuleStats / SynthesisRuleStats and +/// Morpher.MaxDegreeOfParallelism, which are parse-optimization-branch-only additions (#446/#451) that do +/// not exist on this branch's plain-master Morpher. +/// +internal class BatchCommand : ConsoleCommand +{ + private readonly HCContext _context; + private int _startIndex; + + public BatchCommand(HCContext context) + { + _context = context; + + IsCommand("batch", "Parses every word in a word list, recording a result signature and timing per word"); + SkipsCommandSummaryBeforeRunning(); + HasAdditionalArguments(2, " "); + HasOption("start=", "0-based line index to resume at (for crash recovery)", v => _startIndex = int.Parse(v)); + } + + public override int Run(string[] remainingArguments) + { + string wordListPath = remainingArguments[0]; + string outputPath = remainingArguments[1]; + + string[] words = SignatureFormat.LoadWords(wordListPath); + + return RunSequential(words, outputPath); + } + + private int RunSequential(string[] words, string outputPath) + { + using var writer = new StreamWriter(outputPath, append: _startIndex > 0) { AutoFlush = true }; + var totalSw = Stopwatch.StartNew(); + long parsed = 0, + skipped = 0; + for (int i = _startIndex; i < words.Length; i++) + { + string word = words[i]; + // Sentinel written before the attempt: if this word crashes the process, a wrapper script + // can read the last line to find where to resume. + writer.WriteLine($"{i}\t{word}\tSTARTED"); + (string status, long elapsedMs, string signature) = SignatureFormat.ParseOneWord(_context.Morpher, word); + writer.WriteLine($"{i}\t{word}\t{elapsedMs}\t{status}\t{signature}"); + if (status == "SKIPPED") + skipped++; + else + parsed++; + if (i % 100 == 0) + _context.Out.WriteLine("[{0}/{1}]", i, words.Length); + } + totalSw.Stop(); + _context.Out.WriteLine( + "batch complete: {0} words parsed ({1} skipped), {2}ms total", + parsed, + skipped, + totalSw.ElapsedMilliseconds + ); + return 0; + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/Program.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/Program.cs index ff8e86bcc..45397e013 100644 --- a/src/SIL.Machine.Morphology.HermitCrab.Tool/Program.cs +++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/Program.cs @@ -92,9 +92,11 @@ public static int Main(string[] args) new TracingCommand(context), new TestCommand(context), new StatsCommand(context), + new BatchCommand(context), }; string input; + int exitCode = 0; if (!string.IsNullOrEmpty(scriptFile)) { using (var scriptReader = new StreamReader(scriptFile)) @@ -105,7 +107,12 @@ public static int Main(string[] args) if (!input.Trim().StartsWith("#") && input.Trim() != "") { string[] cmdArgs = SplitCommandLine(input); - ConsoleCommandDispatcher.DispatchCommand(commands, cmdArgs, context.Out); + int result = ConsoleCommandDispatcher.DispatchCommand(commands, cmdArgs, context.Out); + // Remember the first nonzero result rather than the last: once a script + // line fails, later lines running (or not) shouldn't paper over that with + // a subsequent success. + if (result != 0 && exitCode == 0) + exitCode = result; } input = scriptReader.ReadLine(); } @@ -133,10 +140,15 @@ public static int Main(string[] args) output?.Close(); - return 0; + return exitCode; } - private static string[] SplitCommandLine(string commandLine) + /// Splits a command line into tokens, honoring double- and single-quoted segments. + /// Shared with SIL.Machine.Morphology.HermitCrab.Conformance's AdapterEngine via + /// InternalsVisibleTo so both consumers tokenize the exact same way instead of + /// maintaining a second, divergent implementation (the Conformance harness's own copy did not + /// handle single quotes). + internal static string[] SplitCommandLine(string commandLine) { var parmChars = commandLine.ToCharArray(); var inSingleQuote = false; diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/Properties/AssemblyInfo.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..d5bf0eece --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Runtime.CompilerServices; + +// Lets the conformance harness (SIL.Machine.Morphology.HermitCrab.Conformance, assembly name +// "hc-conformance" -- see its .csproj's ) call this project's internal +// Program.SplitCommandLine, so both share one command-line tokenizer instead of the harness +// maintaining a second, divergent copy. +[assembly: InternalsVisibleTo("hc-conformance")] diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/SignatureFormat.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/SignatureFormat.cs new file mode 100644 index 000000000..1f1c5c0e0 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/SignatureFormat.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace SIL.Machine.Morphology.HermitCrab; + +/// +/// The `batch` word-list loading, per-word parse, and signature algorithm, factored out of +/// so the conformance harness's self-check mode +/// (SIL.Machine.Morphology.HermitCrab.Conformance) computes byte-identical results from the +/// same code, rather than a second copy that could silently drift from the oracle. See +/// conformance/PROTOCOL.md sections 2-3 for the documented, engine-agnostic description of +/// the TSV format and this signature algorithm. +/// +public static class SignatureFormat +{ + /// Reads a word list file: one word per line, trimmed, blank lines dropped. + public static string[] LoadWords(string path) + { + return File.ReadAllLines(path).Select(w => w.Trim()).Where(w => w.Length > 0).ToArray(); + } + + /// + /// Parses one word, returning its batch-TSV status/elapsed-ms/signature triple. Only + /// is caught here (yielding ("SKIPPED", 0, "-"), per + /// conformance/PROTOCOL.md section 2) -- any other exception propagates, since that's a + /// genuine engine crash, not a normal per-word outcome. + /// + public static (string status, long elapsedMs, string signature) ParseOneWord(Morpher morpher, string word) + { + var sw = Stopwatch.StartNew(); + try + { + Word[] results = morpher.ParseWord(word, out _).ToArray(); + sw.Stop(); + return ("ok", sw.ElapsedMilliseconds, BuildSignature(results)); + } + catch (InvalidShapeException) + { + return ("SKIPPED", 0, "-"); + } + } + + /// + /// Builds the order-independent, sorted, semicolon-joined signature for a set of parse + /// results: one morph+morph|shape entry per distinct analysis, or "-" if empty. + /// + public static string BuildSignature(IEnumerable results) + { + List signatures = results + .Select(w => + string.Join("+", w.AllomorphsInMorphOrder.Select(a => a.Morpheme.Id)) + + "|" + + w.Shape.ToRegexString(w.Stratum.CharacterDefinitionTable, true) + ) + .OrderBy(s => s, StringComparer.Ordinal) + .ToList(); + return signatures.Count == 0 ? "-" : string.Join(";", signatures); + } +} diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/GuesserSignatureTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/GuesserSignatureTests.cs new file mode 100644 index 000000000..4bc31ea1d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/GuesserSignatureTests.cs @@ -0,0 +1,167 @@ +using NUnit.Framework; + +namespace SIL.Machine.Morphology.HermitCrab; + +// This test project does not (and should not, to avoid an unnecessary dependency) reference +// SIL.Machine.Morphology.HermitCrab.Tool, so it can't call the real SignatureFormat.BuildSignature +// directly -- this is a byte-for-byte transcription of that method (see +// src/SIL.Machine.Morphology.HermitCrab.Tool/SignatureFormat.cs), kept here ONLY so this test can +// assert on the exact string a real batch/self-check run would produce. +file static class MiniSignatureFormat +{ + public static string BuildSignature(IEnumerable results) + { + List signatures = results + .Select(w => + string.Join("+", w.AllomorphsInMorphOrder.Select(a => a.Morpheme.Id)) + + "|" + + w.Shape.ToRegexString(w.Stratum.CharacterDefinitionTable, true) + ) + .OrderBy(s => s, StringComparer.Ordinal) + .ToList(); + return signatures.Count == 0 ? "-" : string.Join(";", signatures); + } +} + +/// +/// Pins the exact rendering of a LexicalGuess (Guesser) parse in the 5-column batch +/// signature format (SignatureFormat.BuildSignature, transcribed locally below as +/// ), for +/// docs/conformance-language-suite-plan.md phase G1's "guess-stem rendering" deliverable. +/// +/// Two findings this test documents (see conformance/PROTOCOL.md's guess-stem addendum for the +/// full writeup): +/// +/// The signature's morph-chain component for a guessed root is literally the guessed +/// surface substring itself, e.g. "foo" -- Morpher.LexicalGuess (private) constructs a +/// throwaway with Id = shapeString (the matched text), and the +/// signature algorithm walks AllomorphsInMorphOrder reading each allomorph's +/// Morpheme.Id -- so a guessed root has no distinguishing marker at all in the signature +/// string; it is indistinguishable, by string alone, from a REAL lexical entry that happened to +/// have the identical id. +/// BatchCommand (the CLI `batch` subcommand every conformance adapter/oracle +/// invocation goes through) can NEVER produce a guess-stem parse: SignatureFormat.ParseOneWord +/// calls the 2-argument Morpher.ParseWord(word, out trace) overload, which hardcodes +/// guessRoot: false. No CLI command in SIL.Machine.Morphology.HermitCrab.Tool exposes the +/// 3-argument, guessRoot: true overload at all. This test therefore calls +/// directly -- the only way to exercise +/// this path in this codebase today. +/// +/// +[TestFixture] +public class GuesserSignatureTests +{ + private const string GrammarXml = """ + + + + + GuesserSignatureProbe + + n + + + Main + + f + o + s + + + + Any + + + + Main + + + plural + + + + s + + + PL + + + + + + [Any]* + + + + + + + """; + + private static string WriteTempGrammar() + { + string path = Path.Combine(Path.GetTempPath(), "hc-guesser-signature-" + Guid.NewGuid().ToString("N") + ".xml"); + File.WriteAllText(path, GrammarXml); + return path; + } + + [Test] + public void BatchCommandPath_NeverGuesses_ZeroResultsForAnUnknownRoot() + { + // "foos" has no real lexical entry for "foo" -- only the guess pattern exists. The + // 2-argument overload (what SignatureFormat.ParseOneWord, and therefore every `batch` + // invocation, actually calls) must find nothing. + string path = WriteTempGrammar(); + try + { + Language language = XmlLanguageLoader.Load(path); + var morpher = new Morpher(new TraceManager(), language); + + Word[] results = morpher.ParseWord("foos", out _).ToArray(); + + Assert.That(results, Is.Empty); + Assert.That(MiniSignatureFormat.BuildSignature(results), Is.EqualTo("-")); + } + finally + { + File.Delete(path); + } + } + + [Test] + public void GuessRootTrue_RendersGuessedStemAsItsOwnLiteralSurfaceText() + { + string path = WriteTempGrammar(); + try + { + Language language = XmlLanguageLoader.Load(path); + var morpher = new Morpher(new TraceManager(), language); + + Word[] results = morpher.ParseWord("foos", out _, guessRoot: true).ToArray(); + + Assert.That(results, Is.Not.Empty); + string signature = MiniSignatureFormat.BuildSignature(results); + // Pinned rendering: TWO guesses, because the guess pattern also matches the WHOLE + // surface string as a bare, unaffixed root (the pattern "[Any]*" is tried against every + // analysis-direction candidate, including the one where mrPlural was never unapplied at + // all) -- guessing is not restricted to "the largest/most plausible" segmentation: + // - "foo+PL|foos": PL ("-s") unapplied first, "foo" guessed as the root. + // - "foos|foos": no rule unapplied at all, the entire string "foos" guessed as the root. + // Both confirm the core finding either way: a guessed root's morph-chain component is + // the literal guessed surface substring itself ("foo", or "foos"), exactly as if a real + // LexEntry with that Id had matched -- a guess parse carries no distinguishing marker in + // the signature string itself. + Assert.That(signature, Is.EqualTo("foo+PL|foos;foos|foos")); + } + finally + { + File.Delete(path); + } + } +}