docs verify: nur als .template ausgeliefertes Linkziel gilt als aufgeloest (Defekt aus 0fb8fd6)
CI / verify (push) Successful in 50s
Release / release (push) Successful in 36s

Files changed:
- CHANGES.md
- VERSION
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/tests/test_docs_verify.py
This commit is contained in:
2026-09-13 00:40:10 +02:00
parent f140e26a4c
commit c0dc2129bb
4 changed files with 83 additions and 7 deletions
+27 -5
View File
@@ -470,6 +470,13 @@ def check_toc_regions() -> list[str]:
# escaping, which nothing here uses.
MARKDOWN_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
# The suffix `dist export` re-keys an instance-owned file to, and the one
# `setup-instance.md` renames away again. Spelled here rather than imported
# from `ownership`, whose own `.template` handling answers a different
# question (which side an upstream merge keeps) over a narrower scope
# (paths under a content stage).
TEMPLATE_SUFFIX = ".template"
def is_external_or_anchor(target: str) -> bool:
"""A link this check does not resolve as a filesystem path: an absolute
@@ -498,6 +505,17 @@ def check_reference_targets() -> list[str]:
a stale one anyway. Code fences and inline code spans are masked first
(`markdown_code.strip_code_spans`), so a passage that shows link syntax
as an example is not mistaken for a real reference.
**A target the stack ships only as a `.template` counts as resolving.**
`kb/CONVENTIONS.md` and every `kb/<name>/COLLECTION.md` are instance-owned:
a distribution carries `<name>.template` and the instance adopts it by
renaming, during `instructions/setup-instance.md`'s personalization step.
Between `dist export` and that step the real file legitimately does not
exist yet - while `kb/CONTRACT.md` and three flat instructions link to it
by its adopted name, correctly, because that is the name it will have.
Reporting those as dead links would fail a fresh export for doing exactly
what it is supposed to do, and would describe "not personalized yet" as a
broken link when `doctor`'s `conventions` check already says it precisely.
"""
issues = []
for path in toc.target_files():
@@ -511,11 +529,15 @@ def check_reference_targets() -> list[str]:
target_path = target.split("#", 1)[0]
if not target_path:
continue
if not (path.parent / target_path).resolve().exists():
issues.append(
f"{rel_path(path)}:{line_number} links to `{target}`, which does not "
"resolve to an existing file"
)
resolved = (path.parent / target_path).resolve()
if resolved.exists():
continue
if resolved.with_name(resolved.name + TEMPLATE_SUFFIX).exists():
continue
issues.append(
f"{rel_path(path)}:{line_number} links to `{target}`, which does not "
"resolve to an existing file"
)
return issues
+28
View File
@@ -586,6 +586,34 @@ def test_link_syntax_shown_as_an_example_in_a_fence_is_not_flagged(tmp_path, mon
assert docs_verify.check_reference_targets() == []
def test_a_target_shipped_only_as_a_template_is_not_dead(tmp_path, monkeypatch):
"""Regression guard for a defect this check shipped with. A fresh
`dist export` carries `kb/CONVENTIONS.md.template`, not
`kb/CONVENTIONS.md` - the instance adopts it by renaming, during
`setup-instance.md`'s personalization step. `kb/CONTRACT.md` and three
flat instructions link to the adopted name, correctly. Before this
exemption the check reported 13 dead links on a just-exported tree, for
doing exactly what a fresh export is supposed to do."""
(tmp_path / "kb").mkdir()
(tmp_path / "kb" / "CONVENTIONS.md.template").write_text("# Conventions\n", encoding="utf-8")
fake = tmp_path / "kb" / "CONTRACT.md"
fake.write_text("What this instance decided: [kb/CONVENTIONS.md](CONVENTIONS.md).\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
assert docs_verify.check_reference_targets() == []
def test_a_target_with_neither_the_file_nor_a_template_is_still_dead(tmp_path, monkeypatch):
"""The exemption is narrow: it covers a file the stack ships as a
template, not any missing target."""
(tmp_path / "kb").mkdir()
fake = tmp_path / "kb" / "CONTRACT.md"
fake.write_text("See [kb/CONVENTIONS.md](CONVENTIONS.md).\n", encoding="utf-8")
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
issues = docs_verify.check_reference_targets()
assert len(issues) == 1
assert "CONVENTIONS.md" in issues[0]
def test_every_reference_files_link_targets_resolve():
"""Forward direction, against the real tree: every relative link in a file
`toc.target_files()` covers must resolve - this is what a `../` count