Skip to content

Sync path never applies applyTypography() — every sync strips the French non-breaking spaces the seed inserted #97

Description

@mmcky

applyTypography() runs in the translate init seed path but not in the action's sync path, so every sync silently strips the non-breaking spaces the seed inserted. The French edition degrades a little with each merged sync PR.

Evidence

QuantEcon/lecture-python-programming.fr#6 (merged) synced numba.md from source PR #550. Thirteen lines changed only their whitespace — U+00A0 became a plain space:

-Voici une solution\xa0: +Voici une solution :
-Chronométrons-le maintenant\xa0: +Chronométrons-le maintenant :
-*Symptôme 1\xa0: le résultat n'est plus reproductible.* +*Symptôme 1 : le résultat n'est plus reproductible.*

Counting U+00A0 in lectures/numba.md across its only two commits:

Commit Path U+00A0
697b904 — seed (translate init, Opus 4.8) init 27
82d62aa — sync PR #6 (action mode: sync, Sonnet 5) sync 14

Thirteen lost in one sync of one lecture. The file now mixes both conventions, and the loss is permanent once merged — a later sync only rewrites lines the source touched.

Root cause

applyTypography() has exactly one caller:

src/index.ts, which is what the action runs for mode: sync, never imports it. The sync path therefore relies solely on the prompt instruction at src/language-config.ts#L53:

Insert a non-breaking space before the high punctuation marks ; : ! ? as required by French typography

That instruction is exactly the one #79 was opened to replace, because the model does not follow it. From the seed PR's own write-up: "The translation prompt has always instructed the model to place a non-breaking space before ; : ! ?. It doesn't — a measured translation came back with zero of them. That's now applied deterministically in code instead (#79); 331 non-breaking spaces were inserted here."

So #79 fixed the seed path and left the sync path on the mechanism #79 itself proved does not work. Every subsequent sync regresses whatever lines it touches back to pre-#79 behaviour.

Why it matters

The seed inserted 331 non-breaking spaces across 26 lectures. Sync is the ongoing path — it runs on every upstream merge, forever — so this is a slow erosion of the seed's work, one merged PR at a time, on exactly the lines an editor is least likely to re-read. It is invisible in review: the diff renders as unchanged text, and both automated review comments on #6 scored formatting 9/10 and 10/10 while missing it entirely.

Correctness, not just style: a plain space before : lets the colon wrap to the start of the next line.

Shape of the fix

Call applyTypography() on translated content in the sync path, at the same point init does — before anything derives from the text. hasTypographyRules() already exists to gate it, and the transform is documented as idempotent and spacing-only, so applying it to sync output should be safe.

Two things worth deciding alongside:

  1. Should the prompt rule stay? With deterministic post-processing on both paths, additionalRules asking for nbsp is dead weight at best, and at worst invites the model to produce inconsistent spacing the transform then has to normalise. The guillemets rule on the line above may deserve the same question.
  2. Backfill. Existing editions already carry drift from syncs merged before the fix. scripts/typography/apply.mjs --lang fr is the existing repair tool (it produced .fr#4); a one-off run per edition after this lands would reset the baseline. numba.md needs it today.

Note on scope

I checked whether the seed itself was inconsistent and it was not — a naive "space before :" scan flags many false positives (MyST directives like :class:, code cells, URLs), which is what the transform's masking logic exists to avoid. The seed path is working as designed. This issue is specifically that the sync path never got the same treatment.

Only French currently declares typography rules, so fr is the only affected edition today. Any future language with hasTypographyRules() inherits the same gap.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions