Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Machine.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
263 changes: 263 additions & 0 deletions conformance/PROTOCOL.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -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/<name>/ grammar.xml + words.yaml -- 8 typologically-selected synthetic languages
edge-cases/<name>/ 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 "<command template>"

# 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.
62 changes: 62 additions & 0 deletions conformance/adapters/hc-dotnet-wrapper.sh
Original file line number Diff line number Diff line change
@@ -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 <grammar.xml> <words.txt> <output.tsv> [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 <grammar.xml> <words.txt> <output.tsv> [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
43 changes: 43 additions & 0 deletions conformance/constructs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# HermitCrab grammar-model construct checklist
#
# This is the coverage-report key set: every conformance/languages/<name>/words.yaml and
# conformance/edge-cases/<name>/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)
Loading
Loading