feat: wikitool move - eine kb-Seite folgt ihrem Subtype ins berechnete Verzeichnis (#56)
CI / verify (push) Successful in 58s
Release / release (push) Successful in 35s

Files changed:
- CHANGES.md
- VERSION
- instructions/page-lifecycle.md
- instructions/publish-cycle.md
- tools/CONTRACT.md
- tools/chemenu/cli.py
- tools/chemenu/commands/log_append.py
- tools/chemenu/commands/migrate_cmd.py
- tools/chemenu/commands/new_page.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/corpus_diff.py
- tools/chemenu/lint_core.py
- tools/chemenu/tests/test_corpus_diff.py
- tools/chemenu/tests/test_lint.py
- tools/chemenu/tests/test_migrate_cmd.py
- tools/chemenu/tests/test_page_ops.py
- tools/chemenu/tests/test_type_resolver.py
- tools/chemenu/type_resolver.py
This commit is contained in:
2026-09-04 22:50:22 +02:00
parent 8e25a7865f
commit 9e414319b8
18 changed files with 615 additions and 54 deletions
+61
View File
@@ -27,6 +27,7 @@ from chemenu.provenance import legacy_source_pages as find_legacy_source_pages
from chemenu.provenance import orphan_footnote_defs as find_orphan_footnote_defs
from chemenu.provenance import uncovered_raw_files as find_uncovered_raw_files
from chemenu.provenance import undefined_footnote_refs as find_undefined_footnote_refs
from chemenu.page import Page
from chemenu.version import Version
from chemenu.kb_scan import (
GENERATED_INDEX,
@@ -76,6 +77,52 @@ def count_quote_blocks(body: str) -> int:
return count
def _display(path: Path) -> str:
"""A path for a report line: repo-root-relative if possible, the raw path
otherwise (a fixture tree in a test, or any tree `config.ROOT` does not
contain)."""
try:
return str(path.relative_to(config.ROOT))
except ValueError:
return str(path)
def find_misplaced(pages: dict[str, Page]) -> list[tuple[str, Page, Path]]:
"""(title, page, target_dir) for every page whose type resolves and whose
current directory differs from the one `TypeResolver.compute_target_dir`
would place it under - the same rule `new` places a page by. Shared by
`misplaced_pages()` below and `wikitool move --reconcile`, which is the
fix for what this finds.
A page with no `type:` or an unresolvable one is skipped - each is
reported separately, as `frontmatter_errors`/`type_resolution_errors`."""
found = []
for title, page in sorted(pages.items()):
type_path = page.frontmatter.get("type")
if not type_path:
continue
try:
target_dir = resolver.compute_target_dir(type_path, page.frontmatter, page.path)
except ValueError:
continue
if page.path.parent.resolve() != target_dir.resolve():
found.append((title, page, target_dir))
return found
def misplaced_pages(pages: dict[str, Page]) -> list[dict]:
"""Report form of `find_misplaced`: `{"page", "at", "should_be"}` per
finding, for the lint report.
Advisory rather than a hard error (see `HARD_ERROR_KEYS` below): plenty of
existing instances predate `move`, and a hand-placed page is not a
broken one - only one `wikitool move --page "<Title>"` would relocate."""
return [
{"page": title, "at": _display(page.path.parent), "should_be": _display(target_dir)}
for title, page, target_dir in find_misplaced(pages)
]
def run_lint(kb_dir: Path) -> dict:
pages = load_kb_pages(kb_dir)
duplicate_titles = find_duplicate_title_paths(kb_dir, config.ROOT)
@@ -139,6 +186,8 @@ def run_lint(kb_dir: Path) -> dict:
if h1 is not None and h1 != title:
title_mismatches.append({"page": title, "h1": h1})
misplaced = misplaced_pages(pages)
unmarked_provenance = []
for title, page in sorted(pages.items()):
if page.kind not in ("entity", "concept"):
@@ -318,6 +367,7 @@ def run_lint(kb_dir: Path) -> dict:
"dangling_index_entries": dangling_index_entries,
"title_mismatches": title_mismatches,
"duplicate_titles": duplicate_titles,
"misplaced_pages": misplaced,
"uncovered_raw_files": find_uncovered_raw_files(config.RAW_DIR, pages),
"broken_raw_refs": find_broken_raw_refs(pages),
"duplicate_raw_file_owners": find_duplicate_raw_file_owners(pages),
@@ -381,6 +431,12 @@ def render_markdown(report: dict) -> str:
lines, "Filename / H1 Title Mismatches", report["title_mismatches"],
lambda i: f"[[{i['page']}]] H1 is '{i['h1']}'",
)
_section(
lines, "Misplaced Pages (not under their type-spec's computed directory)",
report.get("misplaced_pages", []),
lambda i: f"[[{i['page']}]] is at `{i['at']}`, should be under `{i['should_be']}` "
f"- `wikitool move --page \"{i['page']}\"`",
)
_section(
lines, "Uncovered Raw Files (no source page)", report["uncovered_raw_files"],
lambda i: f"`{i}`",
@@ -529,6 +585,11 @@ def default_report_path(report: dict) -> Path:
# is no version at which the redundancy becomes an error, only a sweep someone
# does or does not get to.
#
# `misplaced_pages` is the same shape again: it arrived long after most
# instances' corpora were hand-placed, a hand-placed page is not a broken one,
# and there is no version at which "not under the computed directory" becomes
# wrong - only `wikitool move` someone does or does not get to run.
#
# `malformed_edges` and `unbalanced_markers` are hard from the start: neither
# describes an unconverted page, only a broken one.
#