fix: a wikilink wrapped across a line break is its own lint finding; rename and rm see it (#115)
CI / verify (push) Successful in 5m15s
CI / pwsh (push) Successful in 2m1s
Release / release (push) Successful in 35s

Files changed:
- CHANGES.md
- VERSION
- instructions/wiki-lint/SKILL.md
- kb/CONTRACT.md
- tools/CONTRACT.md
- tools/chemenu/commands/lint.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/kb_scan.py
- tools/chemenu/lint_core.py
- tools/chemenu/tests/test_kb_scan.py
- tools/chemenu/tests/test_lint.py
- tools/chemenu/tests/test_page_ops.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Opus 5.5 committed 2026-10-03 11:19:10 +02:00
1 parent b3022b8ffb
commit c261b8f4ca
12 files changed
+204 -13

No files matched your search

+29 -1
View File
@@ -1,5 +1,12 @@
from chemenu.frontmatter_io import write_page
from chemenu.kb_scan import find_nested_pages, iter_kb_pages, load_kb_pages
from chemenu.kb_scan import (
count_wikilinks,
extract_wikilinks,
find_nested_pages,
iter_kb_pages,
load_kb_pages,
wrapped_wikilinks,
)
def _names(kb_dir):
@@ -62,3 +69,24 @@ def test_find_nested_pages_flags_a_page_below_its_area(kb_dir):
def test_find_nested_pages_is_silent_for_pages_directly_in_an_area(kb_dir):
assert find_nested_pages(kb_dir, load_kb_pages(kb_dir)) == []
def test_a_wrapped_wikilink_reads_as_the_title_it_folds_to():
"""A link wrapped at a fixed column captured the break as part of the
title, so it matched no page and `lint` reported the page as missing."""
body = "See [[Foo Bar\n Target]] and [[Foo Bar\r\nTarget|alias]] and [[Other]]."
assert extract_wikilinks(body) == {"Foo Bar Target", "Other"}
assert count_wikilinks(body) == {"Foo Bar Target": 2, "Other": 1}
def test_wrapped_wikilinks_names_only_the_wrapped_ones():
body = "One [[Foo Bar\n Target]], one [[Foo Bar Target]], one [[A\n\nB]]."
assert wrapped_wikilinks(body) == ["Foo Bar Target", "A B"]
def test_a_wrapped_wikilink_inside_a_fence_is_notation():
# Only a fence can hold one: an inline code span is masked within one line
# (see markdown_code), so a span wrapped mid-link is read as prose.
body = "Example:\n\n```\n[[Foo Bar\n Target]]\n```\n"
assert wrapped_wikilinks(body) == []
assert extract_wikilinks(body) == set()
+40
View File
@@ -63,6 +63,46 @@ def test_lint_detects_broken_wikilink(kb_dir):
assert {"page": "gdeploy", "target": "Nonexistent Page"} in report["broken_links"]
def _gdeploy_saying(kb_dir, prose: str) -> None:
write_page(
kb_dir / "entities/tools/gdeploy.md",
{"type": "types/entity.md", "entity_type": "tool", "tags": [], "created": "2026-07-25",
"modified": "2026-07-25", "related": [], "sources": []},
f"\n# gdeploy\n\n## Description\n\n{prose}\n",
)
def test_a_wrapped_link_to_an_existing_page_is_wrapped_not_broken(kb_dir):
"""The reported case: the target exists under its one-line title, and the
finding used to read as a missing page with the line break in its name."""
_gdeploy_saying(kb_dir, "Notes in [[Source -\n Aurora]] for details.")
report = run_lint(kb_dir)
assert report["broken_links"] == []
assert report["wrapped_wikilinks"] == [{"page": "gdeploy", "target": "Source - Aurora"}]
assert has_hard_errors(report)
assert "wraps a wikilink across lines" in render_markdown(report)
def test_a_wrapped_link_to_a_missing_page_is_both_findings(kb_dir):
_gdeploy_saying(kb_dir, "See [[Nonexistent\n Page]] for details.")
report = run_lint(kb_dir)
assert {"page": "gdeploy", "target": "Nonexistent Page"} in report["broken_links"]
assert report["wrapped_wikilinks"] == [{"page": "gdeploy", "target": "Nonexistent Page"}]
def test_the_target_of_a_wrapped_link_is_not_an_orphan(kb_dir):
assert "Source - Aurora" in run_lint(kb_dir)["orphan_pages"] # the precondition
_gdeploy_saying(kb_dir, "Notes in [[Source -\n Aurora]] for details.")
assert "Source - Aurora" not in run_lint(kb_dir)["orphan_pages"]
def test_a_wrapped_link_inside_code_is_no_finding(kb_dir):
_gdeploy_saying(kb_dir, "Example:\n\n```\n[[Nonexistent\n Page]]\n```")
report = run_lint(kb_dir)
assert report["wrapped_wikilinks"] == []
assert report["broken_links"] == []
def test_lint_fixture_has_no_dangling_frontmatter_refs(kb_dir):
assert run_lint(kb_dir)["dangling_frontmatter_refs"] == []
+34
View File
@@ -28,6 +28,13 @@ def test_retarget_body_ignores_partial_title_matches():
assert page_ops.retarget_body("[[Old Thing]]", "Old", "New") == "[[Old Thing]]"
def test_retarget_body_repoints_a_wrapped_link_onto_one_line():
body = "See [[Old\n Title|the old one]] and [[Old\nTitle#Setup]]."
result = page_ops.retarget_body(body, "Old Title", "New")
assert result == "See [[New|the old one]] and [[New#Setup]]."
assert page_ops.body_references(body, "Old Title") == 2
def test_strip_link_bullets_removes_only_bare_bullets():
body = (
"## Relationships\n\n"
@@ -214,6 +221,33 @@ def test_rm_refuses_referenced_page_without_yes(patched_wiki):
page_ops.rm_command(page_title="Borealis", yes=False, dry_run=False)
assert (patched_wiki / "entities/systems/Borealis.md").exists()
def _gdeploy_wrapping_a_link_to_source_aurora(kb) -> None:
write_page(
kb / "entities/tools/gdeploy.md",
{"type": "types/entity.md", "entity_type": "tool", "tags": [],
"created": "2026-07-25", "modified": "2026-07-25", "related": [],
"sources": []},
"\n# gdeploy\n\n## Description\n\nNotes in [[Source -\n Aurora]] nightly.\n",
)
def test_rm_refuses_a_page_referenced_only_by_a_wrapped_link(patched_wiki, capsys):
"""Before the fold, the wrapped link was no reference at all, and the page
went without the `--yes` the inbound list is there to require."""
_gdeploy_wrapping_a_link_to_source_aurora(patched_wiki)
with pytest.raises(typer.Exit):
page_ops.rm_command(page_title="Source - Aurora", yes=False, dry_run=False)
assert (patched_wiki / "sources/notes/Source - Aurora.md").exists()
assert "- gdeploy" in capsys.readouterr().out
def test_rename_repoints_a_wrapped_link(patched_wiki):
_gdeploy_wrapping_a_link_to_source_aurora(patched_wiki)
page_ops.rename_command(old="Source - Aurora", new="Source - Aurora Notes", dry_run=False)
_frontmatter, body = read_page(patched_wiki / "entities/tools/gdeploy.md")
assert "Notes in [[Source - Aurora Notes]] nightly." in body
def test_rm_deletes_and_delinks_with_yes(patched_wiki):
page_ops.rm_command(page_title="Borealis", yes=True, dry_run=False)