v0.17.0 — deterministic typography on every write path; typography-insensitive heading matching
LatestBug-fix release closing the French typography erosion (#97) and hardening heading matching against the divergence class it exposed. Every write path now typesets deterministically, and matching no longer depends on byte equality.
Fixed — sync typography (#99, closes #97): applyTypography() ran only in the translate init seed path, so every sync stripped the non-breaking spaces the seed inserted — one merged sync took numba.md from 27 to 14 × U+00A0, invisibly (the diff renders as unchanged text). Both sync entry points now typeset, with the heading map derived from the same typeset strings that land in the body (typesetting only the body would desync map from body, silently dropping sections on later syncs). Sync PRs on French repos may now carry whitespace-only changes on lines the source PR did not touch — that is drift self-repairing. Existing editions want a one-off scripts/typography/apply.mjs --lang fr; lecture-python-programming.fr got it in .fr#9.
Fixed — typography-insensitive heading matching (#100): matching a heading-map value to its body heading was an exact string compare, and typography legitimately makes the sides diverge (role spans are masked by the transform but stripped in map values; apply.mjs typesets map values independently; human edits touch one side). Comparisons now canonicalize both sides — strip #/MyST roles, fold NBSP/narrow-NBSP, collapse runs, drop spaces before ; : ! ? — with exact-match-first and normalized matches accepted only when unambiguous, so raw-distinct headings keep their positional pairing. Verified over all 401 headings of the French edition. Also fixed by the same audit: the body side of the compare never stripped MyST roles (role-wrapped headings could never match by map); role-stripped key lookups in the CLI validator and subsection merge; the subsection-merge fallback writing a translated heading into the map but not the body; translate forward resync writing raw untypeset model output; translate headingmap ping-ponging with apply.mjs forever on typeset values.
Fixed — concurrent review runs (#98): postReviewComment was check-then-act, so racing runs each posted a "Translation Quality Review" comment (observed on .fr#6). Comments now carry a hidden marker and each run deletes older marked comments after writing its own — exactly one survives any interleaving; pre-marker duplicates are cleaned on the next review.
Changed: review comments are identified by an anchored marker, not prose matching (a human comment quoting a review can no longer be overwritten); the review workflow templates gained a per-PR concurrency group, permissions: pull-requests: write, and (connect-existing) a label filter — copy these into existing target repos or a single sync bills several reviews of the same diff.
Docs/record: restored the [0.16.1] CHANGELOG release header PR #98 accidentally deleted.
Upgrade: bump workflow pins to @v0.17.0 (or use @v0, which now tracks this release). After upgrading a French target repo, run the apply.mjs backfill once if syncs merged while on ≤ v0.16.1.