Skip to content

Resolve companion ranges across filename case differences - #19

Draft
imnasnainaec wants to merge 3 commits into
mainfrom
ranges-companion-filename-case
Draft

Resolve companion ranges across filename case differences#19
imnasnainaec wants to merge 3 commits into
mainfrom
ranges-companion-filename-case

Conversation

@imnasnainaec

@imnasnainaec imnasnainaec commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

A LIFT folder written on Windows can spell its pair inconsistently — Dict.LIFT beside Dict.lift-ranges, or the reverse — and load fine there, because the filesystem folds case. On Linux the sibling candidate is built from the .lift's own suffix (_model.py:408), so it missed, the companion was skipped silently, and every range it defined went absent.

Not zip-specific: sil_lift.load("/data/Foo.LIFT") on Linux hits it with no archive involved, since parse_document never inspects the extension.

Approach

A candidate that matches no file exactly now falls back to one whose name differs only in case.

  • Reached only after an exact miss, so a case-folding filesystem never enters the fallback and behaves exactly as before. The cost lands only where the lookup was already failing.
  • One directory read per folder, cached across the candidate list, so a header with several range/@href entries doesn't re-scan.
  • Both directions, since an exporter sloppy about one is sloppy about the other.

The tie-break

Where several names fold together, the lexicographically first wins. Arbitrary, but fixed — and fixed is the property that matters: directory order varies between filesystems and between runs, and a companion that resolved differently on consecutive reads would be worse than one that never resolved. Documented in _existing_file and asserted by a test.

Open to a different rule if you'd rather have one — e.g. preferring an exact case match on the stem — but I'd want it to stay deterministic.

Tests

Four in test_ranges_folder.py: uppercase .lift, uppercase companion, the tie-break, and a negative case pinning that the fallback doesn't reach past a folder for a name that isn't in it. The first two pass on every platform (they resolve via the old path where the filesystem folds); the tie-break needs both spellings to coexist, so it skips off Linux — the ubuntu-latest matrix leg is where it bites.

python scripts/check.py green: 380 passed, 1 skipped (that tie-break test, on Windows), 94% coverage.

Draft: opening for a read on the tie-break rule before marking ready.

🤖 Generated with Claude Code


Devin review: https://app.devin.ai/review/sillsdev/python-sil-lift/pull/19


This change is Reviewable

@imnasnainaec imnasnainaec self-assigned this Aug 6, 2026
imnasnainaec and others added 3 commits August 12, 2026 15:12
A LIFT folder written on Windows can spell its pair inconsistently —
Dict.LIFT beside Dict.lift-ranges, or the reverse — and load fine there,
because the filesystem folds case. On Linux the sibling candidate is built
from the .lift's own suffix, so it missed, the companion was skipped
without a word, and every range it defined went absent.

Candidates that match no file exactly now fall back to one whose name
differs only in case. The fallback is reached only after an exact miss, so
a case-folding filesystem never enters it and behaves as before; a
case-sensitive one gets one directory read per folder, cached across the
candidate list.

Where several names fold together the lexicographically first wins. The
choice is arbitrary but fixed, which matters more than which file it picks:
directory order varies between filesystems and runs, and a companion that
loads differently on consecutive reads would be worse than one that never
loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0.1.0 has not shipped, so there is no released behavior for an Unreleased
entry to be fixing — the tolerance is simply part of what companion
discovery does in the first release. Fold it into that bullet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The directory listing the fallback builds called its files "entries", the
word this module uses for a LIFT <entry> everywhere else — the same
collision that keeps byte regions from being called spans. Name them
files.

Spell the surrounding prose the way the rest of the package does: a
fallback that runs rather than fires, a name that matched no file rather
than missed, a helper named for the filesystem it probes rather than
abbreviating it, and fixture names deliberately not taken from the
corpus file. Unpack the two densest clauses so each reads in one pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@imnasnainaec
imnasnainaec force-pushed the ranges-companion-filename-case branch from fb6bb44 to dededeb Compare August 12, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant