--- type: plan-task title: "Task 03 — Linkcheck: Schließzaun nach CommonMark" status: draft summary: "strip_code schließt einen Fenced-Codeblock nur noch mit gleichem Zeichen, mindestens gleicher Länge und ohne Info-String; sechs neue Regressionsfälle, Herkunft im Entwurf." tags: [plan-task, vault-tools] created: 2026-09-13 updated: 2026-09-13 modell: haiku modell_begruendung: "Vier Ersetzungen mit wörtlichem Alt- und Neutext in drei namentlich genannten Dateien, erwartete Ausgabe vor und nach der Umsetzung steht fest; kein Ermessen." --- # Task 03 — Linkcheck: Schließzaun nach CommonMark Globale Vorgaben: `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben". Sie gelten hier vollständig und werden nicht wiederholt. ## Zieldateien - `D:\Claude\.vault-tools\linkcheck\faelle.py` (ändern) - `D:\Claude\.vault-tools\linkcheck\linkcheck.py` (ändern) - `D:\Claude\.vault-tools\linkcheck\ENTWURF-codeblock-erkennung.md` (ändern) ## Verbote - Keine andere Datei anfassen, auch nicht `regelwerk_check.py`, obwohl dessen Kommentar die Zahl der Fälle nennt. - Keine Änderung an der Öffnerregel `_FENCE_RE`, an `_INLINE_CODE_RE`, an `wikilinks()` oder an `lauf()`. - Jede Ersetzung positionsgenau über den angegebenen Alttext, kein globales Suchen und Ersetzen. ## Schritt 1 — Test zuerst: sechs Fälle in `faelle.py` Ersetze in `D:\Claude\.vault-tools\linkcheck\faelle.py` diesen Alttext: ```python (16, "escaptes Pipe am Zeilenende ohne Alias", r"Siehe [[Ziel\|]] dort.", ["Ziel"]), ] ``` durch diesen Neutext: ````python (16, "escaptes Pipe am Zeilenende ohne Alias", r"Siehe [[Ziel\|]] dort.", ["Ziel"]), (17, "Dreier-Zaun mit Info-String in Vierer-Zaun", "````markdown\n```bash\ngrep '[[:space:]]' datei\n```\n````\n[[Danach]]", ["Danach"]), (18, "Dreier-Schliesszaun schliesst Vierer nicht", "````\n```\n[[drin]]\n````\n[[Danach]]", ["Danach"]), (19, "Vierer-Zaun schliesst Vierer-Zaun", "````\n[[drin]]\n````\n[[Danach]]", ["Danach"]), (20, "```bash in einem ```-Block schliesst nicht", "```\n```bash\n[[drin]]\n```\n[[Danach]]", ["Danach"]), (21, "Schliesszaun mit Leerraum dahinter", "```\n[[drin]]\n``` \t\n[[Danach]]", ["Danach"]), (22, "Schliesszaun mit CR am Zeilenende", "```\r\n[[drin]]\r\n```\r\n[[Danach]]\r\n", ["Danach"]), ] ```` Fall 19, 21 und 22 laufen schon vor der Umsetzung grün. Sie sichern ab, dass die neue Regel nichts kaputtmacht, was bisher ging (19), und dass der neue Ausdruck Leerraum und CR hinter dem Schließzaun zulässt (21, 22). ## Schritt 2 — Fehlschlag beobachten ```powershell Set-Location D:\Claude $env:PYTHONIOENCODING = "utf-8" python .vault-tools\linkcheck\faelle.py; "exit=$LASTEXITCODE" ``` Erwartet vor der Umsetzung: - Die Zeilen für `FALL 17`, `FALL 18` und `FALL 20` tragen `FEHLER`. - `FALL 17` meldet `erwartet ['Danach'], bekommen [':space:', 'Danach']`. - `FALL 18` meldet `erwartet ['Danach'], bekommen ['drin']`. - `FALL 20` meldet `erwartet ['Danach'], bekommen ['drin']`. - Alle anderen Fälle `ok`. - Schlusszeile `20 von 23 Faellen bestanden.`, danach `exit=1`. Sieht die Ausgabe anders aus, hier anhalten und melden. Nicht mit Schritt 3 weitermachen. ## Schritt 3 — Umsetzung in `linkcheck.py` ### 3a Kopfkommentar, Herkunft Alttext: ```text - Ungeschlossener Zaun gilt bis Dateiende: PROJEKTVORGABE - die konservative Wahl, lieber einen echten Link uebersehen als Rauschen erzeugen. ``` Neutext: ```text - Ungeschlossener Zaun gilt bis Dateiende: PROJEKTVORGABE - die konservative Wahl, lieber einen echten Link uebersehen als Rauschen erzeugen. - Schliesszaun: QUELLE - dasselbe Zeichen wie der Oeffner, mindestens so lang, danach nur Leerzeichen oder Tab. Abgeschrieben aus markdown-it-py 4.2.0, rules_block/fence.py Zeilen 42-43, 106 und 119-123; Referenz ist CommonMark 0.31.2, Abschnitt 4.5 Fenced code blocks. Fundstellen und der offene Abgleich mit dem Spezifikationstext: ENTWURF-codeblock-erkennung.md. ``` ### 3b Kopfkommentar, benannte Lücke Alttext: ```text - Eingerueckte Codebloecke (vier Leerzeichen) werden NICHT erkannt. Sie wuerden eingerueckte Listenpunkte mit echten Links verschlucken. ``` Neutext: ```text - Eingerueckte Codebloecke (vier Leerzeichen) werden NICHT erkannt. Sie wuerden eingerueckte Listenpunkte mit echten Links verschlucken. - Ein Oeffner mit Backtick im Info-String gilt hier als Zaun. CommonMark liest so eine Zeile als Inline-Code. Nicht behandelt. ``` ### 3c Ausdruck für den Schließzaun Alttext: ```python _FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") _INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)*?\1") ``` Neutext: ```python _FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") # Schliesszaun: nur Zaunzeichen, danach hoechstens Leerzeichen, Tab und ein # CR. Eine Zeile mit Info-String im Block ist deshalb Inhalt, kein Ende. _SCHLUSS_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})[ \t]*\r?$") _INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)*?\1") ``` ### 3d Zustand in `strip_code()` Alttext: ```python if zaun is None: if m: zaun = m.group(1)[0] * 3 aus.append(" " * len(zeile)) continue aus.append(_INLINE_CODE_RE.sub(lambda t: " " * len(t.group(0)), zeile)) else: if m and m.group(1)[0] * 3 == zaun: zaun = None aus.append(" " * len(zeile)) ``` Neutext: ```python if zaun is None: if m: zaun = m.group(1) aus.append(" " * len(zeile)) continue aus.append(_INLINE_CODE_RE.sub(lambda t: " " * len(t.group(0)), zeile)) else: s = _SCHLUSS_RE.match(zeile) # Gleiches Zeichen und mindestens so lang wie der Oeffner: ein # Dreier-Zaun schliesst keinen Vierer-Zaun. if s and s.group(1)[0] == zaun[0] and len(s.group(1)) >= len(zaun): zaun = None aus.append(" " * len(zeile)) ``` ## Schritt 4 — Entwurf ergänzen Alle Ersetzungen in `D:\Claude\.vault-tools\linkcheck\ENTWURF-codeblock-erkennung.md`. ### 4a Entscheidungsliste, neue Zeile 7 Alttext (eine Zeile): ```text | 6 | Auch Überschriften betroffen? | **Ja, gleicher Fehler** — `extract_headings` liest `# Kommentar` aus Bash-Blöcken als Überschrift. **In dieser Runde nicht geändert**, weil es den Index-Inhalt verschiebt und einen eigenen Nachweis braucht | **Befund, benannt statt stillschweigend miterledigt** | ``` Neutext (zwei Zeilen): ```text | 6 | Auch Überschriften betroffen? | **Ja, gleicher Fehler** — `extract_headings` liest `# Kommentar` aus Bash-Blöcken als Überschrift. **In dieser Runde nicht geändert**, weil es den Index-Inhalt verschiebt und einen eigenen Nachweis braucht | **Befund, benannt statt stillschweigend miterledigt** | | 7 | Welche Zeile schließt einen Zaun? | Nur eine Zeile aus demselben Zaunzeichen, mindestens so lang wie der Öffner, bis zu drei Zeichen eingerückt, danach nur Leerzeichen oder Tab. Eine Zeile mit Info-String (drei Backticks und `bash`) im Block ist Inhalt | **Quelle** — Fundstellen im Abschnitt „Nachbesserung 2026-09-13" | ``` ### 4b Angriffsliste, sechs neue Zeilen Alttext (eine Zeile): ```text | 16 | escaptes Pipe ohne Alias | Ziel ohne Backslash | ``` Neutext (sieben Zeilen): ```text | 16 | escaptes Pipe ohne Alias | Ziel ohne Backslash | | 17 | Dreier-Zaun mit Info-String innerhalb eines Vierer-Zauns, darin `[[:space:]]` | kein Link aus dem Block, Link danach bleibt | | 18 | Dreier-Schließzaun in einem Vierer-Zaun | schließt nicht | | 19 | Vierer-Zaun schließt Vierer-Zaun | schließt, Link danach bleibt | | 20 | Zeile mit drei Backticks und `bash` in einem Dreier-Block | schließt nicht | | 21 | Schließzaun mit Leerzeichen und Tab dahinter | schließt — neuer Ausdruck, eigener Fall | | 22 | Schließzaun mit CR am Zeilenende | schließt — neuer Ausdruck, eigener Fall | ``` ### 4c Neuer Abschnitt vor „Absturzfestigkeit" Alttext (eine Zeile): ```text ## Absturzfestigkeit ``` Neutext: ````text ## Nachbesserung 2026-09-13: Schließzaun nach CommonMark **Anlass.** Der Linkcheck meldete am 2026-09-13 einen toten Wikilink `[[:space:]]` in `Code Projekte/active - Logging/docs/plans/2026-09-08-a-betriebsmodelle/task-09-gate.md`. Die Datei öffnet in Zeile 356 einen Block mit vier Backticks und zeigt darin ab Zeile 406 ein eingerücktes Beispiel mit drei Backticks und `bash`. Die alte Regel schloss den äußeren Block schon an Zeile 406, weil jede Zaunzeile aus demselben Zeichen genügte. Zeile 407 mit der POSIX-Zeichenklasse lag damit im Fließtext. **Regel.** Ein Block schließt nur mit einer Zeile, die - aus demselben Zaunzeichen besteht wie der Öffner (Backtick oder Tilde), - mindestens so viele Zeichen hat wie der Öffner, - nach den Zaunzeichen nur Leerzeichen oder Tab trägt, also keinen Info-String. Die Einrückung von bis zu drei Zeichen übernimmt der Schließzaun vom Öffner (`\s{0,3}`). Ein CR am Zeilenende ist zugelassen, damit eine Eingabe mit CRLF nicht an jedem Schließzaun vorbeiläuft. **Herkunft.** | Aussage | Fundstelle | |---|---| | dasselbe Zeichen | markdown-it-py 4.2.0, `markdown_it/rules_block/fence.py` Zeile 106 | | mindestens so lang | ebenda Zeilen 42–43, `closing_len >= opening_len` | | danach nur Leerraum | ebenda Zeilen 119–123; Leerraum heißt Leerzeichen oder Tab laut `markdown_it/common/utils.py` Zeilen 167–169 | Gelesen am 2026-09-13 unter `Code Projekte/active - Agentic Cockpit/.venv/Lib/site-packages/markdown_it/`. Die Bibliothek nennt ihre Regel selbst CommonMark-konform (`fence.py` Zeile 145). Referenz der Regel ist die CommonMark-Spezifikation 0.31.2, Abschnitt 4.5 Fenced code blocks, https://spec.commonmark.org/0.31.2/#fenced-code-blocks. **Den Wortlaut der Spezifikation hat beim Planen niemand gezogen, der Planlauf hatte keinen Netzzugriff. Belegt ist die Regel an der Implementierung. Der Abgleich mit dem Spezifikationstext ist offen.** **Benannte Lücke.** Ein Öffner mit Backtick im Info-String ist nach markdown-it-py kein Zaun (`fence.py` Zeilen 79–80). Hier gilt er weiter als Zaun. Diese Runde ändert nur den Schließzaun. Die Öffnerregel verschiebt, welche Zeilen überhaupt als Code gelten, und braucht einen eigenen Lauf über den Vault. **Was schiefgehen kann.** Hat bisher eine Zeile mit Info-String einen Block versehentlich geschlossen, läuft der Block jetzt weiter, notfalls bis zum Dateiende. Wikilinks dahinter prüft der Linkcheck dann nicht mehr. Deshalb gehört zur Abnahme ein Vergleichslauf alt gegen neu über den ganzen Vault. ## Absturzfestigkeit ```` ### 4d Abbruchbedingung ohne gepflegte Zahl Alttext: ```text **und** die vierzehn Angriffsfälle grün sind. ``` Neutext: ```text **und** alle Angriffsfälle aus `faelle.py` grün sind. ``` ## Verifikation ```powershell Set-Location D:\Claude $env:PYTHONIOENCODING = "utf-8" python -m py_compile .vault-tools\linkcheck\linkcheck.py .vault-tools\linkcheck\faelle.py; "exit=$LASTEXITCODE" python .vault-tools\linkcheck\faelle.py; "exit=$LASTEXITCODE" python .vault-tools\linkcheck\linkcheck.py --vault . --alle; "exit=$LASTEXITCODE" python .vault-tools\regelwerk-check\regelwerk_check.py --vault .; "exit=$LASTEXITCODE" ``` Erwartet: 1. `py_compile`: keine Ausgabe, `exit=0`. 2. `faelle.py`: keine Zeile mit `FEHLER` oder `ABSTURZ`, Schlusszeile `N von N Faellen bestanden.` mit derselben Zahl vorn und hinten, `exit=0`. 3. `linkcheck.py`: In der Liste der toten Wikilinks steht keine Zeile mehr mit `task-09-gate.md -> [[:space:]]`. Die übrigen Befunde vor und nach der Änderung melden (Liste wörtlich in den Bericht übernehmen). `exit` ist 1, solange andere tote Links bleiben, sonst 0. 4. `regelwerk_check.py`: dieselben Befunde wie vorher. Beim Planen war das genau einer, `[klein] AI/Regelwerk/pruefwerkzeuge.md — status: pending-review — Freigabe steht aus`, und `exit=1`. Eine zusätzliche Befundzeile ist ein Fehler dieses Tasks und wird gemeldet. ## Bericht an den Koordinator - Ausgabe von Schritt 2 wörtlich (die drei FEHLER-Zeilen und die Schlusszeile). - Ausgabe der vier Verifikationsbefehle wörtlich. - Jede Stelle, an der ein Alttext nicht genau einmal gefunden wurde. In diesem Fall die Ersetzung nicht vornehmen, sondern melden. ## Nachtrag Runde 2 Nachbesserung nach der ersten Prüfrunde (Befunde 5 und 6, Details in `ENTWURF-codeblock-erkennung.md`, Abschnitt „Nachbesserung Runde 2"). Weicht vom Plantext oben ab: - **`_SCHLUSS_RE`** in `linkcheck.py` verlangt jetzt `^[ ]{0,3}` statt `^\s{0,3}` vor dem Zaunzeichen — nur Leerzeichen, kein Tab. Ein Tab vor dem Schließzaun zählt bei markdown-it-py als vier Spalten Einrückung und macht die Zeile zu eingerücktem Code, kein Zaun mehr (`state_block.py` Zeilen 257–261). Die Bedingung „gleiches Zaunzeichen" (`s.group(1)[0] == zaun[0]`) war schon vorher im Code, hatte aber keinen Fall. - **`faelle.py`** bekam drei weitere Fälle (23–25): anderes Zaunzeichen schließt nicht, Schließzaun mit vier Leerzeichen davor schließt nicht, Schließzaun mit Tab davor schließt nicht. - Details, Herkunft und Mutationsbelege stehen in `ENTWURF-codeblock-erkennung.md`, nicht hier verdoppelt.