Über Zeilenumbruch gesplitteter Wikilink: lint meldet fehlende Seite, rename/rm übersehen die Referenz #115

Closed
opened 2026-09-17 21:14:29 +00:00 by torben · 3 comments
Owner

Ergebnis

Behoben in 8.0.0-beta.30, Commit c261b8f. Verifiziert durch pytest (2129 passed, 3 skipped), docs verify und instructions verify lokal vor dem Publish, lint über den Demo-Korpus (keine Broken- und keine Wrapped-Findings) sowie CI auf dem publizierten Commit: Läufe 515 und 516, beide success.

Schlussprüfung (2026-10-03): kein docs/-Text, Contract oder Human-Doc durch die Änderung veraltet - keine docs/-Seite behandelt das Link-Parsing; die rename/rm-Records in tools/CONTRACT.md ("repoints every reference") gelten jetzt auch für umbrochene Links; EVALS.md bezieht harte Fehler aus HARD_ERROR_KEYS, die neue Kategorie ist darüber automatisch enthalten; migrate verify vergleicht Wikilink-Zahlen über count_wikilinks, das jetzt normalisiert. Keine Installationsanweisung berührt.

Befund (bis 8.0.0-beta.29)

Stand in einem [[Titel]]-Wikilink ein literaler Zeilenumbruch (plus Einrückung) zwischen [[ und ]] - typisch, wenn ein Agent Fließtext auf eine feste Spaltenbreite umbricht, ohne den Link als atomares Token zu behandeln -, las der Stack als Linkziel den Text mit dem Umbruch. Gegen den Tree geprüft (8.0.0-beta.29):

extract_wikilinks('siehe [[Foo Bar Target\n  Pattern]] ...')   -> {'Foo Bar Target\n  Pattern'}
page_ops.LINK_RE                                                -> 'Foo Bar Target\n  Pattern'
body_references(b, 'Foo Bar Target Pattern')                    -> 0
retarget_body(b, 'Foo Bar Target Pattern', 'New')               -> unverändert

Ursache: WIKILINK_RE = r"\[\[([^\]|#]+)" (tools/chemenu/kb_scan.py) und LINK_RE (tools/chemenu/commands/page_ops.py) schließen in der negierten Zeichenklasse keinen Zeilenumbruch aus; .strip() entfernte nur Whitespace an den Rändern. Der so gewonnene Titel traf nie eine Seite.

Folgen - drei, nicht nur die ursprünglich gemeldete:

  1. lint meldete den Link unter "Broken Wikilinks" als fehlende Seite, mit dem Umbruch im ausgegebenen Titel - für eine Session nicht von einer echten toten Referenz zu unterscheiden.
  2. rm zählte den umbrochenen Link nicht als Inbound-Referenz (inbound_pages → body_references) - die "still referenced"-Sperre griff nicht, die Seite wurde ohne --yes gelöscht.
  3. rename repointete ihn nicht (retarget_body) - nach dem Rename war der Link wirklich tot.

Dazu falsche Orphan-/Most-Linked-Werte, weil die Kante im Link-Graph fehlte.

Entscheidungen (umgesetzt)

D1 - Normalisieren, nicht ausschließen. Ein Linkziel wird gelesen, indem jeder Whitespace-Lauf, der einen Zeilenumbruch enthält, zu genau einem Leerzeichen zusammenfällt (danach .strip()). Verworfen: \n/\r aus der Zeichenklasse nehmen, sodass ein umbrochener Link gar nicht matcht - der Link verschwände dann still aus Graph und Report, die Zielseite könnte als Orphan auftauchen, und rm/rename sähen ihn weiterhin nicht. Kein Sonderfall für Leerzeilen im Link: [[A\n\nB]] normalisiert ebenfalls zu A B und wird ebenso von D2 gemeldet - nichts davon ist still.

D2 - Eigene Lint-Kategorie "Wrapped Wikilinks", hart. Jeder Body-Wikilink, dessen Rohziel einen Zeilenumbruch enthält, wird unter dem Key wrapped_wikilinks gemeldet ([[Seite]] wraps a wikilink across lines - write it on one: [[Normalisierter Titel]]) - unabhängig davon, ob der normalisierte Titel existiert. "Broken Wikilinks" prüft den normalisierten Titel: ein umbrochener Link auf eine existierende Seite steht nur unter Wrapped, einer auf eine fehlende Seite unter beiden (zwei echte, getrennte Defekte).
Hart (HARD_ERROR_KEYS), weil (a) kb/CONTRACT.md § Titles are identifiers verlangt, dass ein Wikilink den Titel exakt trifft, und ein Renderer einen Link mit Zeilenumbruch nicht verlässlich als Link darstellt; (b) es für bestehende Instanzen nichts verschärft - genau diese Links waren vorher schon harte "Broken Wikilinks", die Lint-Ampel einer Instanz bleibt gleich, nur die Kategorie ist präziser. Kein Migration-Gating.

D3 - Eine Normalisierung, ein Ort. kb_scan.normalize_link_target() (neben WIKILINK_RE) wird von allen Body-Wikilink-Lesern benutzt: extract_wikilinks, count_wikilinks, die Index-Links in lint_core.run_lint, page_ops.retarget_body, page_ops.body_references. retarget_body schreibt den repointeten Link einzeilig - ein Rename repariert den Umbruch also nebenbei.

Bewusst außerhalb:

  • provenance.CITE_DEF_RE (Footnotes-Definitionen): wird von cite add erzeugt, nie von Hand umbrochen; Lint hat dafür eigene harte Checks.
  • provenance.LEGACY_CITE_RE: Pre-Migration-Marker, ohnehin harter Fehler.
  • strip_link_bullets (page_ops.py) / xref.py: matchen ganze See-also-Zeilen mit escaptem Titel; ein über zwei Zeilen umbrochener Bullet bleibt bei rm/xref remove stehen - er wird aber von D2 gemeldet und ist bei rm als Inbound sichtbar.
  • Ein Auto-Fix (lint --fix o.ä.): gibt es für keine Lint-Kategorie; die Korrektur ist ein Einzeiler beim Bearbeiten der Seite.
  • Ein über eine Zeile laufender Inline-Code-Span: markdown_code.strip_code_spans maskiert Spans bewusst nur innerhalb einer Zeile (Schutz davor, dass ein fehlender Backtick einen ganzen Absatz still maskiert). Ein umbrochener Link in so einem Span wird als Prosa gelesen und gemeldet; das bleibt so.

Geänderte Dateien

  • tools/chemenu/kb_scan.py - normalize_link_target(); extract_wikilinks/count_wikilinks benutzen sie; wrapped_wikilinks() liefert die normalisierten Ziele der umbrochenen Links eines Bodys (Code maskiert wie bisher).
  • tools/chemenu/lint_core.py - wrapped_wikilinks in run_lint, Report-Sektion "Wrapped Wikilinks" direkt nach "Broken Wikilinks", Key in HARD_ERROR_KEYS mit Begründungskommentar; Index-Links über dieselbe Funktion.
  • tools/chemenu/commands/page_ops.py - retarget_body, body_references vergleichen normalisiert.
  • tools/chemenu/commands/lint.py - der lint-Command-Record nennt die Kategorie und hat eine eigene Note dazu; tools/CONTRACT.md per docs contract --apply regeneriert.
  • kb/CONTRACT.md § Titles are identifiers - Absatz: ein Wikilink ist ein Token und wird nie über Zeilen umbrochen; lint meldet ihn als Wrapped Wikilinks.
  • instructions/wiki-lint/SKILL.md - Aufzählung der mechanischen Findings ergänzt.
  • Tests: tools/chemenu/tests/test_kb_scan.py, test_lint.py, test_page_ops.py.
  • CHANGES.md - Changeset im Kandidaten-Eintrag.

Versionsteil

--minor, --impact low: neue Lint-Kategorie (neuer Report-Key, neue Sektion) plus Bugfix. Drop-in-Test: vorwärts reines Kopieren, kein Eingriff, keine Migration - die betroffenen Links waren schon vorher harte Fehler; rückwärts liest die alte Version nichts, was die neue geschrieben hat. Kein Grenzübertritt. Der Kandidat stand bereits auf 8.0.0-beta.N; der Bump ergab 8.0.0-beta.30.

Akzeptanzkriterien

  • extract_wikilinks/count_wikilinks liefern für [[Foo Bar\n Target]] und [[Foo Bar\r\nTarget|alias]] den Titel Foo Bar Target.
  • Gegen eine existierende Seite meldet lint einen umbrochenen Link nicht unter broken_links, aber unter wrapped_wikilinks; has_hard_errors (die Grundlage von lint --fail-on-error) ist wahr.
  • Ein umbrochener Link auf eine nicht existierende Seite steht unter broken_links mit dem normalisierten, einzeiligen Titel und zusätzlich unter wrapped_wikilinks.
  • Die Zielseite eines umbrochenen Links ist kein Orphan.
  • rm auf eine Seite, die nur über einen umbrochenen Link referenziert wird, verweigert ohne --yes und nennt die referenzierende Seite.
  • rename repointet einen umbrochenen Link; danach steht er einzeilig als [[Neu]] (Alias/Anker erhalten) im Body.
  • Ein umbrochener [[...]] innerhalb eines Fences erzeugt kein Finding. (Ursprünglich "Code-Spans/Fences" - eingeschränkt, siehe "Bewusst außerhalb".)
  • kb/CONTRACT.md § Titles are identifiers nennt die Regel; wiki-lint-Skill nennt die Kategorie.
  • pytest (2129 passed, 3 skipped), docs verify, instructions verify clean - lokal vor dem Publish. Demo-Korpus: keine Broken- und keine Wrapped-Findings.
  • CI grün auf dem publizierten Commit c261b8f (Läufe 515, 516).

Herkunft

Gemeldet aus einer privaten Instanz (Stack 6.1.0, 2026-09-17) nach einem wiki-ingest-Lauf, in dem zwei frisch angelegte Seiten denselben Wikilink identisch hart umbrochen hatten; dort von Hand auf eine Zeile gezogen. Der ursprüngliche Verdacht zur Ursache wurde am 2026-10-03 gegen den Tree bestätigt; die rm-/rename-Folgen wurden dabei zusätzlich gefunden.

## Ergebnis Behoben in **8.0.0-beta.30**, Commit `c261b8f`. Verifiziert durch `pytest` (2129 passed, 3 skipped), `docs verify` und `instructions verify` lokal vor dem Publish, `lint` über den Demo-Korpus (keine Broken- und keine Wrapped-Findings) sowie CI auf dem publizierten Commit: Läufe [515](https://gitea.nehmer.net/torben/chemenu/actions/runs/515) und [516](https://gitea.nehmer.net/torben/chemenu/actions/runs/516), beide `success`. Schlussprüfung (2026-10-03): kein `docs/`-Text, Contract oder Human-Doc durch die Änderung veraltet - keine `docs/`-Seite behandelt das Link-Parsing; die `rename`/`rm`-Records in `tools/CONTRACT.md` ("repoints every reference") gelten jetzt auch für umbrochene Links; EVALS.md bezieht harte Fehler aus `HARD_ERROR_KEYS`, die neue Kategorie ist darüber automatisch enthalten; `migrate verify` vergleicht Wikilink-Zahlen über `count_wikilinks`, das jetzt normalisiert. Keine Installationsanweisung berührt. ## Befund (bis 8.0.0-beta.29) Stand in einem `[[Titel]]`-Wikilink ein literaler Zeilenumbruch (plus Einrückung) zwischen `[[` und `]]` - typisch, wenn ein Agent Fließtext auf eine feste Spaltenbreite umbricht, ohne den Link als atomares Token zu behandeln -, las der Stack als Linkziel den Text **mit** dem Umbruch. Gegen den Tree geprüft (8.0.0-beta.29): ``` extract_wikilinks('siehe [[Foo Bar Target\n Pattern]] ...') -> {'Foo Bar Target\n Pattern'} page_ops.LINK_RE -> 'Foo Bar Target\n Pattern' body_references(b, 'Foo Bar Target Pattern') -> 0 retarget_body(b, 'Foo Bar Target Pattern', 'New') -> unverändert ``` Ursache: `WIKILINK_RE = r"\[\[([^\]|#]+)"` (`tools/chemenu/kb_scan.py`) und `LINK_RE` (`tools/chemenu/commands/page_ops.py`) schließen in der negierten Zeichenklasse keinen Zeilenumbruch aus; `.strip()` entfernte nur Whitespace an den Rändern. Der so gewonnene Titel traf nie eine Seite. Folgen - drei, nicht nur die ursprünglich gemeldete: 1. **`lint`** meldete den Link unter "Broken Wikilinks" als fehlende Seite, mit dem Umbruch im ausgegebenen Titel - für eine Session nicht von einer echten toten Referenz zu unterscheiden. 2. **`rm`** zählte den umbrochenen Link nicht als Inbound-Referenz (`inbound_pages` → `body_references`) - die "still referenced"-Sperre griff nicht, die Seite wurde ohne `--yes` gelöscht. 3. **`rename`** repointete ihn nicht (`retarget_body`) - nach dem Rename war der Link wirklich tot. Dazu falsche Orphan-/Most-Linked-Werte, weil die Kante im Link-Graph fehlte. ## Entscheidungen (umgesetzt) **D1 - Normalisieren, nicht ausschließen.** Ein Linkziel wird gelesen, indem jeder Whitespace-Lauf, der einen Zeilenumbruch enthält, zu genau einem Leerzeichen zusammenfällt (danach `.strip()`). Verworfen: `\n`/`\r` aus der Zeichenklasse nehmen, sodass ein umbrochener Link gar nicht matcht - der Link verschwände dann *still* aus Graph und Report, die Zielseite könnte als Orphan auftauchen, und `rm`/`rename` sähen ihn weiterhin nicht. Kein Sonderfall für Leerzeilen im Link: `[[A\n\nB]]` normalisiert ebenfalls zu `A B` und wird ebenso von D2 gemeldet - nichts davon ist still. **D2 - Eigene Lint-Kategorie "Wrapped Wikilinks", hart.** Jeder Body-Wikilink, dessen Rohziel einen Zeilenumbruch enthält, wird unter dem Key `wrapped_wikilinks` gemeldet (`[[Seite]] wraps a wikilink across lines - write it on one: [[Normalisierter Titel]]`) - unabhängig davon, ob der normalisierte Titel existiert. "Broken Wikilinks" prüft den **normalisierten** Titel: ein umbrochener Link auf eine existierende Seite steht nur unter Wrapped, einer auf eine fehlende Seite unter beiden (zwei echte, getrennte Defekte). Hart (`HARD_ERROR_KEYS`), weil (a) `kb/CONTRACT.md` § Titles are identifiers verlangt, dass ein Wikilink den Titel exakt trifft, und ein Renderer einen Link mit Zeilenumbruch nicht verlässlich als Link darstellt; (b) es für bestehende Instanzen nichts verschärft - genau diese Links waren vorher schon harte "Broken Wikilinks", die Lint-Ampel einer Instanz bleibt gleich, nur die Kategorie ist präziser. Kein Migration-Gating. **D3 - Eine Normalisierung, ein Ort.** `kb_scan.normalize_link_target()` (neben `WIKILINK_RE`) wird von allen Body-Wikilink-Lesern benutzt: `extract_wikilinks`, `count_wikilinks`, die Index-Links in `lint_core.run_lint`, `page_ops.retarget_body`, `page_ops.body_references`. `retarget_body` schreibt den repointeten Link einzeilig - ein Rename repariert den Umbruch also nebenbei. **Bewusst außerhalb:** - `provenance.CITE_DEF_RE` (Footnotes-Definitionen): wird von `cite add` erzeugt, nie von Hand umbrochen; Lint hat dafür eigene harte Checks. - `provenance.LEGACY_CITE_RE`: Pre-Migration-Marker, ohnehin harter Fehler. - `strip_link_bullets` (`page_ops.py`) / `xref.py`: matchen ganze See-also-Zeilen mit escaptem Titel; ein über zwei Zeilen umbrochener Bullet bleibt bei `rm`/`xref remove` stehen - er wird aber von D2 gemeldet und ist bei `rm` als Inbound sichtbar. - Ein Auto-Fix (`lint --fix` o.ä.): gibt es für keine Lint-Kategorie; die Korrektur ist ein Einzeiler beim Bearbeiten der Seite. - Ein über eine Zeile laufender Inline-Code-Span: `markdown_code.strip_code_spans` maskiert Spans bewusst nur innerhalb einer Zeile (Schutz davor, dass ein fehlender Backtick einen ganzen Absatz still maskiert). Ein umbrochener Link in so einem Span wird als Prosa gelesen und gemeldet; das bleibt so. ## Geänderte Dateien - `tools/chemenu/kb_scan.py` - `normalize_link_target()`; `extract_wikilinks`/`count_wikilinks` benutzen sie; `wrapped_wikilinks()` liefert die normalisierten Ziele der umbrochenen Links eines Bodys (Code maskiert wie bisher). - `tools/chemenu/lint_core.py` - `wrapped_wikilinks` in `run_lint`, Report-Sektion "Wrapped Wikilinks" direkt nach "Broken Wikilinks", Key in `HARD_ERROR_KEYS` mit Begründungskommentar; Index-Links über dieselbe Funktion. - `tools/chemenu/commands/page_ops.py` - `retarget_body`, `body_references` vergleichen normalisiert. - `tools/chemenu/commands/lint.py` - der `lint`-Command-Record nennt die Kategorie und hat eine eigene Note dazu; `tools/CONTRACT.md` per `docs contract --apply` regeneriert. - `kb/CONTRACT.md` § Titles are identifiers - Absatz: ein Wikilink ist ein Token und wird nie über Zeilen umbrochen; `lint` meldet ihn als *Wrapped Wikilinks*. - `instructions/wiki-lint/SKILL.md` - Aufzählung der mechanischen Findings ergänzt. - Tests: `tools/chemenu/tests/test_kb_scan.py`, `test_lint.py`, `test_page_ops.py`. - `CHANGES.md` - Changeset im Kandidaten-Eintrag. ## Versionsteil **`--minor`**, `--impact low`: neue Lint-Kategorie (neuer Report-Key, neue Sektion) plus Bugfix. Drop-in-Test: vorwärts reines Kopieren, kein Eingriff, keine Migration - die betroffenen Links waren schon vorher harte Fehler; rückwärts liest die alte Version nichts, was die neue geschrieben hat. Kein Grenzübertritt. Der Kandidat stand bereits auf 8.0.0-beta.N; der Bump ergab 8.0.0-beta.30. ## Akzeptanzkriterien - [x] `extract_wikilinks`/`count_wikilinks` liefern für `[[Foo Bar\n Target]]` und `[[Foo Bar\r\nTarget|alias]]` den Titel `Foo Bar Target`. - [x] Gegen eine existierende Seite meldet `lint` einen umbrochenen Link **nicht** unter `broken_links`, aber unter `wrapped_wikilinks`; `has_hard_errors` (die Grundlage von `lint --fail-on-error`) ist wahr. - [x] Ein umbrochener Link auf eine **nicht** existierende Seite steht unter `broken_links` mit dem normalisierten, einzeiligen Titel und zusätzlich unter `wrapped_wikilinks`. - [x] Die Zielseite eines umbrochenen Links ist kein Orphan. - [x] `rm` auf eine Seite, die nur über einen umbrochenen Link referenziert wird, verweigert ohne `--yes` und nennt die referenzierende Seite. - [x] `rename` repointet einen umbrochenen Link; danach steht er einzeilig als `[[Neu]]` (Alias/Anker erhalten) im Body. - [x] Ein umbrochener `[[...]]` innerhalb eines Fences erzeugt kein Finding. (Ursprünglich "Code-Spans/Fences" - eingeschränkt, siehe "Bewusst außerhalb".) - [x] `kb/CONTRACT.md` § Titles are identifiers nennt die Regel; `wiki-lint`-Skill nennt die Kategorie. - [x] `pytest` (2129 passed, 3 skipped), `docs verify`, `instructions verify` clean - lokal vor dem Publish. Demo-Korpus: keine Broken- und keine Wrapped-Findings. - [x] CI grün auf dem publizierten Commit `c261b8f` (Läufe 515, 516). ## Herkunft Gemeldet aus einer privaten Instanz (Stack 6.1.0, 2026-09-17) nach einem `wiki-ingest`-Lauf, in dem zwei frisch angelegte Seiten denselben Wikilink identisch hart umbrochen hatten; dort von Hand auf eine Zeile gezogen. Der ursprüngliche Verdacht zur Ursache wurde am 2026-10-03 gegen den Tree bestätigt; die `rm`-/`rename`-Folgen wurden dabei zusätzlich gefunden.
torben added the area/kbkind/defectstatus/unconfirmed labels 2026-09-17 21:15:01 +00:00
torben changed title from wikitool lint: ueber Zeilenumbruch gesplitteter Wikilink wird als fehlende Seite gemeldet statt als Syntaxfehler erkannt to Über Zeilenumbruch gesplitteter Wikilink: lint meldet fehlende Seite, rename/rm übersehen die Referenz 2026-10-03 09:08:15 +00:00
torben added prio/plannedsize/Mkind/build and removed kind/defectstatus/unconfirmed labels 2026-10-03 09:08:21 +00:00
Author
Owner

Changelog: Verdacht gegen den Tree (8.0.0-beta.29) bestätigt, status/unconfirmed entfernt. Neu gefunden: auch rm (Inbound-Sperre) und rename (Repoint) übersehen umbrochene Links (page_ops.LINK_RE). Body neu als Spec: D1 normalisieren statt ausschließen, D2 eigene harte Lint-Kategorie wrapped_wikilinks, D3 eine Normalisierungsfunktion in kb_scan.py für alle Body-Leser; Bewusst-außerhalb-Liste, Dateien, Versionsteil --minor, Akzeptanzkriterien als prüfbare Eigenschaften. Labels: kind/defect → kind/build, prio/planned, size/M. Titel präzisiert.

**Changelog:** Verdacht gegen den Tree (8.0.0-beta.29) bestätigt, `status/unconfirmed` entfernt. Neu gefunden: auch `rm` (Inbound-Sperre) und `rename` (Repoint) übersehen umbrochene Links (`page_ops.LINK_RE`). Body neu als Spec: D1 normalisieren statt ausschließen, D2 eigene harte Lint-Kategorie `wrapped_wikilinks`, D3 eine Normalisierungsfunktion in `kb_scan.py` für alle Body-Leser; Bewusst-außerhalb-Liste, Dateien, Versionsteil `--minor`, Akzeptanzkriterien als prüfbare Eigenschaften. Labels: `kind/defect` → `kind/build`, `prio/planned`, `size/M`. Titel präzisiert.
Author
Owner

Changelog: Gebaut und publiziert als 8.0.0-beta.30 (c261b8f), CI grün (Läufe 515, 516). Alle Akzeptanzkriterien abgehakt. Abweichung: das Code-Kriterium gilt nur für Fences - ein über die Zeile laufender Inline-Code-Span wird bewusst nicht maskiert (markdown_code.py), steht jetzt unter "Bewusst außerhalb". Zusätzlich angefasst: lint-Command-Record (Aufzählung + eigene Note), CHANGES.md. Neuer Abschnitt "Stand".

**Changelog:** Gebaut und publiziert als 8.0.0-beta.30 (`c261b8f`), CI grün (Läufe 515, 516). Alle Akzeptanzkriterien abgehakt. Abweichung: das Code-Kriterium gilt nur für Fences - ein über die Zeile laufender Inline-Code-Span wird bewusst nicht maskiert (`markdown_code.py`), steht jetzt unter "Bewusst außerhalb". Zusätzlich angefasst: `lint`-Command-Record (Aufzählung + eigene Note), `CHANGES.md`. Neuer Abschnitt "Stand".
Author
Owner

Changelog: Body in Endfassung: "Stand" → "Ergebnis" mit allen Verifikationen und der Schlussprüfung (kein Dokument veraltet, keine Installationsanweisung berührt); Befund in Vergangenheitsform. Geschlossen.

**Changelog:** Body in Endfassung: "Stand" → "Ergebnis" mit allen Verifikationen und der Schlussprüfung (kein Dokument veraltet, keine Installationsanweisung berührt); Befund in Vergangenheitsform. Geschlossen.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#115