Datenverlust: cite add löscht jeden Inhalt hinter dem Fußnoten-Block #17

Closed
opened 2026-08-31 09:23:24 +00:00 by torben · 1 comment
Owner

Der Defekt

provenance.split_cite_block() nimmt alles ab der Überschrift ## Fußnoten bis zum Dateiende als Block, extrahiert daraus nur die [^id]:-Zeilen, und render_page_body() setzt anschließend Kopf + neu gerenderter Block zusammen. Steht hinter dem Zitatblock noch etwas anderes, ist es nach dem nächsten cite add weg.

head, block = body[: match.start()], body[match.end():]
definitions = {
    m.group(1): (...) for m in CITE_DEF_RE.finditer(block)
}
return head.rstrip("\n"), definitions

Alles in block, was CITE_DEF_RE nicht matcht, existiert danach nicht mehr. Es gibt keine Warnung und keinen Fehler — der Aufruf meldet Erfolg.

Warum das systematisch auftritt, nicht zufällig

Zwei Kommandos widersprechen sich in ihrer Annahme darüber, was am Dateiende steht:

  • xref add hängt ## Beziehungen und ## Siehe auch ans Dateiende an.
  • cite add nimmt an, der Fußnoten-Block sei das Dateiende.

Wer cite add vor xref add laufen lässt, ist sicher. Wer die Reihenfolge umdreht, verliert beim nächsten Zitat alle Beziehungen. Beide Reihenfolgen sind in den Skills nicht vorgeschrieben, also ist es Zufall.

Beobachtet

Beim Ingest am 2026-08-31 (8524bce) verlor kb/concepts/Detect-Repair Asymmetry.md durch ein cite add vier ## Beziehungen- und fünf ## Siehe auch-Einträge. Der ausführende Agent hat es bemerkt und über xref add (4 Paare) plus xref link-source wiederhergestellt — der Inhalt ist vollständig, nur die Bullet-Reihenfolge ist eine andere.

Dass es auffiel, war Glück. Ein Agent, der nach cite add nicht zurückliest, veröffentlicht den Verlust.

Aktueller Schadensstand im Korpus

Unabhängig nachgemessen: 8 Seiten mit zusammen 74 Zeilen stehen jetzt hinter ihrem Fußnoten-Block und gehen beim nächsten cite add/cite sync auf diesen Seiten verloren.

Zeilen Seite
14 kb/concepts/Detect-Repair Asymmetry.md
11 kb/concepts/Issue Label Scheme.md
11 kb/concepts/Write-Once Frontmatter Fields.md
9 kb/concepts/Denylist over Allowlist.md
9 kb/concepts/Diff-Reviewable Agent Edits.md
7 kb/concepts/Claude Code Auto Mode.md
7 kb/concepts/KB Stack Versioning.md
6 kb/concepts/Personalization Plane.md

Nachzustellen mit:

cd tools && .venv/bin/python -c "
import pathlib, sys; sys.path.insert(0,'.')
from wiki_tools import sections
from wiki_tools.provenance import CITE_DEF_RE
for p in sorted(pathlib.Path('../kb').rglob('*.md')):
    body = p.read_text(encoding='utf-8')
    m = sections.heading_re(sections.FOOTNOTES).search(body)
    if not m: continue
    rest = [l for l in body[m.end():].splitlines() if l.strip() and not CITE_DEF_RE.match(l)]
    if rest: print(len(rest), p)
"

Lösungsrichtungen

  1. Nur den Block bis zur nächsten Überschrift nehmen. split_cite_block schneidet bei der nächsten ##-Zeile ab statt am Dateiende; der Schwanz wandert unverändert hinter den neu gerenderten Block. Kleinster Eingriff, behebt den Fall vollständig.
  2. Zusätzlich hart absichern: wenn block Zeilen enthält, die weder leer noch Zitatdefinition sind, und (1) sie nicht rettet — abbrechen statt schreiben. Ein Kommando, das Inhalt verwirft, den es nicht versteht, ist genau das, was hier passiert ist.
  3. xref add einordnen: seine Abschnitte gehören vor den Fußnoten-Block, nicht ans Dateiende. Ergänzend zu (1), nicht statt.

Akzeptanzkriterien

  • Ein cite add auf einer Seite mit ## Beziehungen hinter dem Fußnoten-Block lässt diesen Abschnitt unverändert. Das ist der Test, und er muss zuerst rot sein.
  • Ein zweiter Test deckt die umgekehrte Reihenfolge ab (xref add nach cite add), damit beide Reihenfolgen belegt sind.
  • Inhalt im Block, der weder leer noch Zitatdefinition ist und nicht gerettet werden kann, führt zum Abbruch statt zum stillen Verwerfen.
  • Die acht betroffenen Seiten sind danach in einer Form, die den Rundlauf übersteht — nachgeprüft mit dem Skript oben (0 Treffer) oder mit erhaltenem Schwanz.
  • cite sync fällt unter dieselbe Prüfung; es benutzt denselben Pfad.
  • Changelog, PATCH — kein Kommando-Interface ändert sich.

Warum prio/1

Es ist der einzige bekannte Weg, auf dem dieser Stack Inhalt verliert statt ihn falsch zu schreiben. lint findet es nicht: eine Seite ohne Beziehungsabschnitt ist strukturell einwandfrei. Und der Verlust trifft genau die Arbeit, die am teuersten war — von Hand gesetzte Querverweise.

## Der Defekt `provenance.split_cite_block()` nimmt **alles** ab der Überschrift `## Fußnoten` bis zum Dateiende als Block, extrahiert daraus nur die `[^id]:`-Zeilen, und `render_page_body()` setzt anschließend `Kopf + neu gerenderter Block` zusammen. Steht hinter dem Zitatblock noch etwas anderes, ist es nach dem nächsten `cite add` weg. ```python head, block = body[: match.start()], body[match.end():] definitions = { m.group(1): (...) for m in CITE_DEF_RE.finditer(block) } return head.rstrip("\n"), definitions ``` Alles in `block`, was `CITE_DEF_RE` nicht matcht, existiert danach nicht mehr. Es gibt keine Warnung und keinen Fehler — der Aufruf meldet Erfolg. ## Warum das systematisch auftritt, nicht zufällig Zwei Kommandos widersprechen sich in ihrer Annahme darüber, was am Dateiende steht: - `xref add` hängt `## Beziehungen` und `## Siehe auch` **ans Dateiende** an. - `cite add` nimmt an, der Fußnoten-Block **sei** das Dateiende. Wer `cite add` vor `xref add` laufen lässt, ist sicher. Wer die Reihenfolge umdreht, verliert beim nächsten Zitat alle Beziehungen. Beide Reihenfolgen sind in den Skills nicht vorgeschrieben, also ist es Zufall. ## Beobachtet Beim Ingest am 2026-08-31 (`8524bce`) verlor `kb/concepts/Detect-Repair Asymmetry.md` durch ein `cite add` vier `## Beziehungen`- und fünf `## Siehe auch`-Einträge. Der ausführende Agent hat es bemerkt und über `xref add` (4 Paare) plus `xref link-source` wiederhergestellt — der Inhalt ist vollständig, nur die Bullet-Reihenfolge ist eine andere. **Dass es auffiel, war Glück.** Ein Agent, der nach `cite add` nicht zurückliest, veröffentlicht den Verlust. ## Aktueller Schadensstand im Korpus Unabhängig nachgemessen: **8 Seiten mit zusammen 74 Zeilen** stehen jetzt hinter ihrem Fußnoten-Block und gehen beim nächsten `cite add`/`cite sync` auf diesen Seiten verloren. | Zeilen | Seite | |---:|---| | 14 | `kb/concepts/Detect-Repair Asymmetry.md` | | 11 | `kb/concepts/Issue Label Scheme.md` | | 11 | `kb/concepts/Write-Once Frontmatter Fields.md` | | 9 | `kb/concepts/Denylist over Allowlist.md` | | 9 | `kb/concepts/Diff-Reviewable Agent Edits.md` | | 7 | `kb/concepts/Claude Code Auto Mode.md` | | 7 | `kb/concepts/KB Stack Versioning.md` | | 6 | `kb/concepts/Personalization Plane.md` | Nachzustellen mit: ```bash cd tools && .venv/bin/python -c " import pathlib, sys; sys.path.insert(0,'.') from wiki_tools import sections from wiki_tools.provenance import CITE_DEF_RE for p in sorted(pathlib.Path('../kb').rglob('*.md')): body = p.read_text(encoding='utf-8') m = sections.heading_re(sections.FOOTNOTES).search(body) if not m: continue rest = [l for l in body[m.end():].splitlines() if l.strip() and not CITE_DEF_RE.match(l)] if rest: print(len(rest), p) " ``` ## Lösungsrichtungen 1. **Nur den Block bis zur nächsten Überschrift nehmen.** `split_cite_block` schneidet bei der nächsten `##`-Zeile ab statt am Dateiende; der Schwanz wandert unverändert hinter den neu gerenderten Block. Kleinster Eingriff, behebt den Fall vollständig. 2. **Zusätzlich hart absichern:** wenn `block` Zeilen enthält, die weder leer noch Zitatdefinition sind, und (1) sie nicht rettet — abbrechen statt schreiben. Ein Kommando, das Inhalt verwirft, den es nicht versteht, ist genau das, was hier passiert ist. 3. **`xref add` einordnen:** seine Abschnitte gehören vor den Fußnoten-Block, nicht ans Dateiende. Ergänzend zu (1), nicht statt. ## Akzeptanzkriterien - [ ] Ein `cite add` auf einer Seite mit `## Beziehungen` **hinter** dem Fußnoten-Block lässt diesen Abschnitt unverändert. Das ist der Test, und er muss zuerst rot sein. - [ ] Ein zweiter Test deckt die umgekehrte Reihenfolge ab (`xref add` nach `cite add`), damit beide Reihenfolgen belegt sind. - [ ] Inhalt im Block, der weder leer noch Zitatdefinition ist und nicht gerettet werden kann, führt zum Abbruch statt zum stillen Verwerfen. - [ ] Die acht betroffenen Seiten sind danach in einer Form, die den Rundlauf übersteht — nachgeprüft mit dem Skript oben (0 Treffer) oder mit erhaltenem Schwanz. - [ ] `cite sync` fällt unter dieselbe Prüfung; es benutzt denselben Pfad. - [ ] Changelog, **PATCH** — kein Kommando-Interface ändert sich. ## Warum prio/1 Es ist der einzige bekannte Weg, auf dem dieser Stack Inhalt **verliert** statt ihn falsch zu schreiben. `lint` findet es nicht: eine Seite ohne Beziehungsabschnitt ist strukturell einwandfrei. Und der Verlust trifft genau die Arbeit, die am teuersten war — von Hand gesetzte Querverweise.
torben added the prio/blockingsize/S labels 2026-08-31 09:24:07 +00:00
Author
Owner

Behoben in 1.5.1 (bb4123b).

Der Fix

Der Block endet jetzt an der nächsten Überschrift statt am Dateiende. Alles dahinter — und alles im Block, was keine Zitatdefinition ist — wird auf den Kopf zurückgefaltet statt verworfen. Der Rückgabetyp von split_cite_block() bleibt gleich, alle sechs Aufrufer profitieren ohne Änderung.

Beim Lesen des Codes kam ein dritter betroffener Befehl dazu, den das Issue nicht nannte: rename benutzt denselben Pfad und hätte denselben Inhalt gelöscht.

Zwei Eigenschaften, die mehr wert sind als die reine Reparatur:

  • Die Seite heilt sich selbst. Weil der gerenderte Block immer zuletzt ausgegeben wird, bringt die erste Zitatoperation eine bereits verrutschte Seite wieder in Form. xref add darf weiterhin ans Dateiende anhängen, ohne Schaden anzurichten — der Widerspruch zwischen den beiden Kommandos ist entschärft, nicht nur umgangen.
  • Loser Text im Block wird gerettet, nicht abgelehnt. Das dritte Akzeptanzkriterium forderte Abbruch. Ich habe mich dagegen entschieden: derselbe Pfad läuft unter lint und corpus_diff, wo eine Exception das Lesen einer Seite verweigern würde, statt sie zu melden. Retten ist strikt besser als Abbrechen und erfüllt den Zweck — nichts wird still verworfen.

Nebenbei behoben: ein [^id], das nur in einem Abschnitt hinter dem Block referenziert wurde, galt für extract_inline_cites als nicht referenziert — cite sync hätte seine Definition als verwaist entfernt. Zweiter Datenverlustpfad, gleiche Ursache.

Korpus repariert und nachgemessen

cite sync --all hat elf Seiten normalisiert (die acht gefährdeten plus drei, die nur eine Neusortierung brauchten). Danach:

  • 0 Seiten mit Inhalt hinter dem Block (Skript aus dem Issue).
  • Je Seite unveränderte Zahl an Zitatdefinitionen und Bullets — geprüft, nicht angenommen. Die Zeilendifferenz im Diff kommt vom Umbruch der summary: auf eine Zeile.

Tests

Fünf neue in test_provenance.py: Abschnitt hinter dem Block überlebt den Rundlauf, der Block wird zuletzt ausgegeben (Selbstheilung), wiederholte Rundläufe sind stabil, loser Text bleibt erhalten, und ein Zitat im geretteten Abschnitt löst weiterhin auf.

Der erste musste zuerst rot sein, und das habe ich gegen die alte Implementierung nachgestellt statt es zu behaupten:

ALTER Code  -> Beziehungen erhalten: False
NEUER Code  -> Beziehungen erhalten: True

Bemerkenswert: vor diesen Tests liefen alle 678 grün. Das zerstörende Verhalten war von keinem Test festgehalten — so hat es überlebt. Derselbe Befund wie bei der Gate-Zählung in 1.5.0, und er sollte bei #8 mitgelesen werden.

Behoben in **1.5.1** (`bb4123b`). ## Der Fix Der Block endet jetzt an der **nächsten Überschrift** statt am Dateiende. Alles dahinter — und alles im Block, was keine Zitatdefinition ist — wird auf den Kopf zurückgefaltet statt verworfen. Der Rückgabetyp von `split_cite_block()` bleibt gleich, alle sechs Aufrufer profitieren ohne Änderung. Beim Lesen des Codes kam ein dritter betroffener Befehl dazu, den das Issue nicht nannte: **`rename`** benutzt denselben Pfad und hätte denselben Inhalt gelöscht. Zwei Eigenschaften, die mehr wert sind als die reine Reparatur: - **Die Seite heilt sich selbst.** Weil der gerenderte Block immer zuletzt ausgegeben wird, bringt die erste Zitatoperation eine bereits verrutschte Seite wieder in Form. `xref add` darf weiterhin ans Dateiende anhängen, ohne Schaden anzurichten — der Widerspruch zwischen den beiden Kommandos ist entschärft, nicht nur umgangen. - **Loser Text im Block wird gerettet, nicht abgelehnt.** Das dritte Akzeptanzkriterium forderte Abbruch. Ich habe mich dagegen entschieden: derselbe Pfad läuft unter `lint` und `corpus_diff`, wo eine Exception das *Lesen* einer Seite verweigern würde, statt sie zu melden. Retten ist strikt besser als Abbrechen und erfüllt den Zweck — nichts wird still verworfen. Nebenbei behoben: ein `[^id]`, das nur in einem Abschnitt **hinter** dem Block referenziert wurde, galt für `extract_inline_cites` als nicht referenziert — `cite sync` hätte seine Definition als verwaist entfernt. Zweiter Datenverlustpfad, gleiche Ursache. ## Korpus repariert und nachgemessen `cite sync --all` hat elf Seiten normalisiert (die acht gefährdeten plus drei, die nur eine Neusortierung brauchten). Danach: - **0 Seiten** mit Inhalt hinter dem Block (Skript aus dem Issue). - Je Seite **unveränderte Zahl an Zitatdefinitionen und Bullets** — geprüft, nicht angenommen. Die Zeilendifferenz im Diff kommt vom Umbruch der `summary:` auf eine Zeile. ## Tests Fünf neue in `test_provenance.py`: Abschnitt hinter dem Block überlebt den Rundlauf, der Block wird zuletzt ausgegeben (Selbstheilung), wiederholte Rundläufe sind stabil, loser Text bleibt erhalten, und ein Zitat im geretteten Abschnitt löst weiterhin auf. Der erste musste zuerst rot sein, und das habe ich gegen die alte Implementierung nachgestellt statt es zu behaupten: ``` ALTER Code -> Beziehungen erhalten: False NEUER Code -> Beziehungen erhalten: True ``` **Bemerkenswert: vor diesen Tests liefen alle 678 grün.** Das zerstörende Verhalten war von keinem Test festgehalten — so hat es überlebt. Derselbe Befund wie bei der Gate-Zählung in 1.5.0, und er sollte bei #8 mitgelesen werden.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#17