Repository navigation
Skip a companion candidate that is not a ranges document - #46
Merged
Merged
Conversation
Companion discovery loaded whatever existed at a candidate path and let RangesFile.load's refusal propagate, so one unusable file cost the whole lexicon its entries. A zero-byte or truncated .lift-ranges beside the .lift - an interrupted export, a failed sync, a partial checkout - was enough, with no unusual href involved. _resolve_ranges now catches LiftError and OSError around the companion load and records the rejection instead, extending to every non-companion the treatment the .lift itself already got: skipped, not fatal. The record is a private dict keyed by resolved path, left None until discovery runs so "nothing was rejected" stays distinguishable from "nothing was looked at". Validation reports each rejection as a new unreadable-ranges-file warning, addressed to the offending file and naming the route that reached it. _ranges_candidates now yields that route alongside each path, deduping on the path so the surviving source is the one load used; a broken sidecar is then fixed in the file, while an href pointing at an unrelated file is fixed in the header, and an intact file a stray href happens to name is never reported as though it were the defect. RangesFile.load is unchanged and still raises. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three companion-folder checks derive from the .lift's path and a directory listing, never from what the load accepted, so they reported on companions even for a lexicon loaded with resolve_ranges=False. That is the caller saying companions are out of scope for this load, which makes a finding about one unwanted whether or not it is accurate. Each of the two older checks also holds a suppression guard that reads lexicon.ranges_files - a collision where one of the colliding files loaded under an exact name, an href whose range a loaded sibling supplies anyway - and neither guard can fire on an empty dict, so both reported cases a resolving load would have withheld. _rejected_ranges already distinguishes "discovery ran" from "discovery never ran", so it gates the whole block; no second flag is needed. missing-media stays ungated: media is never resolved into the model, so there is nothing for a caller to have opted out of. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Path.resolve() canonicalizes case on Windows but not on macOS, so a companion found under a candidate's spelling is recorded under that spelling rather than the name it has on disk. Two tests compared the reported path to the on-disk spelling with ==, which held only where resolve() folds case. samefile() settles it the way the rest of the companion code already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tand The rejection record was replayed verbatim by validation, while every other companion-folder check re-derives from the current header and a live directory listing. The two disagreed as soon as either input moved: deleting a rejected file drew both dangling-ranges-href and unreadable-ranges-file, and repointing a header href left the warning quoting an href the document no longer carried. Validation now walks the current candidates and looks each resolved path up in the record, so a file that is gone, or that nothing names any more, drops out, and the route named is the one the header describes. Only the parser's reason stays from the load - as the content of a tracked companion does, which the schema layer reads from memory rather than from disk. The record has nothing left to carry but that reason, so _Rejection goes and the dict holds plain strings. Rejections also join the identity test that already guarded ranges_files. Nothing skipped a candidate resolving to an already-rejected file, so a second spelling of one file - a .. segment, a symlink, a case variant - parsed it again and overwrote its entry with the later route; on macOS, where resolve() leaves those spellings distinct, it also keyed and reported the same file twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_resolve_ranges keys a rejection under the spelling resolve() returned for the candidate that reached it, and guards that dict with an exact test and a _same_file test, since resolve() canonicalizes case on Windows but not on macOS. Validation looked the same dict up by == only, so on macOS a candidate reaching a rejected file under a second spelling found nothing and the warning went silent - a case-only edit to a header href being one way in. The lookup now falls back to _same_file, and dedups on the key that matched rather than on the candidate's spelling, so two spellings of one rejected file still report once. The test needs a filesystem that folds case while resolve() keeps the spelling given, which is macOS and not Windows or Linux. It probes for that behavior rather than for a platform name: a macOS volume formatted case-sensitive does not have it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The problem-code table and the layer list in the validation guide gave different counts - eleven against ten - where the table was right. A twelfth code lands in both, so they now agree, matching the table's fourteen rows less the two the schema layers produce. The changelog counted eleven with the table and follows. 0.1.0 has not shipped, so nothing else here is a change to record: a companion candidate that cannot be read is part of what discovery does in the first release, described where it is looked up rather than as a delta against behavior no reader has seen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unreadable-companion paragraph in the folder guide ran to three sentences where the paragraph above it states the same kind of rule in one, and wedged a three-item aside between its subject and verb. It keeps the example readers actually hit, drops the two that were there for completeness, and loses the "still raises" framing, which implies a release that has not happened. The companion-folder gate in the validation guide named a lexicon as what put companions out of scope, where the caller is what did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
imnasnainaec
marked this pull request as ready for review
September 17, 2026 18:46
add_ranges_file attaches a companion and writes its header range/@href without reading the folder, so _rejected_ranges stays None and the three companion-folder codes stay silent after a resolve_ranges=False load. That is what those checks need: they re-derive from the folder as it stands, and on what such a load holds, a header range supplied by a sibling companion that went unloaded reads as a dangling href. The guide and the docstrings stated the rule as companion discovery having run, which reads as though putting a companion back in scope brings them back. The validation guide, Lexicon.iter_problems and add_ranges_file now say it does not, the gate carries the misfire it avoids, and a test pins the silence. The four normalization fixtures and the probe name beside them are spelled with \N{...} escapes. They differ only in which accent is decomposed, so an editor or tool that normalizes the source would collapse the distinction they exist to hold - silently, since a probe that no longer decomposes reports the filesystem as insensitive and skips its tests rather than failing them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jasonleenaylor
approved these changes
Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #38.
What changed
Lexicon.load()no longer raises from companion discovery.<lift-ranges>is skipped, not fatal — extending to every non-companion the treatment Resolve companion ranges across filename case differences #19 gave the.liftitself.LiftErrorandOSError: a file that cannot be read at all failed the same way.RangesFile.load()is unchanged and still raises.unreadable-ranges-filewarning, addressed to the offending file and naming how the document reaches it.the conventional companion beside 'Dict.lift' could not be read as a ranges document…this file, named by header range 'etymology' href 'pictures.png', could not be read…ambiguous-ranges-file,dangling-ranges-hrefandunreadable-ranges-fileare silent underresolve_ranges=False.missing-mediais unaffected — media is never resolved into the model, so nothing was opted out of.Before / after
Zero-byte
Dict.lift-rangesbeside an intactDict.lift:Decisions worth a second opinion
warning, matching both neighbours.ambiguous-ranges-filehas the identical consequence: the file goes untracked, so it is absent from a relocatingsave(path)and carried through verbatim bysave_zip. An in-placesave()never touches it.lexicon.ranges_files. That isambiguous-ranges-fileanddangling-ranges-href, whose suppression guards cannot fire on an empty dict and so report cases a resolving load would have withheld;unreadable-ranges-fileandmissing-mediaread nothing from the model and would stay ungated.resolve_ranges=Falsethe alternative still reportsunreadable-ranges-file. It would also land as Fixed rather than Changed.Notes
Lexicon._rejected_rangesis the "record why a candidate was rejected" plumbing Second companion defining the same range id is dropped, causing false undefined-range-value #29 also wants for addressing findings to the right file.resolve()does not canonicalize case there. Pre-existing —ranges_fileskeys have always behaved this way — and undocumented.…fails_the_load→…is_skipped), nine added. One of the nine is gated on a filesystem that folds case whileresolve()keeps the spelling given, so it runs on the macOS leg only.scripts/check.pygreen — 664 passed, 4 skipped, 97.90% coverage.mkdocs build --strictgreen.🤖 Generated with Claude Code
This change is