lint liest Zitat-Syntax in Backticks und Codeblöcken als echte Referenz #20

Closed
opened 2026-08-31 13:44:17 +00:00 by torben · 2 comments
Owner

Der Defekt

CITE_REF_RE ist ein reiner Textmatch (\[\^(...)\]) ohne Kenntnis von Markdown-Codespannen. Eine Seite, die Zitat-Syntax erwähnt statt sie zu benutzen, produziert damit Undefined Footnote References — und das ist ein Hard-Error, der lint --fail-on-error und damit CI rot macht.

Nachgestellt:

body = 'Prosa mit dem Token `[^cite-id]` in Backticks, und einem Codeblock:\n\n```\n[^s-beispiel]: [[Source - X]]\n```\n'
[m.group(1) for m in CITE_REF_RE.finditer(body)]
# -> ['cite-id', 's-beispiel']

Beide sind Erwähnungen. Keine ist eine Referenz.

Beobachtet

Beim Ingest am 2026-08-31 (c28f8ce). Der ausführende Agent schrieb Concept-Seiten über den Zitat-Mechanismus — das Thema der Quelle war ein Datenverlust im Fußnoten-Block — und setzte die Syntax dabei in Backticks. Ergebnis: fünf undefinierte Referenzen, Hard-Error. Er hat es vor dem Publish gefangen und die Prosa umformuliert („Zitatdefinitionszeilen", „Fußnotenreferenz").

Der Ausweg war also, über die eigene Syntax nicht in ihrer eigenen Schreibweise zu schreiben. Das ist die falsche Richtung: eine Wissensbasis über einen Stack muss dessen Notation zitieren können.

Warum das mehr als kosmetisch ist

  • Es trifft ausgerechnet die Seiten, die den Stack dokumentieren — also genau das, was dieses Wiki über sich selbst weiß.
  • Der Fehler ist hart, nicht advisory: er blockiert CI.
  • Die Ausweichformulierung ist unsichtbar. Niemand, der die Seite später liest, erkennt, dass dort eine Umschreibung steht, weil der Linter das Original nicht zulässt.

Betroffene Prüfungen

undefined_footnote_refs sicher. Zu prüfen, ob dieselbe Blindheit auch gilt für:

  • orphan_footnote_defs (Gegenrichtung: eine Definition in einem Codeblock zählt als echte Definition)
  • citation_frontmatter_drift
  • legacy_citation_markers
  • extract_inline_cites in provenance.py — dort entscheidet es mit, was als belegt gilt, also potenziell auch über Konfidenz

Ebenso zu prüfen: [[wikilinks]] in Codeblöcken. Wenn broken_links denselben Textmatch macht, ist ein Beispiel-Wikilink in einer Doku-Seite ein kaputter Link — vermutlich derselbe Defekt an einer zweiten Stelle.

Lösungsrichtung

Codespannen und eingezäunte Blöcke vor dem Matchen maskieren, an genau einer Stelle. Eine Hilfsfunktion in provenance.py (strip_code_spans(body)), die

  • eingezäunte Blöcke (``` und ~~~, inklusive Sprach-Info),
  • eingerückte Codeblöcke,
  • Inline-Codespannen (auch mehrfache Backticks)

durch Leerraum gleicher Länge ersetzt, sodass Offsets erhalten bleiben, falls ein Aufrufer sie braucht.

Die Alternative — den Regexen einzeln Kontextlogik beibringen — wäre die zweite Kopie derselben Regel und genau das, was Invariante 8 verbietet.

Akzeptanzkriterien

  • Eine Seite, die [^cite-id] in Backticks und eine Definitionszeile in einem Codeblock enthält, erzeugt keinen undefined_footnote_refs- und keinen orphan_footnote_defs-Befund. Das ist der Test, und er muss zuerst rot sein.
  • Eine echte Referenz direkt neben einer erwähnten wird weiterhin gefunden — die Maskierung darf nicht zu viel schlucken.
  • Geprüft und dokumentiert, welche der oben genannten Prüfungen betroffen waren; jede behobene bekommt ihren eigenen Testfall.
  • [[wikilink]]-Beispiele in Codeblöcken sind untersucht; falls betroffen, im selben Zug behoben oder als eigenes Issue abgetrennt.
  • Die Seiten, die heute umformuliert wurden, dürfen die Syntax wieder direkt schreiben. Ob sie zurückgeändert werden, ist eine Inhaltsentscheidung und nicht Teil dieses Issues.
  • Changelog, PATCH — kein Kommando-Interface ändert sich.

Hinweis

Passt in die Reihe von Green Suite Blind Spot: auch dieser Fall war von keinem Test abgedeckt, weil bisher niemand eine Seite über die Zitat-Notation geschrieben hatte.

## Der Defekt `CITE_REF_RE` ist ein reiner Textmatch (`\[\^(...)\]`) ohne Kenntnis von Markdown-Codespannen. Eine Seite, die Zitat-Syntax **erwähnt** statt sie zu benutzen, produziert damit `Undefined Footnote References` — und das ist ein Hard-Error, der `lint --fail-on-error` und damit CI rot macht. Nachgestellt: ```python body = 'Prosa mit dem Token `[^cite-id]` in Backticks, und einem Codeblock:\n\n```\n[^s-beispiel]: [[Source - X]]\n```\n' [m.group(1) for m in CITE_REF_RE.finditer(body)] # -> ['cite-id', 's-beispiel'] ``` Beide sind Erwähnungen. Keine ist eine Referenz. ## Beobachtet Beim Ingest am 2026-08-31 (`c28f8ce`). Der ausführende Agent schrieb Concept-Seiten **über** den Zitat-Mechanismus — das Thema der Quelle war ein Datenverlust im Fußnoten-Block — und setzte die Syntax dabei in Backticks. Ergebnis: fünf undefinierte Referenzen, Hard-Error. Er hat es vor dem Publish gefangen und die Prosa umformuliert („Zitatdefinitionszeilen", „Fußnotenreferenz"). Der Ausweg war also, **über die eigene Syntax nicht in ihrer eigenen Schreibweise zu schreiben.** Das ist die falsche Richtung: eine Wissensbasis über einen Stack muss dessen Notation zitieren können. ## Warum das mehr als kosmetisch ist - Es trifft ausgerechnet die Seiten, die den Stack dokumentieren — also genau das, was dieses Wiki über sich selbst weiß. - Der Fehler ist **hart**, nicht advisory: er blockiert CI. - Die Ausweichformulierung ist unsichtbar. Niemand, der die Seite später liest, erkennt, dass dort eine Umschreibung steht, weil der Linter das Original nicht zulässt. ## Betroffene Prüfungen `undefined_footnote_refs` sicher. Zu prüfen, ob dieselbe Blindheit auch gilt für: - `orphan_footnote_defs` (Gegenrichtung: eine Definition in einem Codeblock zählt als echte Definition) - `citation_frontmatter_drift` - `legacy_citation_markers` - `extract_inline_cites` in `provenance.py` — dort entscheidet es mit, was als belegt gilt, also potenziell auch über Konfidenz Ebenso zu prüfen: `[[wikilinks]]` in Codeblöcken. Wenn `broken_links` denselben Textmatch macht, ist ein Beispiel-Wikilink in einer Doku-Seite ein kaputter Link — vermutlich derselbe Defekt an einer zweiten Stelle. ## Lösungsrichtung Codespannen und eingezäunte Blöcke vor dem Matchen maskieren, an genau einer Stelle. Eine Hilfsfunktion in `provenance.py` (`strip_code_spans(body)`), die - eingezäunte Blöcke (```` ``` ```` und `~~~`, inklusive Sprach-Info), - eingerückte Codeblöcke, - Inline-Codespannen (auch mehrfache Backticks) durch Leerraum gleicher Länge ersetzt, sodass Offsets erhalten bleiben, falls ein Aufrufer sie braucht. Die Alternative — den Regexen einzeln Kontextlogik beibringen — wäre die zweite Kopie derselben Regel und genau das, was Invariante 8 verbietet. ## Akzeptanzkriterien - [ ] Eine Seite, die `[^cite-id]` in Backticks und eine Definitionszeile in einem Codeblock enthält, erzeugt keinen `undefined_footnote_refs`- und keinen `orphan_footnote_defs`-Befund. Das ist der Test, und er muss zuerst rot sein. - [ ] Eine *echte* Referenz direkt neben einer erwähnten wird weiterhin gefunden — die Maskierung darf nicht zu viel schlucken. - [ ] Geprüft und dokumentiert, welche der oben genannten Prüfungen betroffen waren; jede behobene bekommt ihren eigenen Testfall. - [ ] `[[wikilink]]`-Beispiele in Codeblöcken sind untersucht; falls betroffen, im selben Zug behoben oder als eigenes Issue abgetrennt. - [ ] Die Seiten, die heute umformuliert wurden, dürfen die Syntax wieder direkt schreiben. Ob sie zurückgeändert werden, ist eine Inhaltsentscheidung und nicht Teil dieses Issues. - [ ] Changelog, **PATCH** — kein Kommando-Interface ändert sich. ## Hinweis Passt in die Reihe von `Green Suite Blind Spot`: auch dieser Fall war von keinem Test abgedeckt, weil bisher niemand eine Seite über die Zitat-Notation geschrieben hatte.
torben added the prio/plannedsize/S labels 2026-08-31 13:44:23 +00:00
Author
Owner

Umgesetzt in 1.7.2 (49bd7d4).

Was gebaut wurde

Die Hilfsfunktion liegt nicht in provenance.py, sondern in einem neuen Modul tools/wiki_tools/markdown_code.py. Grund: kb_scan.extract_wikilinks() braucht dieselbe Regel, und einen Wikilink-Belang aus provenance zu importieren wäre die falsche Richtung. Der Name aus dem Issue bleibt (strip_code_spans).

Sie ersetzt eingezäunte Blöcke (``` und ~~~, inklusive der Fence-Zeilen selbst) und Inline-Codespannen durch Leerzeichen gleicher Länge — Offsets, Zeilenstruktur und Gesamtlänge bleiben erhalten, sodass ein Aufrufer gegen den maskierten Text matchen und den echten schneiden kann. Genau das tut split_cite_block().

Durchgereicht an: provenance.iter_cite_refs() (neu, der eine Einstiegspunkt für Referenz-Scans), split_cite_block() für Definitionen, legacy_citation_markers(), kb_scan.extract_wikilinks() und count_wikilinks().

Zwei bewusste Grenzen

Eingerückte Codeblöcke werden nicht maskiert. In diesem Korpus ist eine Vier-Leerzeichen-Einrückung weit öfter eine Listenfortsetzung als Code: von drei eingerückten kb/-Zeilen mit Wiki-Notation sind zwei Aufzählungspunkte, deren [[wikilink]] ein echter Link ist (Event-Driven Automation). Eine Maskierung nach Einrückung hätte sie stumm aus dem Linkgraphen gelöscht. CommonMarks eigene Regel dafür braucht den Listenkontext, nicht die Zeile.

Inline-Codespannen werden zeilenlokal gematcht. Ein fehlender schließender Backtick ist ein häufiger Tippfehler, und ein Matcher über Zeilengrenzen macht daraus einen stumm maskierten Absatz. Zu viel zu maskieren lässt Befunde verschwinden — die Richtung, die niemandem auffällt.

Aus demselben Grund sucht split_cite_block() die Fußnoten-Überschrift weiterhin im unmaskierten Body: ein einziger unclosed Fence irgendwo in der Prosa würde sonst die Überschrift mitschwärzen, die Seite hätte keine Definitionen mehr, und jede Zitat-Referenz darauf wäre undefiniert. Ein gezeigtes ## Fußnoten im Codeblock ist der seltenere und billigere Unfall.

Betroffene Prüfungen — geprüft und dokumentiert

Prüfung Betroffen Behoben
undefined_footnote_refs ja ja, über iter_cite_refs()
orphan_footnote_defs ja, beide Richtungen ja
citation_frontmatter_drift ja, transitiv über extract_inline_cites ja
legacy_citation_markers ja ja
extract_inline_cites (provenance.py) ja — entschied mit, was als belegt gilt ja
broken_links / extract_wikilinks ja, derselbe Defekt an zweiter Stelle ja, im selben Zug
count_wikilinks (corpus_diff) ja ja

Der Fund, der eine Entscheidung brauchte

Der Korpus hatte die spiegelbildliche Gewohnheit: 21 [^s-…]-Marker standen innerhalb eingezäunter Blöcke, hinter einer Kommando- oder YAML-Zeile, auf vier Seiten. Sie haben dort nie als Fußnote gerendert — GFM zeigt sie wörtlich, wer den Befehl kopiert, kopiert den Marker mit. Mit der Maskierung wurden zwei davon zu verwaisten Definitionen, also Hard-Error.

Entscheidung des Nutzers: Fences mitmaskieren und die Seiten korrigieren. Die Marker stehen jetzt auf je einer Quelle: [^s-…]-Zeile unter ihrem Block — atlantis (5 Blöcke), docker.nehmer.net (2), hephaestus.nehmer.net (2), External Ingress Model (1). Kein Befehl und keine Konfiguration wurde inhaltlich geändert; modified: über wikitool touch, Eintrag in kb/log.md.

Die Regel dazu steht jetzt in kb/CONTRACT.md bei der Zitat-Sektion — sonst wäre sie nur im Linter kodiert, und ein Autor, der den Marker künftig in einen Fence setzt, verlöre das Zitat stumm.

Akzeptanzkriterien

  • Eine Seite mit [^cite-id] in Backticks und einer Definitionszeile im Codeblock erzeugt weder undefined_footnote_refs noch orphan_footnote_defstest_lint_ignores_citation_syntax_shown_as_code. Zuerst rot: gegen eine No-op-Maskierung schlagen die drei maskierungsabhängigen Lint-Tests und die drei Provenance-Tests fehl (verifiziert).
  • Eine echte Referenz direkt neben einer erwähnten wird weiterhin gefunden — test_lint_still_sees_a_real_citation_beside_a_mentioned_one, plus test_real_notation_next_to_a_mentioned_one_survives auf Modulebene.
  • Betroffene Prüfungen geprüft und dokumentiert (Tabelle oben); jede behobene hat einen eigenen Testfall.
  • [[wikilink]]-Beispiele in Codeblöcken untersucht — betroffen, im selben Zug behoben (test_lint_ignores_wikilink_examples_in_code). Gemessen: 7 Beispiel-Links fallen aus dem Graphen, null neue Orphans, null neue Broken Links.
  • Die heute umformulierten Seiten dürfen die Syntax wieder direkt schreiben. Ob sie zurückgeändert werden, bleibt wie vorgesehen eine Inhaltsentscheidung außerhalb dieses Issues.
  • Changelog, PATCH — 1.7.1 → 1.7.2.

Neu

tools/wiki_tools/tests/test_markdown_code.py (11 Tests) pinnt die Eigenschaften, auf die sich alle sechs Prüfungen verlassen: Offset-Erhalt, Tilde-Fences, längerer Fence wird nicht von einem kürzeren Lauf geschlossen, unclosed Fence läuft bis Dateiende, mehrfache Backticks, unclosed Backtick maskiert nichts, eingerückte Zeilen bleiben unangetastet.

Umgesetzt in 1.7.2 (`49bd7d4`). ## Was gebaut wurde Die Hilfsfunktion liegt **nicht** in `provenance.py`, sondern in einem neuen Modul `tools/wiki_tools/markdown_code.py`. Grund: `kb_scan.extract_wikilinks()` braucht dieselbe Regel, und einen Wikilink-Belang aus `provenance` zu importieren wäre die falsche Richtung. Der Name aus dem Issue bleibt (`strip_code_spans`). Sie ersetzt eingezäunte Blöcke (``` und `~~~`, inklusive der Fence-Zeilen selbst) und Inline-Codespannen durch Leerzeichen gleicher Länge — Offsets, Zeilenstruktur und Gesamtlänge bleiben erhalten, sodass ein Aufrufer gegen den maskierten Text matchen und den echten schneiden kann. Genau das tut `split_cite_block()`. Durchgereicht an: `provenance.iter_cite_refs()` (neu, der eine Einstiegspunkt für Referenz-Scans), `split_cite_block()` für Definitionen, `legacy_citation_markers()`, `kb_scan.extract_wikilinks()` und `count_wikilinks()`. ## Zwei bewusste Grenzen **Eingerückte Codeblöcke werden nicht maskiert.** In diesem Korpus ist eine Vier-Leerzeichen-Einrückung weit öfter eine Listenfortsetzung als Code: von drei eingerückten `kb/`-Zeilen mit Wiki-Notation sind zwei Aufzählungspunkte, deren `[[wikilink]]` ein echter Link ist (`Event-Driven Automation`). Eine Maskierung nach Einrückung hätte sie stumm aus dem Linkgraphen gelöscht. CommonMarks eigene Regel dafür braucht den Listenkontext, nicht die Zeile. **Inline-Codespannen werden zeilenlokal gematcht.** Ein fehlender schließender Backtick ist ein häufiger Tippfehler, und ein Matcher über Zeilengrenzen macht daraus einen stumm maskierten Absatz. Zu viel zu maskieren lässt Befunde *verschwinden* — die Richtung, die niemandem auffällt. Aus demselben Grund sucht `split_cite_block()` die Fußnoten-Überschrift weiterhin im **unmaskierten** Body: ein einziger unclosed Fence irgendwo in der Prosa würde sonst die Überschrift mitschwärzen, die Seite hätte keine Definitionen mehr, und jede Zitat-Referenz darauf wäre undefiniert. Ein gezeigtes `## Fußnoten` im Codeblock ist der seltenere und billigere Unfall. ## Betroffene Prüfungen — geprüft und dokumentiert | Prüfung | Betroffen | Behoben | |---|---|---| | `undefined_footnote_refs` | ja | ja, über `iter_cite_refs()` | | `orphan_footnote_defs` | ja, beide Richtungen | ja | | `citation_frontmatter_drift` | ja, transitiv über `extract_inline_cites` | ja | | `legacy_citation_markers` | ja | ja | | `extract_inline_cites` (`provenance.py`) | ja — entschied mit, was als belegt gilt | ja | | `broken_links` / `extract_wikilinks` | ja, derselbe Defekt an zweiter Stelle | ja, im selben Zug | | `count_wikilinks` (`corpus_diff`) | ja | ja | ## Der Fund, der eine Entscheidung brauchte Der Korpus hatte die spiegelbildliche Gewohnheit: **21 `[^s-…]`-Marker standen innerhalb eingezäunter Blöcke**, hinter einer Kommando- oder YAML-Zeile, auf vier Seiten. Sie haben dort nie als Fußnote gerendert — GFM zeigt sie wörtlich, wer den Befehl kopiert, kopiert den Marker mit. Mit der Maskierung wurden zwei davon zu verwaisten Definitionen, also Hard-Error. Entscheidung des Nutzers: Fences mitmaskieren **und** die Seiten korrigieren. Die Marker stehen jetzt auf je einer `Quelle: [^s-…]`-Zeile unter ihrem Block — `atlantis` (5 Blöcke), `docker.nehmer.net` (2), `hephaestus.nehmer.net` (2), `External Ingress Model` (1). Kein Befehl und keine Konfiguration wurde inhaltlich geändert; `modified:` über `wikitool touch`, Eintrag in `kb/log.md`. Die Regel dazu steht jetzt in `kb/CONTRACT.md` bei der Zitat-Sektion — sonst wäre sie nur im Linter kodiert, und ein Autor, der den Marker künftig in einen Fence setzt, verlöre das Zitat stumm. ## Akzeptanzkriterien - [x] Eine Seite mit `[^cite-id]` in Backticks und einer Definitionszeile im Codeblock erzeugt weder `undefined_footnote_refs` noch `orphan_footnote_defs` — `test_lint_ignores_citation_syntax_shown_as_code`. Zuerst rot: gegen eine No-op-Maskierung schlagen die drei maskierungsabhängigen Lint-Tests und die drei Provenance-Tests fehl (verifiziert). - [x] Eine echte Referenz direkt neben einer erwähnten wird weiterhin gefunden — `test_lint_still_sees_a_real_citation_beside_a_mentioned_one`, plus `test_real_notation_next_to_a_mentioned_one_survives` auf Modulebene. - [x] Betroffene Prüfungen geprüft und dokumentiert (Tabelle oben); jede behobene hat einen eigenen Testfall. - [x] `[[wikilink]]`-Beispiele in Codeblöcken untersucht — betroffen, im selben Zug behoben (`test_lint_ignores_wikilink_examples_in_code`). Gemessen: 7 Beispiel-Links fallen aus dem Graphen, null neue Orphans, null neue Broken Links. - [x] Die heute umformulierten Seiten dürfen die Syntax wieder direkt schreiben. Ob sie zurückgeändert werden, bleibt wie vorgesehen eine Inhaltsentscheidung außerhalb dieses Issues. - [x] Changelog, PATCH — 1.7.1 → 1.7.2. ## Neu `tools/wiki_tools/tests/test_markdown_code.py` (11 Tests) pinnt die Eigenschaften, auf die sich alle sechs Prüfungen verlassen: Offset-Erhalt, Tilde-Fences, längerer Fence wird nicht von einem kürzeren Lauf geschlossen, unclosed Fence läuft bis Dateiende, mehrfache Backticks, unclosed Backtick maskiert nichts, eingerückte Zeilen bleiben unangetastet.
Author
Owner

Korrektur zur eigenen Zusammenfassung oben: 12, nicht 21, In-Fence-Zitatmarker.

Nachgezählt am Diff von 49bd7d4: git show 49bd7d4 -- <die vier Seiten> | grep -c "^-.*\[\^s-" → 12 (atlantis: 6, docker.nehmer.net: 2, hephaestus.nehmer.net: 3, External Ingress Model: 1). Die 21 war eine Verwechslung während der Sitzung: 9 Beispiel-Wikilinks in Codeblöcken (unverändert, kein Zitat-Fund, nur zur Illustration im Scan mitgezählt) plus 12 echte Zitatmarker wurden addiert und das Ergebnis fälschlich als „Zitatmarker" bezeichnet.

Die vier Seiten und der Fix selbst sind davon nicht betroffen — nur die Zahl in der Prosa oben, in CHANGES.md (jetzt korrigiert) und in kb/log.md (dort per neuem Log-Eintrag korrigiert, da log.md append-only ist).

Korrektur zur eigenen Zusammenfassung oben: **12**, nicht 21, In-Fence-Zitatmarker. Nachgezählt am Diff von `49bd7d4`: `git show 49bd7d4 -- <die vier Seiten> | grep -c "^-.*\[\^s-"` → 12 (atlantis: 6, docker.nehmer.net: 2, hephaestus.nehmer.net: 3, External Ingress Model: 1). Die 21 war eine Verwechslung während der Sitzung: 9 Beispiel-Wikilinks in Codeblöcken (unverändert, kein Zitat-Fund, nur zur Illustration im Scan mitgezählt) plus 12 echte Zitatmarker wurden addiert und das Ergebnis fälschlich als „Zitatmarker" bezeichnet. Die vier Seiten und der Fix selbst sind davon nicht betroffen — nur die Zahl in der Prosa oben, in `CHANGES.md` (jetzt korrigiert) und in `kb/log.md` (dort per neuem Log-Eintrag korrigiert, da `log.md` append-only ist).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#20