This file says how to change the suite and what to run before the change leaves your machine. It applies to
this repository, which engines (sillsdev/machine's C# HermitCrab and PanGloss) consume as a clone pinned by
commit, not as a submodule. Nothing here assumes any engine's build, layout or language. Commands run from the root of this
repository; python3 is the only tool the checks need.
If this file disagrees with .github/workflows/, the scripts in scripts/, or the files you are changing,
prefer the executable behaviour and say so.
The suite covers only grammars a real FieldWorks project can produce. FieldWorks builds every grammar
through HCLoader.cs (Src/LexText/ParserCore/HCLoader.cs in the FieldWorks repository), and that loader
reaches less than the HermitCrab XML can say. A grammar that needs a construct the loader never writes does
not belong here, however expressible it is in the XML and however well an engine handles it.
- Before you author, change or keep a fixture, check each construct it uses against the table in
docs/fieldworks-cannot-say.md, against the census
fieldworks-writable-shapes.tsv(every DTD member, with theHCLoader.csline that writes it, only labels it, or never writes it), and against PROTOCOL.md section 9. - The standard is the round trip: a real FieldWorks project, put through the loader, yields the grammar. Reading
the loader source is weaker evidence.
fieldworks_producible: trueis a claim read from the loader until the fixture has a FieldWorks witness (section 3), and only the 10 fixtures that carry one are certified today (of 135; none of the 16 stress fixtures has one). Say which of the two you are relying on in the pull request. - If the grammar needs a construct in that table, stop. Rewrite the grammar without it or do not add it.
Do not relax
fieldworks_producible: every fixture must carrytrue, andscripts/check_words_schema.pyand the consuming engines' gates fail on anything else. - A new construct goes into
fieldworks-writable-shapes.tsv, with its loader line, before a fixture may use it.
A fixture is a directory under languages/ or edge-cases/ holding grammar.xml and words.yaml, and
optionally a README.md and a fieldworks/ witness. docs/what-it-is.md and
docs/consuming.md ("Consuming the suite from another repository") describe the files; schema/words.schema.json and
HermitCrabInput.dtd validate them.
- Check producibility (section 1). Name the
HCLoader.csfunction that writes each construct you rely on. - Write
grammar.xmlandwords.yaml. Putfieldworks_producible: truedirectly afterrequires. Give each word anotethat says what it pins. - Take the expected rows from an engine run, never from memory. Run the consuming engine's self-check or
adapter with a
--propose-style option if it has one (sillsdev/machine's runner does), or run an engine through thebatchcontract in PROTOCOL.md and read its TSV. Read every row against the rules before you accept it. A row the engine produces and you cannot explain is a finding, not a pin. Record the build you took the rows from in the fixture'swords.yamlheader (# oracle-provenance: ...) and, if it is a new build, add it to PROVENANCE.md. - Update docs/grammars.md: one bullet per fixture, in the group it belongs to, in the
form
- `edge-cases/name`: one short line.scripts/check_grammars_md.pyfails if a fixture is missing, listed twice or gone. - If you ruled a construct out, add a row to the table in docs/fieldworks-cannot-say.md named in section 1 and, when the loader
verdict changed, update
fieldworks-writable-shapes.tsv. Cite theHCLoader.csfunction and line, and the FieldWorks commit you read. If you delete a fixture for this reason, add its row to the table in docs/engine-only-fixture-retirement.md. - Regenerate the manifest (section 2a), then read the
git diff. The manifest should move only for the fixtures you touched. - Delete a fixture by removing its directory and its line in
docs/grammars.md, then regenerating. Remove adead-rules-allowed.tsvline that names it. - Tell the consuming engines. sillsdev/machine and PanGloss each pin a commit of
maininconformance.commit, so a change reaches an engine when it bumps that pin. Their own self-check is regenerated on their side, not here. The order of the pull requests, and what each pin names at each step, is in .claude/skills/land-a-conformance-change/SKILL.md.
| File | Written by | Checked by |
|---|---|---|
generated/hc-conformance-manifest.v2.json |
python3 scripts/generate_manifest.py |
python3 scripts/generate_manifest.py --check, and CI |
generated/stress-manifest.v2.json |
the same command | the same --check, and CI |
coverage.csv, rules.csv |
the consuming engine's coverage report (sillsdev/machine's runner: --coverage-report) |
that engine's CI; not yet checked here |
- The stress manifest lists
stress/edge-cases/*in the same layout and is kept apart from the conformance manifest, which consumers pin: changing a stress fixture changes only the stress manifest. - The manifest hashes
grammar.xmlandwords.yamlof every fixture, the DTD,constructs.txt,parity-check.pyand the two schemas. Editing a fixtureREADME.md, a file underdocs/, or this file changes nothing generated. coverage.csvandrules.csvare derived fromwords.yamlandconstructs.txt. Until a Python generator exists here, regenerate them with the consuming engine's runner and commit the result in this repository.
| File | Change it when |
|---|---|
dead-rules-allowed.tsv |
a fixture declares a rule on purpose that no word exercises (add a row with the reason), or a word now exercises an allowed rule (delete the row). |
constructs.txt |
the checklist of constructs gains or loses an entry. |
fieldworks-writable-shapes.tsv |
the loader census changes (section 1). |
PROVENANCE.md |
a new engine build supplied ground truth. |
A fieldworks_producible: true fixture may carry a fieldworks/ directory with a real FieldWorks project.
PROTOCOL.md section 10 defines it. This repository stores witnesses and checks their shape;
it never opens one. The PanGloss xample-projector tool produces and reads them; it stays in PanGloss.
- Add one only when a real project backs the claim. Do not hand-edit
project.fwdata. - The directory holds exactly
project.fwdata,phonology-mutations.yamlandWritingSystemStore/, andWritingSystemStore/holds only.ldmlfiles. - Keep each
.ldmlminimal: only the elements that define the writing system (identity,layout,characters,delimiters,collations,special). Remove the CLDR locale data LibLCM copies in (dates, numbers, units, display names); nothing reads it. Keep thegenerationdate at the value the other witnesses use, so identical writing systems stay byte-identical. phonology-mutations.yamlcarriesversion: 1and abase_sha256equal to the SHA-256 ofproject.fwdata. Recompute it whenever the project changes.scripts/check_fwdata.pychecks it..gitattributesmarks*.fwdatabinary: its exact bytes are hashed, so do not let an editor or a checkout change its line endings.- No file may carry a username, email address, or local path.
scripts/privacy_lint.pyfails on them.
Run each command from the repository root and read the output. Do not push on a partial run.
python3 scripts/generate_manifest.py --check
python3 scripts/check_words_schema.py
python3 scripts/check_dtd.py
python3 scripts/check_grammars_md.py
python3 scripts/check_fwdata.py
python3 parity-check.py
python3 scripts/check_links.py
python3 scripts/privacy_lint.py
generate_manifest.py --check covers both manifests, check_grammars_md.py covers both fixture lists (and the
stress README table), and check_fwdata.py covers stress witnesses as well as fixture witnesses.
check_words_schema.pyneeds PyYAML and jsonschema, andcheck_dtd.pyneeds lxml orxmllint; CI installs them (pip install -r scripts/requirements.txt). The other scripts use the standard library only.- Then run the self-check of each consuming engine you can reach (for sillsdev/machine,
--fixtures <path to this clone> --include-pathological, and--fixtures <path to this clone>/stressfor the stress fixtures; itsconformance/is a clone at its own pinned commit, so it does not see your change, and its tests read this clone whenHC_CONFORMANCE_SUITE=<path to this clone>is set). This repository cannot run an engine; CI here proves the files are well formed, not that an engine agrees with them.
The grammars under stress/edge-cases/ measure the cost of an engine's analysis pass. They are not
conformance fixtures, and this section says how they differ from sections 1 to 4.
What stays the same. The one rule applies: only what FieldWorks can produce, with the same evidence
(section 1) and the same meaning of fieldworks_producible: true (a claim read from the loader until a
fieldworks/ witness exists, section 3). Every stress fixture carries the flag; none has a witness yet (0 of
16), and stress/README.md states the count.
What differs.
| Conformance fixture | Stress fixture | |
|---|---|---|
| Directory | languages/<name>/ or edge-cases/<name>/ |
stress/edge-cases/<name>/ |
| Expected output means | what the oracle engine does, read against the rules; each word pins one behaviour | parity: an optimization must return the same parses; the words are a workload, and a changed row is the failure |
| Measure | pass or fail | work steps, never wall time; budget_ms only guards against a hang |
words.yaml starts with |
front matter | # kind: stress (...), then fieldworks_producible: true and budget_ms |
exercises, coverage, rules.csv, dead-rules-allowed.tsv |
yes | none; no ledger counts them |
Fixture README.md |
optional | none; stress/README.md describes every fixture in one table |
| Listed in | docs/grammars.md bullets, generated/hc-conformance-manifest.v2.json |
the docs/grammars.md "Stress grammars" bullets, the stress/README.md table and generated/stress-manifest.v2.json |
| Run by | the consumer's default runner | only on request, through the consumer's opt-in stress category |
Adding or changing a stress fixture.
- Check producibility as in section 1 and name the
HCLoader.csfunction that writes each construct. Say in the pull request whether a witness exists (it will not). - Write
stress/edge-cases/<name>/grammar.xmlandwords.yaml. Startwords.yamlwith# kind: stress (timing and optimization parity fixture, not conformance; see stress/README.md), thenfieldworks_producible: trueandbudget_ms. In the header comment say what the grammar models and what it really exercises; do not name a script or a switch that is not in a repository. - Take the expected rows from an engine run, never from memory: three identical runs of the reference engine
(sillsdev/machine
masterfor the existing fixtures). Record the build in the header (# oracle-provenance: ...) and, if it is new, in PROVENANCE.md. Read each row against the grammar. A row you cannot explain is a finding to file, not a pin. A word an engine in the field renders differently frommasteris left out and named instress/README.md(seepolundanasak). - Change an existing row only when the correct behaviour of the reference engine changed, and say which change (for example FLEx-style null-affix boundaries) in the pull request. An optimization that moves a row has failed; it is not a reason to edit the row.
- Add a row to the table in stress/README.md: what the fixture really exercises (checked against the words and the grammar, not copied from the commit that added it), the word count, the no-parse count, and the sillsdev/machine issue or pull request by number. Never cite a local file for a machine record; cite the issue. Update the witness sentence if a witness was added.
- Add a bullet to the "Stress grammars" section of docs/grammars.md and keep its count sentence
true. Update the counts in docs/README.md (
stress/ ... 16 timingand "The 16 timing grammars") and the numbers line of the root README. - Run
python3 scripts/generate_manifest.pyand read the diff. Onlygenerated/stress-manifest.v2.jsonmay move; if the conformance manifest moves, you changed something it hashes. - Delete a fixture by removing its directory, its README row and its bullet, and regenerating. Rename one only
with the consumers in step 9: the directory name
stress/and the fixture names are read by sillsdev/machine. - Tell the consumers (section 2, step 8). sillsdev/machine's stress test and runner discover the directory themselves; a rename or a changed row there is a bump of its pin.
stress/README.md shows how a consumer runs the fixtures; this repository cannot, so CI proves only that the files are
well formed, listed, and hashed.