Skip to content

Commit cf1a61f

Browse files
committed
Keep language builds from stopping on a stale snippet path
A stored translation can still name a docs_src file that an English change has since renamed; language configs now build with snippet path checks off, so that block renders empty under the outdated notice instead of failing the docs build. On update jobs the carried sections are put back before any check runs, so drift in text the run discards can no longer cost repair turns.
1 parent 8ed9da1 commit cf1a61f

4 files changed

Lines changed: 50 additions & 8 deletions

File tree

scripts/docs/build_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ def build_config(lang: str | None = None, root: Path = ROOT) -> Path:
210210
# No API reference on a language site, so no mkdocstrings pass either.
211211
plugins: list[str | dict[str, Any]] = config["plugins"]
212212
config["plugins"] = [p for p in plugins if (next(iter(p)) if isinstance(p, dict) else p) != "mkdocstrings"]
213+
# A stored translation can name a `docs_src` file an English change has since renamed:
214+
# that block renders empty under the outdated notice instead of stopping the build.
215+
extensions: list[str | dict[str, Any]] = config["markdown_extensions"]
216+
for extension in extensions:
217+
if isinstance(extension, dict) and extension.get("pymdownx.snippets"):
218+
extension["pymdownx.snippets"]["check_paths"] = False
213219
config["theme"]["language"] = language.theme
214220
# Zensical resolves docs_dir/site_dir against the config file and
215221
# rejects absolute paths.

scripts/docs/translations.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,9 @@ def _validate_open(english: str, body: str, job: Job, glossary: Glossary) -> lis
891891
def translate_page(repo: Repo, inputs: Inputs, job: Job, translator: Translator, model: str, usage: Usage) -> str:
892892
"""Translate one page and return its body; token usage accumulates into `usage`.
893893
894+
Each reply has its carried sections overwritten before any check runs, so
895+
only text this run keeps can cost a repair turn or fail the page.
896+
894897
Raises:
895898
PageError: The page could not be produced (API failure, refusal, or unrepairable structure).
896899
ConfigError: The credentials were rejected.
@@ -908,9 +911,12 @@ def translate_page(repo: Repo, inputs: Inputs, job: Job, translator: Translator,
908911
raise PageError(f"the reply was cut off at {OUTPUT_TOKEN_BUDGET} output tokens")
909912
if completion.stop_reason == "refusal":
910913
raise PageError("the model declined to translate this page")
911-
result = reimpose(english, ids, unwrap(english, completion.text))
912-
if isinstance(result, str): # aligned, so it can be assembled: carry sections forward, pin today's ids on them
913-
result = reimpose(english, ids, carry_forward(job, result))
914+
reply = unwrap(english, completion.text)
915+
# Only a reply whose sections line up with the English can be assembled; one that
916+
# does not is checked as it stands and fails the heading check.
917+
if len(sections(reply)) == len(job.state.hashes):
918+
reply = carry_forward(job, reply)
919+
result = reimpose(english, ids, reply) # today's ids pinned on carried sections too
914920
if isinstance(result, Mismatch):
915921
findings = result.findings
916922
elif not (findings := _validate_open(english, result, job, inputs.glossary)): # checked as it would be written

tests/docs/test_build_config.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"nav": NAV,
2424
"theme": {"name": "material"},
2525
"plugins": ["search", {"mkdocstrings": {"handlers": {"python": {"paths": ["src"]}}}}],
26+
"markdown_extensions": ["admonition", {"pymdownx.snippets": {"base_path": ["."], "check_paths": True}}],
2627
}
2728

2829
LANGUAGES = """\
@@ -86,15 +87,17 @@ def test_alternate_lists_english_then_each_language_as_code_labelled_links_relat
8687

8788
def test_lang_config_builds_the_staged_tree_into_site_code_without_an_api_reference(tmp_path: Path) -> None:
8889
"""Tool-defined: `--lang ja` writes mkdocs.ja.gen.yml pointing Zensical at the staged tree (repo-relative),
89-
building into site/ja/ under the ja URL prefix and theme language, with mkdocstrings dropped, the API
90+
building into site/ja/ under the ja URL prefix and theme language, with mkdocstrings dropped, snippet path
91+
checks off (a stale include in a stored translation renders empty rather than failing the build), the API
9092
entry turned into a link up to the English reference, section titles from the `titles.json` staged
9193
beside the tree, and the switcher with links relative to this site."""
9294
write_repo(tmp_path)
9395

9496
written = build_config.build_config("ja", tmp_path)
9597

9698
config = cast(dict[str, Any], yaml.safe_load(written.read_text(encoding="utf-8")))
97-
picked = {key: config[key] for key in ("docs_dir", "site_dir", "site_url", "theme", "plugins", "nav", "extra")}
99+
keys = ("docs_dir", "site_dir", "site_url", "theme", "plugins", "markdown_extensions", "nav", "extra")
100+
picked = {key: config[key] for key in keys}
98101
assert (written.name, picked) == snapshot(
99102
(
100103
"mkdocs.ja.gen.yml",
@@ -104,6 +107,10 @@ def test_lang_config_builds_the_staged_tree_into_site_code_without_an_api_refere
104107
"site_url": "https://docs.example/ja/",
105108
"theme": {"name": "material", "language": "ja"},
106109
"plugins": ["search"],
110+
"markdown_extensions": [
111+
"admonition",
112+
{"pymdownx.snippets": {"base_path": ["."], "check_paths": False}},
113+
],
107114
"nav": [
108115
"index.md",
109116
{"サーバー": ["servers/index.md", "servers/tools.md"]},

tests/docs/test_translations.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,29 @@ def test_banned_rendering_is_a_finding_in_a_retranslated_section_but_not_in_a_ca
779779
assert body.endswith("## エラー {#errors}\n\nファンクションから送出します。\n")
780780

781781

782+
def test_link_dropped_in_a_carried_section_costs_no_repair_turn_and_the_stored_section_is_kept(
783+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
784+
) -> None:
785+
"""Tool-defined: a reply is assembled with the carried sections before its structure is checked, so a link
786+
the model lost in a section this run discards anyway is no finding: one call, and the published intro is
787+
the stored one, link and all."""
788+
root = make_repo(tmp_path)
789+
translate_all(capsys, root)
790+
write(root / "docs" / "tools.md", TOOLS.replace("Raise to signal a failure.", "Raise `ToolError` to fail."))
791+
reply = TOOLS_JA.replace("[ホーム](index.md#install)から", "ホームから").replace(
792+
"失敗を伝えるには例外を送出します。", "失敗するには `ToolError` を送出します。"
793+
)
794+
fake = FakeTranslator([reply])
795+
796+
code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake)
797+
798+
assert (code, err, out.split("\n")[0]) == (0, "", "translated: tools.md (1 of 3 sections)")
799+
assert [len(conversation) for conversation in fake.conversations] == [1] # one call, no repair turn
800+
body = t.split_front_matter((root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8"))[1]
801+
assert t.sections(body)[0] == t.sections(TOOLS_JA)[0].replace("# ツール\n", "# ツール {#tools}\n")
802+
assert body.endswith("## エラー {#errors}\n\n失敗するには `ToolError` を送出します。\n")
803+
804+
782805
def test_removed_english_section_is_reassembled_with_no_client_but_an_edited_one_needs_credentials_first(
783806
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
784807
) -> None:
@@ -921,8 +944,9 @@ def test_code_block_moved_out_of_a_retranslated_section_gets_repair_turns_like_a
921944
tmp_path: Path, capsys: pytest.CaptureFixture[str]
922945
) -> None:
923946
"""Tool-defined: on an update the same slip (the retranslated section's reply swallows the code block of
924-
the carried section before it) is fed back for repair rather than failing the page outright, and the
925-
fixed reply is assembled with the carried sections."""
947+
the carried section before it) is fed back for repair rather than failing the page outright; the carried
948+
section is the stored one by then, so only the retranslated section's extra fence is named, and the fixed
949+
reply is assembled with the carried sections."""
926950
root = make_repo(tmp_path)
927951
translate_all(capsys, root)
928952
write(root / "docs" / "tools.md", TOOLS.replace("Raise to signal a failure.", "Raise `ToolError` to fail."))
@@ -937,7 +961,6 @@ def test_code_block_moved_out_of_a_retranslated_section_gets_repair_turns_like_a
937961
Your translation broke the following structural rules. Fix each problem and return the
938962
full corrected page, changing nothing else:
939963
940-
- ## Your first tool: 0 code fences vs 1 in the English: keep each where it is, add none
941964
- ## Errors: 1 code fences vs 0 in the English: keep each where it is, add none\
942965
""")
943966
body = t.split_front_matter((root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8"))[1]

0 commit comments

Comments
 (0)