feat: lint reports sections that hold only template placeholders as Unfilled Template Sections (#94)
Files changed: - CHANGES.md - EVALS.md - VERSION - instructions/wiki-lint/SKILL.md - instructions/wiki-manage/SKILL.md - tools/CONTRACT.md - tools/chemenu/blocks.py - tools/chemenu/commands/lint.py - tools/chemenu/evals/scorecard.py - tools/chemenu/lint_core.py - tools/chemenu/tests/test_blocks.py - tools/chemenu/tests/test_evals.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_pipeline_l0.py - types/type-spec.md 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
af8bcb5b57
commit
a2a6baa265
15 files changed
+320
-5
No files matched your search
+20
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
||||
|
||||
---
|
||||
|
||||
## 8.0.0-beta.36 - 2026-10-04 - kb/CONTRACT.md names section anchors in wikilinks and the Broken Anchors finding
|
||||
## 8.0.0-beta.37 - 2026-10-04 - lint: Unfilled Template Sections - a section still holding only its template's TODO placeholders (advisory)
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
@@ -106,6 +106,7 @@ concern - readable here, never shipped as something to parse.
|
||||
- The test suite no longer ships, and dist upgrade deletes what a release stops shipping
|
||||
- Link-Katalog: Beteiligungs- und RACI-Label von der Projektseite aus
|
||||
- Organisationsseiten: Personen als Abschnitt mit Aufstieg, entity_type organization, member-of, Lint-Befund broken_anchors
|
||||
- lint: Unfilled Template Sections - a section still holding only its template's TODO placeholders (advisory)
|
||||
|
||||
**Low impact**
|
||||
- version bump no longer points at version release in its output
|
||||
@@ -149,6 +150,24 @@ concern - readable here, never shipped as something to parse.
|
||||
- kb/CONTRACT.md names section anchors in wikilinks and the Broken Anchors finding
|
||||
<!-- /wikitool:bumps -->
|
||||
|
||||
### lint: Unfilled Template Sections - a section still holding only its template's TODO placeholders (advisory)
|
||||
|
||||
`lint` had no finding for a page without substance: every check measured structure, links,
|
||||
provenance or schema, so a page carrying only its `wikitool new` scaffold and one line read the
|
||||
same as a complete one - and a later session took its subject as covered and stopped looking at
|
||||
the source. The new advisory finding `unfilled_sections` names each page with a `##` section
|
||||
whose non-blank lines are all template placeholders, with those sections' headings. A
|
||||
placeholder line is one whose content starts with the token `TODO` (after an optional list
|
||||
marker, checkbox, table cell or bold field label); code, generated regions and footnote
|
||||
definitions are out of view, and neither an empty section nor a single open field beside written
|
||||
ones is reported. Word count and "page carries a placeholder" were measured against two corpora
|
||||
and rejected - both misjudge short complete pages or pages with one honest gap marker; the
|
||||
section rule separated stubs from complete pages in both. On the demo corpus it names 34 pages.
|
||||
`types/type-spec.md` now makes `TODO` the contract for placeholder text in a template, an
|
||||
instance-owned one included; `wiki-lint` keeps the finding out of its mechanical repair step;
|
||||
`eval score` counts it under `advisories`. Not a hard error, so no instance's
|
||||
`lint --fail-on-error` turns red on this upgrade (Gitea #94).
|
||||
|
||||
### kb/CONTRACT.md names section anchors in wikilinks and the Broken Anchors finding
|
||||
|
||||
The stage contract's rules for writing a wikilink (§ Titles are identifiers) covered wrapped links
|
||||
|
||||
@@ -330,7 +330,8 @@ giving it its own runner would have duplicated the suite to no end.
|
||||
|
||||
One behaviour it pins is easy to mistake for a defect: **a scaffolded page does not lint
|
||||
clean**. `new` writes placeholder wikilinks for the author to replace, so a page that was
|
||||
created but not yet written reports broken links. That is the scaffold saying it is unfinished.
|
||||
created but not yet written reports broken links, and its `TODO`-only sections as the advisory
|
||||
*Unfilled Template Sections*. That is the scaffold saying it is unfinished.
|
||||
|
||||
### How much of the stack the suite reaches
|
||||
|
||||
|
||||
@@ -47,9 +47,16 @@ mechanical half looks exactly like a complete one.
|
||||
names on Windows and macOS, filename/title mismatches, broken
|
||||
`raw_files:` references, raw files claimed by more than one source page, invalid type paths,
|
||||
schema failures, citation/frontmatter drift, and edges whose label is missing, not authorised
|
||||
by the source collection, or redundant beside a specific label on the reverse direction.
|
||||
by the source collection, or redundant beside a specific label on the reverse direction, and
|
||||
pages with a section that still holds nothing but its template's `TODO` placeholders.
|
||||
**Do not re-derive any of it by reading pages.**
|
||||
|
||||
*Unfilled Template Sections* is not mechanical either - do **not** clear it under step 7.
|
||||
Filling a section is authoring from a source (`wiki-manage`, AGENTS.md invariant 3), and
|
||||
deleting its placeholders to quiet the finding leaves the same unwritten page without the
|
||||
marker that made it visible. Retiring the page instead is `instructions/page-lifecycle.md`.
|
||||
Report the pages at step 9.
|
||||
|
||||
The *Redundant see-also* section is the one that looks mechanical and is not - do **not**
|
||||
clear it under step 7. It names a `see-also` edge standing beside a specific label on the
|
||||
reverse direction, and the obvious repair destroys the thing worth keeping: `xref remove`
|
||||
|
||||
@@ -47,7 +47,8 @@ requirements come from `tools/wikitool types describe <type>`.
|
||||
4. **Gather what the wiki already knows** - `tools/wikitool search` again, for the surrounding
|
||||
subjects - so the prose connects to existing pages instead of restating them.
|
||||
|
||||
5. **Draft.** Fill in the generated skeleton's TODO sections, following the tone rules in
|
||||
5. **Draft.** Fill in the generated skeleton's TODO sections - `lint` reports a section still
|
||||
made of nothing else as unfilled - following the tone rules in
|
||||
`kb/CONVENTIONS.md` § Tone. If `provenance:` is `sourced` or `mixed`, cite
|
||||
hard facts as you write them with `tools/wikitool cite add --page "<Title>" --source
|
||||
"Source - X"`, which also adds `X` to `sources:` - paste the `[^cite-id]` marker it prints.
|
||||
|
||||
@@ -1132,6 +1132,7 @@ Run structural lint checks against kb/.
|
||||
- Advisory only: source pages sitting in the `unclassified` catalog slot.
|
||||
- Advisory only: Long Paths - a file under `kb/` or `raw/` whose path below the instance root is over 160 UTF-16 code units, the budget that keeps a Windows checkout without long paths working. Reported as `{path, length}`; a corpus over the budget breaks no lint run. `wikitool rename` is the fix for a page.
|
||||
- Advisory only: quote-limit overages (>2 blockquotes/page).
|
||||
- Advisory only: Unfilled Template Sections - a page with at least one `##` section whose non-blank lines are all template placeholders (content starting with `TODO`, after an optional list marker, checkbox, table cell or bold field label), reported as `{page, sections}`. Code, generated regions and footnote definitions do not count; an empty section, or one placeholder beside written lines, is no finding. Writing the section from the page's sources or retiring the page is the fix - not a mechanical one.
|
||||
- Prints only the sections that found something and always writes the full report to `reports/Lint Report <date>.md` (or `--markdown`), naming the path. `--full` prints everything; `--json` prints the findings and writes nothing.
|
||||
- Exits 0 whatever it finds unless `--fail-on-error` is passed.
|
||||
|
||||
|
||||
@@ -125,6 +125,22 @@ def marker_pairs(body: str) -> dict[str, int]:
|
||||
return {name: min(opens.count(name), closes.count(name)) for name in sorted(names)}
|
||||
|
||||
|
||||
_ANY_REGION_RE = re.compile(
|
||||
rf"<!-- wikitool:({_NAME}) -->.*?<!-- /wikitool:\1 -->", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def mask_regions(body: str) -> str:
|
||||
"""`body` with every complete generated region, markers included, replaced
|
||||
by spaces - same length, same line structure, like
|
||||
`markdown_code.strip_code_spans()`, so a scan of what the author wrote can
|
||||
run over the result without reading what the tool wrote. An unpaired
|
||||
marker masks nothing; `unbalanced_markers()` is that finding."""
|
||||
return _ANY_REGION_RE.sub(
|
||||
lambda m: "".join(c if c == "\n" else " " for c in m.group(0)), body
|
||||
)
|
||||
|
||||
|
||||
def unbalanced_markers(body: str) -> list[str]:
|
||||
"""Region names whose open and close markers do not pair up."""
|
||||
opens = [m.group(1) for m in _ANY_OPEN_RE.finditer(body)]
|
||||
|
||||
@@ -94,6 +94,12 @@ __all__ = [
|
||||
"without long paths working. Reported as `{path, length}`; a corpus over the budget breaks "
|
||||
"no lint run. `wikitool rename` is the fix for a page.",
|
||||
"Advisory only: quote-limit overages (>2 blockquotes/page).",
|
||||
"Advisory only: Unfilled Template Sections - a page with at least one `##` section whose "
|
||||
"non-blank lines are all template placeholders (content starting with `TODO`, after an "
|
||||
"optional list marker, checkbox, table cell or bold field label), reported as "
|
||||
"`{page, sections}`. Code, generated regions and footnote definitions do not count; an "
|
||||
"empty section, or one placeholder beside written lines, is no finding. Writing the "
|
||||
"section from the page's sources or retiring the page is the fix - not a mechanical one.",
|
||||
"Prints only the sections that found something and always writes the full report to "
|
||||
"`reports/Lint Report <date>.md` (or `--markdown`), naming the path. `--full` prints "
|
||||
"everything; `--json` prints the findings and writes nothing.",
|
||||
|
||||
@@ -31,6 +31,7 @@ ADVISORY_KEYS = (
|
||||
"missing_from_index",
|
||||
"title_mismatches",
|
||||
"redundant_see_also",
|
||||
"unfilled_sections",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ of the line because the markdown report is a data product (it is what
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,6 +24,7 @@ from chemenu import blocks, config, kb_collections, links
|
||||
from chemenu.catalog import SHARD_THRESHOLD, group_pages
|
||||
from chemenu.frontmatter_io import frontmatter_error
|
||||
from chemenu.markdown_code import strip_code_spans
|
||||
from chemenu.provenance import CITE_DEF_RE
|
||||
from chemenu.provenance import broken_raw_refs as find_broken_raw_refs
|
||||
from chemenu.provenance import duplicate_raw_file_owners as find_duplicate_raw_file_owners
|
||||
from chemenu.provenance import extract_inline_cites
|
||||
@@ -333,6 +335,77 @@ def broken_anchors(pages: dict[str, Page]) -> list[dict]:
|
||||
return findings
|
||||
|
||||
|
||||
# The token every type-spec template writes its placeholder text with
|
||||
# (`types/type-spec.md` § Template variables). A line is a placeholder when its
|
||||
# content starts with it - after an optional list marker, task checkbox, table
|
||||
# cell opener and bold field label - and the token ends there: `TODO: ...`,
|
||||
# `TODO (falls zutreffend)`, `| TODO |`. Not `TODO.md`, `TODO-Liste` or
|
||||
# `TODOs`, and not a `TODO` anywhere but the start of the line's content.
|
||||
PLACEHOLDER_TOKEN = "TODO"
|
||||
_PLACEHOLDER_LINE_RE = re.compile(
|
||||
r"^[ \t]*"
|
||||
r"(?:(?:[-*+]|\d+[.)])[ \t]+)?"
|
||||
r"(?:\[[ xX]\][ \t]+)?"
|
||||
r"(?:\|[ \t]*)?"
|
||||
r"(?:\*\*[^*\n]+\*\*[ \t]*)?"
|
||||
+ re.escape(PLACEHOLDER_TOKEN)
|
||||
+ r"(?=$|[\s:()|])"
|
||||
)
|
||||
_SECTION_HEADING_RE = re.compile(r"^##[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$")
|
||||
|
||||
|
||||
def unfilled_sections(body: str) -> list[str]:
|
||||
"""The `##` headings of this body whose sections hold nothing but
|
||||
placeholder lines, in page order.
|
||||
|
||||
A section runs to the next `##` heading; a `###` heading inside it is
|
||||
content, not a boundary. A section with no non-blank line at all is not
|
||||
reported - an empty legacy heading is untidy, not an unwritten scaffold -
|
||||
and neither is one where a single placeholder sits beside written lines: a
|
||||
field nobody could fill (`**Version:** TODO (nicht erfasst)`) is an honest
|
||||
gap marker, and a finding on it would push a session to delete the marker
|
||||
or invent a value.
|
||||
|
||||
What the author did not write is out of view first: generated regions and
|
||||
footnote definitions are dropped as if absent, and code is masked, so a
|
||||
`TODO` quoted in a span or a fence is content rather than a placeholder.
|
||||
"""
|
||||
visible = blocks.mask_regions(body)
|
||||
visible = CITE_DEF_RE.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), visible)
|
||||
masked = strip_code_spans(visible).split("\n")
|
||||
sections: list[tuple[str, bool, bool]] = [] # heading, any line, all placeholders
|
||||
for line, masked_line in zip(visible.split("\n"), masked):
|
||||
heading = _SECTION_HEADING_RE.match(masked_line)
|
||||
if heading:
|
||||
sections.append((_SECTION_HEADING_RE.match(line).group(1), False, True))
|
||||
continue
|
||||
if not sections or not line.strip():
|
||||
continue
|
||||
title, _, all_placeholders = sections[-1]
|
||||
sections[-1] = (title, True, all_placeholders and bool(_PLACEHOLDER_LINE_RE.match(masked_line)))
|
||||
return [title for title, any_line, all_placeholders in sections if any_line and all_placeholders]
|
||||
|
||||
|
||||
def unfilled_section_pages(pages: dict[str, Page]) -> list[dict]:
|
||||
"""`{"page", "sections"}` for every page with at least one section that is
|
||||
still nothing but its template's placeholders.
|
||||
|
||||
Advisory (see `HARD_ERROR_KEYS`). What it is for is not the thin page
|
||||
itself but what the page does to the next session: it stands in the
|
||||
catalog, `search` finds it, and a reader takes the subject as covered and
|
||||
does not open the source again - costlier than a missing page, which is
|
||||
visibly missing. Word count could not tell such a page from a short
|
||||
complete one, and "carries a placeholder" also hits complete pages with
|
||||
one field left open; a section made of placeholders alone separated both.
|
||||
"""
|
||||
findings = []
|
||||
for title, page in sorted(pages.items()):
|
||||
sections = unfilled_sections(page.body)
|
||||
if sections:
|
||||
findings.append({"page": title, "sections": sections})
|
||||
return findings
|
||||
|
||||
|
||||
def run_lint(kb_dir: Path) -> dict:
|
||||
pages = load_kb_pages(kb_dir)
|
||||
duplicate_titles = find_duplicate_title_paths(kb_dir, config.ROOT)
|
||||
@@ -581,6 +654,7 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
"broken_links": broken_links,
|
||||
"wrapped_wikilinks": wrapped_links,
|
||||
"broken_anchors": broken_anchors(pages),
|
||||
"unfilled_sections": unfilled_section_pages(pages),
|
||||
"orphan_pages": orphan_pages,
|
||||
"most_linked": most_linked,
|
||||
"inbound_counts": inbound_counts,
|
||||
@@ -651,6 +725,12 @@ def render_markdown(report: dict) -> str:
|
||||
lambda i: f"[[{i['page']}]] links to [[{i['target']}#{i['anchor']}]], but [[{i['target']}]] "
|
||||
"has no such heading - point the link at the page that section became, or drop the anchor",
|
||||
)
|
||||
_section(
|
||||
lines, "Unfilled Template Sections - advisory, not an error",
|
||||
report.get("unfilled_sections", []),
|
||||
lambda i: f"[[{i['page']}]] - only template placeholders under: {', '.join(i['sections'])} "
|
||||
"- write them from the page's sources (wiki-manage) or retire the page (page-lifecycle)",
|
||||
)
|
||||
_section(lines, "Orphan Pages (no inbound links)", report["orphan_pages"], lambda i: f"[[{i}]]")
|
||||
_section(
|
||||
lines, f"Most-Linked Pages (top {MOST_LINKED_COUNT} hubs)", report["most_linked"],
|
||||
@@ -906,6 +986,13 @@ def default_report_path(report: dict) -> Path:
|
||||
# instance's lint red on the upgrade that shipped it, which no other part of
|
||||
# that upgrade asked for.
|
||||
#
|
||||
# `unfilled_sections` is advisory for the reason `broken_anchors` is: it
|
||||
# arrived after corpora that already carry such pages - the demo corpus had 34 -
|
||||
# and failing on it would turn their lint red on the upgrade that shipped it. It
|
||||
# is not migration-gated either: no migration can fill a section mechanically,
|
||||
# so there is no version at which the corpus has grown into it, only pages
|
||||
# someone does or does not get to write or retire.
|
||||
#
|
||||
# `wrapped_wikilinks` is hard from the start without tightening anything: every
|
||||
# link it names was a hard `broken_links` finding before the link graph learned
|
||||
# to fold a wrapped target, so an instance's lint is exactly as red as it was -
|
||||
|
||||
@@ -88,3 +88,14 @@ def test_marker_pairs_counts_rather_than_sets():
|
||||
body = blocks.replace(PROSE, blocks.LINKS, _links())
|
||||
doubled = body + "\n" + _links() + "\n"
|
||||
assert blocks.marker_pairs(doubled) == {"links": 2}
|
||||
|
||||
|
||||
def test_mask_regions_keeps_offsets_and_skips_an_unpaired_marker():
|
||||
body = blocks.replace(PROSE, blocks.LINKS, _links())
|
||||
masked = blocks.mask_regions(body)
|
||||
assert len(masked) == len(body)
|
||||
assert masked.count("\n") == body.count("\n")
|
||||
assert masked.startswith(PROSE.rstrip("\n"))
|
||||
assert "Beziehungen" not in masked and "wikitool" not in masked
|
||||
unpaired = body.replace(blocks.close_marker(blocks.LINKS), "")
|
||||
assert blocks.mask_regions(unpaired) == unpaired
|
||||
@@ -168,6 +168,15 @@ def test_an_advisory_alone_does_not_fail_a_run(clean_report):
|
||||
assert any(not r["passed"] for r in card["trajectory"])
|
||||
|
||||
|
||||
def test_unfilled_sections_are_counted_as_an_advisory(clean_report):
|
||||
"""A session that leaves a scaffold unwritten shows in the score without
|
||||
failing it."""
|
||||
report = {**clean_report, "unfilled_sections": [{"page": "X", "sections": ["A"]}]}
|
||||
card = scorecard.score("s", records=[], report=report)
|
||||
assert card["structure"]["advisories"]["unfilled_sections"] == 1
|
||||
assert not card["structure"]["hard_errors"]
|
||||
|
||||
|
||||
def test_a_broken_tree_fails_the_run_whatever_the_trajectory(clean_report):
|
||||
card = scorecard.score("s", records=[], report={**clean_report, "broken_links": [{"page": "X"}]})
|
||||
assert scorecard.failed(card)
|
||||
|
||||
@@ -16,6 +16,7 @@ from chemenu.commands.lint import (
|
||||
run_lint,
|
||||
)
|
||||
from chemenu.frontmatter_io import write_page
|
||||
from chemenu.lint_core import unfilled_sections
|
||||
from chemenu.provenance import cite_id, render_cite_block
|
||||
from chemenu.version import Version
|
||||
|
||||
@@ -1111,3 +1112,147 @@ def test_rendered_report_names_rename_for_a_long_path(kb_dir, raw_dir):
|
||||
text = render_markdown(run_lint(kb_dir))
|
||||
assert "## Long Paths" in text
|
||||
assert "wikitool rename" in text
|
||||
|
||||
|
||||
# --- unfilled template sections ---
|
||||
|
||||
def _scaffolded_concept(name: str) -> str:
|
||||
"""The body `wikitool new concept` scaffolds, rendered from the shipped
|
||||
template rather than copied here, so a template change cannot leave this
|
||||
test judging a skeleton nobody produces any more."""
|
||||
from chemenu.commands.new_page import _apply_template_variables
|
||||
|
||||
spec = resolver.load_type_spec("types/concept.md")
|
||||
return _apply_template_variables(
|
||||
resolver.page_template(spec), {"name": name, "concept_type": "pattern"}
|
||||
)
|
||||
|
||||
|
||||
def _concept_page(kb_dir, name: str, body: str) -> None:
|
||||
write_page(
|
||||
kb_dir / f"concepts/protocols/{name}.md",
|
||||
{"type": "types/concept.md", "concept_type": "pattern", "tags": [],
|
||||
"created": "2026-10-04", "modified": "2026-10-04", "related": [], "sources": [],
|
||||
"provenance": "general"},
|
||||
"\n" + body + "\n",
|
||||
)
|
||||
|
||||
|
||||
def test_a_freshly_scaffolded_concept_reports_its_unwritten_sections(kb_dir):
|
||||
"""`Beispiele` and `Verwandte Concepts` hold placeholder wikilinks, not
|
||||
`TODO` lines - written by nobody, but not what the token marks, so they
|
||||
stay out of the finding."""
|
||||
_concept_page(kb_dir, "Scaffold", _scaffolded_concept("Scaffold"))
|
||||
report = run_lint(kb_dir)
|
||||
assert {
|
||||
"page": "Scaffold",
|
||||
"sections": ["Definition", "Kernpunkte", "Wann zu verwenden", "Wann NICHT zu verwenden"],
|
||||
} in report["unfilled_sections"]
|
||||
assert "only template placeholders under: Definition, Kernpunkte" in render_markdown(report)
|
||||
assert "Unfilled Template Sections" in render_summary(report)
|
||||
|
||||
|
||||
def test_the_same_concept_written_is_no_finding(kb_dir):
|
||||
body = _scaffolded_concept("Written")
|
||||
for placeholder, prose in (
|
||||
("TODO: Klare Definition dessen, was dieses Concept ist.", "Ein Muster für X."),
|
||||
("- TODO: Kernpunkt 1", "- Erstens."),
|
||||
("- TODO: Kernpunkt 2", "- Zweitens."),
|
||||
("- TODO: Kernpunkt 3", "- Drittens."),
|
||||
("TODO: Bedingungen und Kontexte, in denen dieses Concept greift", "Wenn Y gilt."),
|
||||
("TODO: Anti-Muster, Warnungen oder Situationen, in denen es fehl am Platz ist", "Nie bei Z."),
|
||||
):
|
||||
assert placeholder in body
|
||||
body = body.replace(placeholder, prose)
|
||||
_concept_page(kb_dir, "Written", body)
|
||||
assert not any(f["page"] == "Written" for f in run_lint(kb_dir)["unfilled_sections"])
|
||||
|
||||
|
||||
def test_the_fixture_corpus_has_no_unfilled_sections(kb_dir):
|
||||
assert run_lint(kb_dir)["unfilled_sections"] == []
|
||||
|
||||
|
||||
def test_one_open_field_beside_filled_ones_is_no_finding():
|
||||
"""The honest gap marker: a complete page whose source does not give a
|
||||
version. A finding here would push a session to delete the marker or
|
||||
invent the value."""
|
||||
body = (
|
||||
"# Tool\n\n## Übersicht\n\nEin Werkzeug.\n\n## Kerndaten\n\n"
|
||||
"- **Zweck:** Bauen\n- **Version:** TODO (nicht erfasst)\n"
|
||||
"- **Repository:** TODO (falls zutreffend)\n"
|
||||
)
|
||||
assert unfilled_sections(body) == []
|
||||
|
||||
|
||||
def test_a_short_complete_page_is_no_finding():
|
||||
body = "# Rohit\n\n## Übersicht\n\nAutor des Artikels. Weitere Angaben macht die Quelle nicht.\n"
|
||||
assert unfilled_sections(body) == []
|
||||
|
||||
|
||||
def test_an_empty_section_is_no_finding():
|
||||
"""A heading with nothing under it is untidy, not an unwritten scaffold."""
|
||||
assert unfilled_sections("# P\n\n## Leer\n\n## Voll\n\nText.\n\n## Auch leer\n") == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line", [
|
||||
"Siehe `TODO` im Code.",
|
||||
"`TODO: kein Platzhalter`",
|
||||
"[[TODO Retirement]] beschreibt das.",
|
||||
"Ein TODO mitten im Satz.",
|
||||
"TODO.md war die alte Liste.",
|
||||
"TODO-Liste und TODOs.",
|
||||
"- **Datei:** TODO.md",
|
||||
])
|
||||
def test_todo_that_is_not_a_placeholder(line):
|
||||
assert unfilled_sections(f"# P\n\n## A\n\n{line}\n") == []
|
||||
|
||||
|
||||
def test_todo_in_a_fence_is_content():
|
||||
assert unfilled_sections("# P\n\n## A\n\n```\nTODO: fix\n```\n") == []
|
||||
|
||||
|
||||
def test_generated_regions_and_footnote_definitions_are_out_of_view():
|
||||
"""A generated region is not part of the section above it, and its own
|
||||
heading opens no section; a footnote definition is not content."""
|
||||
body = (
|
||||
"# P\n\n## A\n\nTODO: Text\n\n"
|
||||
"<!-- wikitool:links -->\n## Beziehungen\n\n- **see-also:** [[TODO List]]\n"
|
||||
"<!-- /wikitool:links -->\n\n"
|
||||
"<!-- wikitool:footnotes -->\n## Fußnoten\n\nTODO: generated\n"
|
||||
"<!-- /wikitool:footnotes -->\n"
|
||||
)
|
||||
assert unfilled_sections(body) == ["A"]
|
||||
assert unfilled_sections("# P\n\n## B\n\nTODO: x\n[^s]: [[Source - S]]\n") == ["B"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line", [
|
||||
"TODO",
|
||||
"TODO: Satz",
|
||||
"TODO (falls zutreffend)",
|
||||
"- TODO: Punkt",
|
||||
"* TODO",
|
||||
"1. TODO: Schritt",
|
||||
"- [ ] TODO: Aufgabe",
|
||||
"- **Zweck:** TODO",
|
||||
"**Zweck:** TODO (nicht erfasst)",
|
||||
"| TODO | x |",
|
||||
])
|
||||
def test_placeholder_forms(line):
|
||||
assert unfilled_sections(f"# P\n\n## A\n\n{line}\n") == ["A"]
|
||||
|
||||
|
||||
def test_a_section_runs_to_the_next_level_two_heading():
|
||||
"""A `###` heading is content of its `##` section, so a section whose
|
||||
only written line is a subheading is written - and the preamble before
|
||||
the first `##` belongs to no section."""
|
||||
body = "# P\n\nTODO: preamble\n\n## A\n\nTODO: x\n\n### Sub\n\n## B\n\nTODO: y\n- TODO: z\n"
|
||||
assert unfilled_sections(body) == ["B"]
|
||||
|
||||
|
||||
def test_unfilled_sections_alone_keep_lint_green():
|
||||
"""Advisory: corpora carrying such pages predate the check, and no
|
||||
migration can fill a section."""
|
||||
assert "unfilled_sections" not in HARD_ERROR_KEYS
|
||||
report = {key: [] for key in HARD_ERROR_KEYS}
|
||||
report["unfilled_sections"] = [{"page": "a", "sections": ["B"]}]
|
||||
assert not has_hard_errors(report)
|
||||
@@ -96,6 +96,8 @@ def test_a_scaffolded_page_is_not_yet_a_finished_one(empty_kb):
|
||||
assert report["broken_links"], \
|
||||
"the scaffold no longer carries placeholder links - update this test"
|
||||
assert {f["page"] for f in report["broken_links"]} == {"Scaffold Only"}
|
||||
assert [f["page"] for f in report["unfilled_sections"]] == ["Scaffold Only"], \
|
||||
"the scaffold no longer marks its placeholders with TODO - see types/type-spec.md"
|
||||
|
||||
|
||||
def test_a_wiki_built_by_the_tools_lints_clean(empty_kb):
|
||||
|
||||
@@ -233,6 +233,15 @@ between markers by `xref` and `cite`, rendered from frontmatter, and re-rendered
|
||||
so scaffolding them would create a section an author is forbidden to edit and the tool would
|
||||
replace anyway. See `tools/chemenu/blocks.py`.
|
||||
|
||||
**Placeholder text in a template starts with the token `TODO`** - as the whole line or after a
|
||||
list marker, checkbox, table cell or bold field label (`TODO: ...`, `- TODO: ...`,
|
||||
`- **Version:** TODO (falls zutreffend)`, `| TODO |`), with the token followed by the line end,
|
||||
whitespace, `:`, `(`, `)` or `|`. It is a contract, not a habit: `wikitool lint` reports a `##`
|
||||
section whose non-blank lines on a page are still all such lines as *Unfilled Template Sections*
|
||||
(advisory), and a template that marks its placeholders any other way - an instance-owned one
|
||||
included - leaves its unwritten sections invisible to that finding. A single placeholder beside
|
||||
written lines is not reported, so a field whose value is genuinely unknown may keep its marker.
|
||||
|
||||
### Ownership boundary
|
||||
|
||||
| Owned here | Owned by `kb/CONTRACT.md` | Owned by `kb/CONVENTIONS.md` and the collection contracts |
|
||||
|
||||
Reference in new issue
Block a user