From the review on PR #1: #1 (comment)
map-anchors --apply rewrites translated sources in place. Two defects make that unsafe.
It can point a link at the wrong heading, non-deterministically. The loop iterates set(LINK.findall(text)) — unordered — while mutating text with str.replace, which rewrites every occurrence. If rewriting link A produces a string equal to link B, the later pass rewrites both. With English ids ['a','b'] and translated ids ['b','c'], the correct result is #a→#b, #b→#c; under PYTHONHASHSEED=3 and 5 the fixture produced [eka](#c) ja [toka](#c), under 0/1/2/4 it was correct. check-anchors passes the corrupted file because #c is a real id, so nothing downstream detects it. The precondition — a translated heading id equal to a different English heading id on the same page — is ordinary where headings like Signal K stay untranslated while neighbours move.
It crashes mid-run and leaves a half-rewritten tree. target_page calls .relative_to(Path.cwd()), which raises ValueError on any relative link resolving above the root — one .. too many. Pages processed before the bad link are already written, the summary never prints, and re-running crashes at the same place.
Fix: build the whole old→new mapping first and apply it in a single pass (one re.sub over the LINK pattern with a lookup), so no replacement can be re-read as input — sorting the set only makes the corruption deterministic. Catch the ValueError and return None, folding those links into the existing unmapped report. Buffer rewrites and write only after every page is processed.
Test: run the suite under a varied PYTHONHASHSEED; add a case where a translated id collides with a different English id on the same page; add an out-of-tree relative link; add a second --apply run asserting zero rewrites and a byte-identical file.
From the review on PR #1: #1 (comment)
map-anchors --applyrewrites translated sources in place. Two defects make that unsafe.It can point a link at the wrong heading, non-deterministically. The loop iterates
set(LINK.findall(text))— unordered — while mutatingtextwithstr.replace, which rewrites every occurrence. If rewriting link A produces a string equal to link B, the later pass rewrites both. With English ids['a','b']and translated ids['b','c'], the correct result is#a→#b,#b→#c; underPYTHONHASHSEED=3and5the fixture produced[eka](#c) ja [toka](#c), under 0/1/2/4 it was correct.check-anchorspasses the corrupted file because#cis a real id, so nothing downstream detects it. The precondition — a translated heading id equal to a different English heading id on the same page — is ordinary where headings likeSignal Kstay untranslated while neighbours move.It crashes mid-run and leaves a half-rewritten tree.
target_pagecalls.relative_to(Path.cwd()), which raisesValueErroron any relative link resolving above the root — one..too many. Pages processed before the bad link are already written, the summary never prints, and re-running crashes at the same place.Fix: build the whole old→new mapping first and apply it in a single pass (one
re.subover the LINK pattern with a lookup), so no replacement can be re-read as input — sorting the set only makes the corruption deterministic. Catch theValueErrorand returnNone, folding those links into the existingunmappedreport. Buffer rewrites and write only after every page is processed.Test: run the suite under a varied
PYTHONHASHSEED; add a case where a translated id collides with a different English id on the same page; add an out-of-tree relative link; add a second--applyrun asserting zero rewrites and a byte-identical file.