Skip to content
Open
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
16 changes: 7 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,13 @@ releases may contain breaking changes.
- Validation: `validate_file()` / `iter_problems()` /
`Lexicon.iter_problems()` returning a `Problem` stream, each carrying the
file, entry, and line it concerns. RELAX NG layer with two documented
deviations from raw libxml2 (href masking with `uri-not-rfc` warnings;
tag-grouped validation to sidestep libxml2's interleave limitation); vendored
ranges schema over companions; semantic checks: duplicate-guid (entries, and
ranges/range-elements within their own document — matching the C#
`Validator`'s document-wide guid scan), dangling-ref, range-parent,
undefined-range-value (every grammatical-info and range-keyed trait
reachable from an entry, however nested \+ NFC-normalized),
duplicate-form-lang, missing-media, dangling-ranges-href, and (opt-in via
`require_ids`) missing-id.
departures from strict validation (invalid `file://` hrefs downgraded to
`uri-not-rfc` warnings; legal interleaving not falsely flagged); vendored
ranges schema over companions; and nine semantic checks the grammar cannot
express, one `Problem` code each (with missing-id opt-in via `require_ids`).
Names resolve against range and range-element ids under NFC; a match that
needed normalizing is reported as normalization-mismatch, once per id.
Every code is described in `docs/en/guides/validate.md`.
- Canonical sort: `Lexicon.sort()` / `RangesFile.sort()` (entries by
case-folded guid/id, ranges/range-elements by id, field definitions by tag;
informed by the C# LiftSorter, locale-independent) and
Expand Down
2 changes: 1 addition & 1 deletion docs/en/csharp-differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The C# `Validator` runs one RELAX NG pass and reports the first errors as string

- **Invalid URIs are warnings, not errors.** The C# RELAX NG engine never enforced the `anyURI` datatype, so FieldWorks (FLEx) has been writing `file://C:/...` hrefs into real lexicons for years. Rejecting those files would flag virtually every FLEx export.
- **Schematron rules are enforced** (as semantic checks): duplicate form languages and similar co-constraints in the LIFT grammar were silently ignored by both C# and raw lxml validation.
- **Cross-file comparisons are Unicode-normalized**, because FLEx writes the `.lift` in NFC and the companion `.lift-ranges` in NFD.
- **Range id comparisons are Unicode-normalized** (NFC), because FLEx's own export is not internally consistent: it normalizes to NFC on the way out, but a few writes bypass that step and emit the NFD it holds in memory. A `grammatical-info` or `lexical-relation` range-element `id` can therefore be NFD while its labels, the `parent` attribute on that same element, and the `.lift` value referring to it are all NFC. References that resolve only after normalizing are reported as `normalization-mismatch` warnings — a check with no C# counterpart — so the encoding split stays visible to anyone whose own comparisons are exact.

sil-lift also validates the `.lift-ranges` companions of a loaded lexicon against a schema for standalone ranges documents (vendored from `lift-standard` alongside the base LIFT grammar) — every tracked external ranges file is checked whenever the `.lift` is validated — with no such schema (or check) in the C# world. (There is no entry point for validating a `.lift-ranges` file on its own, detached from a `.lift`.)

Expand Down
2 changes: 1 addition & 1 deletion docs/en/guides/lift-export-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The companion carries each range's full definition. Values are `<range-element>`
</lift-ranges>
```

An entry then refers to a value by id: a sense's part of speech is `<grammatical-info value="Noun"/>`, and a semantic domain is `<trait name="semantic-domain-ddp4" value="1.6.1.2"/>`. `sil-lift validate` warns (`undefined-range-value`) when a value isn't defined in its range and errors (`range-parent`) when a `parent` isn't a sibling id — so emit the ranges your data actually uses. See also [Ranges and media](folder-media.md).
An entry then refers to a value by id: a sense's part of speech is `<grammatical-info value="Noun"/>`, and a semantic domain is `<trait name="semantic-domain-ddp4" value="1.6.1.2"/>`. `sil-lift validate` warns (`undefined-range-value`) when a value isn't defined in its range and errors (`range-parent`) when a `parent` isn't a sibling id — so emit the ranges your data actually uses. Those comparisons are NFC-normalized, so an id and the value or `parent` referring to it may differ in Unicode normalization — that difference is a `normalization-mismatch` warning rather than an error, but write one consistent normalization if you can: consumers that compare raw strings will not resolve those references. See also [Ranges and media](folder-media.md).

If you build the export in Python, `Lexicon.add_ranges_file()`, `RangesFile.add_range()`, and `Range.add_element()` construct the companion and add the header references for you; `open_writer(..., ranges=...)` does the same on the streaming path.

Expand Down
29 changes: 25 additions & 4 deletions docs/en/guides/validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,40 @@ lex = sil_lift.load("dictionary.lift")
problems = list(lex.iter_problems())
```

Each `Problem` carries `level` (`"error"`/`"warning"`), a stable `code`, `message`, and an address: `file`, `entry_id`, `guid`, `line`.
Each `Problem` carries `level` (`"error"`/`"warning"`), a stable `code`, `message`, and as much of an address as the finding has: `file` (`None` when the lexicon has no path), `entry_id` when it concerns one entry, `guid` when the object it concerns has one (an entry, or a range-element), and `line` when it maps to a line in the document. A finding about a range is addressed to the `.lift-ranges` companion that defines it, and carries no entry. Unset fields are `None` — `null` in `--format json`, where every key is always present.

## The layers

1. **RELAX NG** against the LIFT 0.13 grammar (vendored from lift-standard — a byte-identical copy committed into this package).
2. **Ranges schema** — this project's `lift-ranges-0.13.rng` — over every tracked `.lift-ranges` companion.
3. **Semantic checks** the grammar cannot express: `duplicate-guid`, `dangling-ref`, `range-parent`, `undefined-range-value`, `duplicate-form-lang`, `missing-media`.
2. **Ranges schema** — this project's `lift-ranges-0.13.rng` — over every tracked `.lift-ranges` companion, addressed to the companion rather than the `.lift`.
3. **Semantic checks** the grammar cannot express — nine of them, one code each.

## Problem codes

Every finding carries one of these, whichever layer produced it — `schema` and `uri-not-rfc` come from the schema layers, the other nine are semantic checks. The strings are a supported interface; `--strict` promotes every warning to an error.

| code | level | what it flags |
| ------------------------ | ------- | -------------------------------------------------------------------------- |
| `dangling-ranges-href` | warning | a header `range/@href` resolving to no companion file |
| `dangling-ref` | error | a `relation/@ref` or `variant/@ref` matching no entry or sense |
| `duplicate-form-lang` | warning | two forms in one multitext sharing a language |
| `duplicate-guid` | error | a guid reused among entries, or among one document's ranges/range-elements |
| `missing-id` | error | opt-in via `require_ids`: an entry without a guid, a sense without an id |
| `missing-media` | warning | a referenced audio or picture file not on disk |
| `normalization-mismatch` | warning | a name that reaches the id it refers to only under NFC |
| `range-parent` | error | a `range-element/@parent` no sibling id defines |
| `schema` | error | a RELAX NG grammar violation, in the `.lift` or in a companion |
| `undefined-range-value` | warning | a grammatical-info or range-keyed trait value the range does not list |
| `uri-not-rfc` | warning | an href that is not a valid URI — FLEx's `file://C:/...` |

## Real-world FieldWorks (FLEx) output

FieldWorks systematically writes some content that strict tooling rejects. Here is sil-lift's policy, so that real lexicons validate usefully:

- `file://C:/...` hrefs (invalid URIs) are reported as **warnings** (`uri-not-rfc`), not schema errors — the C# validator never rejected them.
- Legally interleaved children (e.g. `field, note, field, note` in a sense) are **not** flagged, working around a false positive in libxml2.
- Range values are compared under Unicode NFC normalization — FLEx writes the `.lift` in NFC but the `.lift-ranges` in NFD within the same export.
- FLEx's `trait`/`field` extensions inside `range-element` **are** reported (schema errors against the ranges schema): they are genuine spec deviations.
- Names are resolved against range and range-element `id`s under Unicode **NFC normalization** — `parent` links, range values, and the `trait` name or header `range` id that keys a range. FLEx normalizes to NFC on export, but a few writes bypass that step, so a range-element `id` can be NFD while its labels, its own `parent`, and the `.lift` values naming it are NFC.
- Compared exactly, a sound export looks broken — and a range whose `id` is spelled the other way goes unchecked entirely, since a trait name that reaches no range is silently accepted.
- A name that matched only after normalizing is reported as a `normalization-mismatch` **warning**, once per id however many references differ, addressed to the file that defines it. The data is sound, but a consumer comparing raw strings — a Send/Receive merge, say — will not resolve those references.
- The ids are never rewritten: the file keeps the spellings it came with.
Loading