fix: a wikilink wrapped across a line break is its own lint finding; rename and rm see it (#115)
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:
1 parent
b3022b8ffb
commit
c261b8f4ca
12 files changed
+204
-13
No files matched your search
@@ -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()
|
||||
@@ -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"] == []
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user