Summary
The semantic range-parent check compares a range-element's parent against its
siblings' ids as exact strings. FieldWorks (FLEx) writes those two attributes with
different Unicode normalization, so a perfectly well-formed parent link in a real
FLEx export is reported as an error.
Evidence (corpus)
tests/corpus/large/sango/sango.lift-ranges — both hits are the same element, aliased
under FLEx's two names for its part-of-speech list:
| range |
child @id |
@parent |
matches after NFC |
grammatical-info |
Complément du lieu (NFD) |
Compléments (NFC) |
yes |
from-part-of-speech |
Complément du lieu (NFD) |
Compléments (NFC) |
yes |
The sibling those parents refer to exists; its id is Compléments (NFD).
tests/test_validate.py currently locks these in as "two genuinely dangling
range-element parents" — they are not dangling.
Root cause
FLEx holds strings as NFD in memory and normalizes to NFC on export, but a handful of
writes bypass the normalizing helpers (MakeSafeAndNormalizedXml /
MakeSafeAndNormalizedAttribute in Src/LexText/LexTextControls/LiftExporter.cs) and
emit raw NFD. The grammatical-info range-element id is one of them
(LiftExporter.cs:1738) while the parent written two lines later
(LiftExporter.cs:1744) goes through the normalizer — hence the asymmetry within a
single element. Same for lexical-relation (:1775/:1781).
Other known bypass sites, for completeness: morph-type range-element id (:2173,
which also skips XML escaping), leading-symbol / trailing-symbol traits
(:2180/:2185), stem-name feature-set trait (:2294), reversal-type id
(:1954).
Our docs state this imprecisely
Three places say FLEx writes the .lift in NFC and the .lift-ranges in NFD:
docs/en/csharp-differences.md (Validation)
docs/en/guides/validate.md (Real-world FieldWorks output)
- the comment above the
undefined-range-value check in src/sil_lift/_validate.py
That is not what the corpus shows. In sango.lift-ranges every label, abbrev,
description, and every @parent is NFC; only certain range-element @ids are NFD
(grammatical-info, from-part-of-speech, lexical-relation — while
semantic-domain-ddp4, translation-type, and usage-type ids are NFC). It is
specific writes that bypass the normalizer, not the file.
Fix
- NFC-normalize the
range-parent comparison, the way the undefined-range-value
check already normalizes range values (report the original spelling in the message).
- Correct the three prose/comment sites to describe the mechanism.
- Update the sango expectation and add a regression test for an NFD id with an NFC
parent; note the asymmetry in tests/corpus/PROVENANCE.md next to the existing
duplicate-guid finding.
Summary
The semantic
range-parentcheck compares arange-element'sparentagainst itssiblings'
ids as exact strings. FieldWorks (FLEx) writes those two attributes withdifferent Unicode normalization, so a perfectly well-formed parent link in a real
FLEx export is reported as an error.
Evidence (corpus)
tests/corpus/large/sango/sango.lift-ranges— both hits are the same element, aliasedunder FLEx's two names for its part-of-speech list:
@id@parentgrammatical-infoComplément du lieu(NFD)Compléments(NFC)from-part-of-speechComplément du lieu(NFD)Compléments(NFC)The sibling those
parents refer to exists; itsidisCompléments(NFD).tests/test_validate.pycurrently locks these in as "two genuinely danglingrange-element parents" — they are not dangling.
Root cause
FLEx holds strings as NFD in memory and normalizes to NFC on export, but a handful of
writes bypass the normalizing helpers (
MakeSafeAndNormalizedXml/MakeSafeAndNormalizedAttributeinSrc/LexText/LexTextControls/LiftExporter.cs) andemit raw NFD. The
grammatical-inforange-elementidis one of them(
LiftExporter.cs:1738) while theparentwritten two lines later(
LiftExporter.cs:1744) goes through the normalizer — hence the asymmetry within asingle element. Same for
lexical-relation(:1775/:1781).Other known bypass sites, for completeness:
morph-typerange-elementid(:2173,which also skips XML escaping),
leading-symbol/trailing-symboltraits(
:2180/:2185), stem-namefeature-settrait (:2294),reversal-typeid(
:1954).Our docs state this imprecisely
Three places say FLEx writes the
.liftin NFC and the.lift-rangesin NFD:docs/en/csharp-differences.md(Validation)docs/en/guides/validate.md(Real-world FieldWorks output)undefined-range-valuecheck insrc/sil_lift/_validate.pyThat is not what the corpus shows. In
sango.lift-rangeseverylabel,abbrev,description, and every@parentis NFC; only certain range-element@ids are NFD(
grammatical-info,from-part-of-speech,lexical-relation— whilesemantic-domain-ddp4,translation-type, andusage-typeids are NFC). It isspecific writes that bypass the normalizer, not the file.
Fix
range-parentcomparison, the way theundefined-range-valuecheck already normalizes range values (report the original spelling in the message).
parent; note the asymmetry in
tests/corpus/PROVENANCE.mdnext to the existingduplicate-guidfinding.