fix: integration wiring — warnings, renamed seals, v0.1.x repair, ambiguous codes - #6
Merged
Merged
Conversation
Fixer.fix has returned a FixOutcome with warnings since the backup work landed — a .bak that is no longer a copy of the file it sits beside, kept rather than overwritten because the oldest backup is the pristine one. ScanView threw the outcome away, so the one case where Uncoded knows the user's safety net is stale was the one case it kept quiet about. FrameFix.fixed now carries them. The frame keeps its FIXED seal and its revert — the write landed, and only the backup is in question — and gets a small amber tick beside the seal, the warning underneath, and the full text in the tooltip: the same shape as revertRefused, which is the other "this succeeded, but" state. The run summary counts them apart, so "12 fixed (2 with warnings)" never reads as a failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A scan sealed frames by comparing journal paths against file paths, which is exactly the comparison that fails for the most common thing that happens to a fixed file: Lightroom renames it on import. The frame came back looking untouched, its undo unreachable, and fixing it again would have appended a second copy of the metadata. JournalStore.sealedURLs already answers this properly — path first, then content for anything unmatched, and it declines to seal a copy or an ambiguous claim, because a seal promises an undo and only one file can have it. Wire it in place of fixedPaths(), and keep the filename it recorded: the frame's tooltip now reads "fixed as L1000123.DNG, renamed since", which is the difference between a mystery and a fact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Once a fix lands, the file stops claiming a Leica lens and starts claiming the user's. That matches nothing in the 6-bit table, so on the next scan the frame fell out of the mapped set entirely and sat there as unrecognised — which is why the files v0.1.x fixed with an empty crs:LensProfileDigest had no route back: they had to be recognised before they could be repaired. UserLens.claiming is now the scan plan's last resort, consulted only for frames the code table had nothing at all to say about, so it can neither shadow a marked override nor put a thumb on an ambiguity. Such a frame resolves to its lens — not as an override, and not as a coded frame — and is then treated exactly like a sealed one for re-fix purposes. Which means the re-fix test had to grow up: comparing the claimed name cannot see a file that names the right lens beside a missing digest, and that is precisely the file this is for. LensWrite.differs compares the fields a write actually sets (skipping empty ones, since xmpProperties skips those too — otherwise a hand-typed lens would ask to be rewritten forever). Tested both ways round, including on a real M11 packet: a frame Uncoded just fixed must never come back asking to be fixed again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Some camera lens names fit more than one row of the 6-bit table — the generations share a name, and the M11 writes no numeric code tag to separate them. matchCandidates has always returned both, and matchedCode has always returned nil for that case, which meant the frame rendered as if it wore no code at all: absent from the "coded" count, absent from the unclaimed-code banner, no way to map either code from its context menu. A frame that is coded, and whose ambiguity is the one thing Uncoded knows about it, said nothing. It now says the one honest thing there is to say — here are the two codes, pick the one on your lens — through the patterns that already exist. The banner keys on the candidate list rather than a single code, prints both pit patterns with an "or" between, and offers each candidate through a menu instead of choosing. The context menu lists both "Map Code … to a Lens…" items. The tooltip names both codes and their Leica lenses and says why the app won't guess. And the frame counts as coded, because it is: which code is the open question, not whether. No new visual language, and still no guessing — mapping either code is the user's call, and one of them is engraved on the lens in their hand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ren't The writer serializes the packet through XMLDocument, so a lens called "Cooke & Sons" goes to disk as "Cooke & Sons" — and it deliberately emits 
/
/	 for whitespace, because attribute-value normalization eats those characters written literally. xmpValue handed all of that straight back. Every consumer therefore saw a value that was not the value written. The tooltip printed the escapes, which is cosmetic; LensWrite.differs compared them against the unescaped name, which is not. Any lens or profile name containing & < > " ' read as different from itself the moment it was written: the frame carried a FIXED seal and a RE-FIX row at once, every Fix run rewrote it byte-identically, and every run left another journal record behind. Nothing corrupted, but nothing that ever settled either. xmpValue now unescapes — the five named entities and numeric character references in both bases — in one left-to-right pass, so the escaped text &lt; stays < instead of decoding twice into markup that was never in the file. Anything that is not a reference we recognise survives verbatim: an XMP packet is a file on disk like any other, and inventing a character for ½ would be worse than printing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
differs compared the name and the profile fields, and a lens with no Adobe profile has no profile fields — so it returned early and such a frame could never look like it needed anything. But v0.1.1 wrote only the EXIF LensSpecification and left the camera's aux:LensInfo alone, describing the borrowed Leica lens at its maximum aperture rather than the real lens's. That copy is the one Lightroom shows, and for a lens with no profile it is the whole of what a repair has to put right. So the doc comment's promise — "only the fields this write actually sets are compared" — is now true of the numbers as well: the focal length and aperture in EXIF, and their XMP twin. aux:LensInfo compares exactly, being a string either way. LensSpecification compares through the reader's own rendering, shared rather than copied so the two cannot drift, and asks what the writer would *store*: it puts every rational over a denominator of 1000, so comparing a fourth decimal place against the rounded one on disk would leave that lens asking to be rewritten for ever. Not done, deliberately: the orphan crs:LensProfileSetup="Custom" v0.1.1 left beside no profile at all. It deserves to count as a difference, but the writer only ever sets properties — it cannot clear one — so a fix would leave it exactly where it found it and the frame would ask to be rewritten on every scan, which is the failure this whole round is about. Repairing those needs the writer to learn removal first; the reasoning is written down where the check would have gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mapped A frame whose lens name fits two codes resolves to nothing when those two codes point at two different lenses — the app has no business picking one. It landed in the unclaimed-code banner all the same, which told the user the codes were "not claimed by any of your lenses yet" and offered to map one. Both false, and the offer was a dead end: mapping a code that is already mapped changes nothing about this frame. The banner now knows the difference. Reaching the unclaimed set with any candidate mapped can only mean two of them are mapped to different lenses — a single mapped candidate would have resolved the frame — so the group is marked contested and gets its own sentence and its own way out: mark these frames, which raises the Assign Lens menu that already exists for a marked set. Two clicks to the only answer there is, instead of a button that does nothing. The frame's own line said "none mapped", which was the same lie in miniature; it now says what is true either way — that Uncoded won't guess — and the tooltip mentions assigning as well as mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l fix The unit-level round trips prove differs against synthetic packets and one write path. This proves the thing that actually matters end to end: fix a real M11 frame — real 2 KB camera XMP, real EXIF IFD — through Fixer, which fills in a missing profile digest of its own accord and so can write something the lens row never said, and then ask what the next scan would make of it. Nothing, twice over, for a profiled lens and for a hand-typed one with no crs:LensProfile* at all. Skips cleanly without UNCODED_TEST_DNG, like its neighbours. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Final integration round wiring the deferred cross-PR pieces now that #1–#5 are merged:
FrameFix.fixedcarries warnings (stale/foreign.bakcases); amber badge beside the FIXED seal, detail text, and "(m with warnings)" in the run summary.JournalStore.sealedURLs(in:); renamed frames show "fixed as " and revert stays reachable.UserLens.claiming, and the re-fix predicate is now field-based (LensWrite.differs) — a v0.1.x fix with an empty digest or the borrowed lens'saux:LensInforeads as a difference and re-enters the fix set.&/<re-flagged forever after a successful fix. Verified settled on a real frame through the full Fixer path, for profiled and hand-typed lenses.Known follow-up (backlog): removing the orphan
crs:LensProfileSetup="Custom"that v0.1.x left on no-profile fixes needs a property-removal path in the writer; flagging it without one would loop. Reasoning recorded indiffers' doc comment.158 tests, 0 failures (0 skipped with the real-DNG fixture + exiftool available).
Implemented and reviewed with Opus 5.
🤖 Generated with Claude Code