You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lexicon.load() raises LiftParseError and hands back nothing when a companion candidate exists but is not a .lift-ranges document. One unusable file costs the whole lexicon, including its entries — where every other candidate that cannot serve as a companion is skipped, and the reference reported as dangling-ranges-href.
The realistic trigger needs no unusual href at all: a zero-byte or truncated Dict.lift-ranges beside Dict.lift — an interrupted export, a failed sync, a partial checkout — makes the lexicon unloadable.
Reproduction
Sibling only, no href involved:
(folder/"Dict.lift").write_bytes(intact_lift)
(folder/"Dict.lift-ranges").write_bytes(b"") # or a truncated copysil_lift.load(folder/"Dict.lift")
zero-byte sibling LiftParseError: ...\pkg\Dict.lift-ranges: not well-formed XML: Document is empty, line 1, column 1
truncated sibling LiftParseError: ...\pkg\Dict.lift-ranges: not well-formed XML: ...
intact sibling loaded ok, ranges_files=['Dict.lift-ranges']
Any header range/@href resolving to any existing non-ranges file does the same. Exact spellings throughout — no case folding involved:
another .lift LiftParseError: ...\pkg\Other.lift: root element is <lift>, expected <lift-ranges>
a text file LiftParseError: ...\pkg\notes.txt: not well-formed XML: Start tag expected, ...
a png LiftParseError: ...\pkg\pictures.png: not well-formed XML: Start tag expected, ...
well-formed XML, wrong root LiftParseError: ...\pkg\stuff.xml: root element is <stuff>, expected <lift-ranges>
Nothing between the existence test and the load asks whether the file is a ranges document. RangesFile.load decides, and its refusal propagates straight out of Lexicon.load.
One file is already exempt: the lexicon itself. #19 added a _same_file check for it precisely because a case-variant href folding onto the .lift took the whole load down, and settled on skipping the candidate and letting dangling-ranges-href report the reference. That establishes the principle — a candidate that exists but cannot serve as a companion is skipped and reported, not fatal. It just was not extended past the lexicon: a second .lift in the folder, or any other file, still ends the load.
Consequences
sil-lift validate cannot diagnose it. The CLI catches LiftError, prints error: ..., and exits 2 with no Problem stream — the tool that exists to report a broken export cannot reach the report. (exit 1 is the documented "errors found" code.)
Zip packages inherit it: load_zip extracts and calls Lexicon.load.
The only workaround is resolve_ranges=False, which drops every companion, so ranges-dependent checks (undefined-range-value, the ranges schema layer) go silent too.
Fix options
Never raise from companion discovery. Skip a candidate that does not parse as a ranges document, and report it. This extends Resolve companion ranges across filename case differences #19's existing treatment of the lexicon to every non-companion file, so the whole class behaves one way.
Needs somewhere to put the reason: validation re-derives existence independently via _existing_file, so without a record it would report dangling-ranges-href saying "no companion file was found" when one was found and rejected. Either the load path records rejections for validation to read, or a new code covers "found, but not a ranges document".
Distinguish an asserted reference from a guessed one. The conventional sibling and an href basename are discovery heuristics — nothing in the document claims those files exist, so failing over them is disproportionate. A relative href the header writes out is an assertion by the document, where a rejection is meaningful. The code already knows which candidate is which, so this line can be drawn if the wholesale skip looks too permissive.
Leave load strict and document it. Smallest change, but keeps a zero-byte sidecar fatal to reading a lexicon's entries, which is hard to defend.
The first looks right, with the second as the fallback if silently skipping a malformed genuine companion is judged to hide too much.
Notes
Pre-existing: before #19, _resolve_ranges went straight from candidate.is_file() to RangesFile.load(candidate), so this failed the same way — the reproductions above use exact spellings and never reach the folded lookup. Split out of review discussion on #19, which kept its scope to companion resolution.
Lexicon.load()raisesLiftParseErrorand hands back nothing when a companion candidate exists but is not a.lift-rangesdocument. One unusable file costs the whole lexicon, including its entries — where every other candidate that cannot serve as a companion is skipped, and the reference reported asdangling-ranges-href.The realistic trigger needs no unusual href at all: a zero-byte or truncated
Dict.lift-rangesbesideDict.lift— an interrupted export, a failed sync, a partial checkout — makes the lexicon unloadable.Reproduction
Sibling only, no href involved:
Any header
range/@hrefresolving to any existing non-ranges file does the same. Exact spellings throughout — no case folding involved:Mechanism
_resolve_ranges(src/sil_lift/_model.py) loads whatever exists:Nothing between the existence test and the load asks whether the file is a ranges document.
RangesFile.loaddecides, and its refusal propagates straight out ofLexicon.load.One file is already exempt: the lexicon itself. #19 added a
_same_filecheck for it precisely because a case-variant href folding onto the.lifttook the whole load down, and settled on skipping the candidate and lettingdangling-ranges-hrefreport the reference. That establishes the principle — a candidate that exists but cannot serve as a companion is skipped and reported, not fatal. It just was not extended past the lexicon: a second.liftin the folder, or any other file, still ends the load.Consequences
sil-lift validatecannot diagnose it. The CLI catchesLiftError, printserror: ..., and exits 2 with noProblemstream — the tool that exists to report a broken export cannot reach the report. (exit 1is the documented "errors found" code.)load_zipextracts and callsLexicon.load.resolve_ranges=False, which drops every companion, so ranges-dependent checks (undefined-range-value, the ranges schema layer) go silent too.Fix options
_existing_file, so without a record it would reportdangling-ranges-hrefsaying "no companion file was found" when one was found and rejected. Either the load path records rejections for validation to read, or a new code covers "found, but not a ranges document".loadstrict and document it. Smallest change, but keeps a zero-byte sidecar fatal to reading a lexicon's entries, which is hard to defend.The first looks right, with the second as the fallback if silently skipping a malformed genuine companion is judged to hide too much.
Notes
Pre-existing: before #19,
_resolve_rangeswent straight fromcandidate.is_file()toRangesFile.load(candidate), so this failed the same way — the reproductions above use exact spellings and never reach the folded lookup. Split out of review discussion on #19, which kept its scope to companion resolution.