raw derive (Mail): ein Platzhalter-text/plain verdrängt den HTML-Teil, derived.md bleibt fast leer #204

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

Gefunden beim Lesen der Ableitungen in #203 (2026-10-09). Dort bewusst nicht gefixt: Echte Fehler einer Ableitung werden eigene Issues.

Stand

Erledigt am 2026-10-09. Gebaut und veröffentlicht als 9.0.0-beta.20 (Commit 918157c). Geprüft durch pytest (2714 bestanden, 3 übersprungen), docs verify, instructions verify und CI auf diesem Commit (Runs 632 und 633, beide success).

Abschlussprüfung: Keine Doku ist veraltet. raw/CONTRACT.md (Abschnitt Container), docs/how-to/ingest-a-source.md und tools/README.md sagen nichts darüber, wie der Mailtext gewählt wird. Die generierte Referenz docs/reference/commands/raw-material-and-uploads.md nennt die neue Regel bereits.

Was passierte (vor 9.0.0-beta.20)

testfiles/mail/gog-roundcube.eml ist multipart/alternative mit zwei Teilen:

Teil Länge Inhalt
text/plain 32 Zeichen Plain text version not available
text/html 40 204 Zeichen (9 682 nach web_capture.derive_text) der eigentliche Newsletter

tools/chemenu/filters/mail.py wählte den Textteil mit msg.get_body(preferencelist=("plain", "html")). text/plain gewann immer, wenn es existierte. Deshalb bestand derived.md nur aus dem Kopf und diesem einen Satz. Ein Agent, der die Quelle ingestiert, las also eine leere Mail.

Dieselbe Mail als testfiles/mail/gog-outlook.msg bekam den HTML-Text, weil die .msg keinen Plaintext-Stream hat (__substg1.0_1000001F fehlt), nur HTML (10130102). Je nach Mailprogramm entstand aus demselben Newsletter also eine volle oder eine leere Quelle. tools/chemenu/filters/msg.py _body() hatte dieselbe Regel „Plaintext, sonst HTML“.

test_raw_derive_testfiles.py blieb dabei grün: Der Test prüft, ob die Ableitung gelingt und stabil ist, nicht, ob sie Inhalt hat. Dafür gibt es jetzt die Sidecar-Datei (siehe Kriterien).

Zweiter Befund: die nicht gewählte Alternative wurde ein Anhang

parse() übersprang nur den gewählten body_part. Jeder andere Blattteil wurde ein Attachment, auch die zweite Fassung desselben Texts aus multipart/alternative. Das traf alle drei Roundcube-Mails: Jede hatte attachments: ['attachment-2'] (text/html) im Kopf, und im Container-Modus lag attachment-2 als Datei im Ordner.

Messung über testfiles/mail/ (2026-10-09)

Datei Plaintext HTML-Text Verhältnis
github-roundcube.eml 8 438 9 457 0,892
gog-roundcube.eml 32 9 682 0,003
tldr-roundcube.eml 9 807 16 232 0,604
*-outlook.msg (alle drei) – 9 457 / 9 682 / 16 232 kein Plaintext

Gemessen wurde die Länge von Plaintext und derive_text-Ergebnis. Beim Bauen ergaben sich für den gestrippten Plaintext der beiden Roundcube-Mails 8 353 und 9 589 Zeichen (Verhältnis 0,883 und 0,591); die Aussage bleibt: Ein guter Plaintext liegt bei 60–90 %, der Platzhalter bei 0,3 %.

Entscheidungen (2026-10-09, vom Betreiber bestätigt, umgesetzt)

A: Plaintext, außer er ist kürzer als 10 % des HTML-Texts

Die Regel, für .eml und .msg gleich:

  • plain = Plaintext mit \r\n/\r → \n, gestrippt. html_text = web_capture.derive_text(...), gestrippt.
  • Kein plain → html_text. Kein html_text → plain. Keines von beiden → die früheren Warnungen bleiben unverändert.
  • Beide da und len(plain) < 0.10 * len(html_text) → html_text, und ein warnings-Eintrag nennt beide Längen: „The plain text (32 characters) is under 10% of the HTML text (9682 characters); the text is taken from the HTML part.“
  • Sonst → plain, ohne Warnung.

Verworfen: Immer HTML (ändert den Text jeder Mail mit gutem Plaintext ohne Fehler), Platzhaltersätze erkennen (eine Liste in fremden Sprachen, die nie vollständig wird).

Umsetzung: mail.choose_text(plain, html_text) liefert Text, Herkunft (HTML ja/nein) und Warnung; die Schwelle ist die Konstante mail.PLAIN_MIN_SHARE (0,10) und steht nur dort. msg._body() ruft dieselbe Funktion. parse() ermittelt Plaintext- und HTML-Teil getrennt über get_body(preferencelist=("plain",)) und ("html",).

B: Die anderen Fassungen des Mailtexts sind keine Anhänge

Ein text/plain- oder text/html-Blattteil unter dem nächsten multipart/alternative, das den gewählten Textteil enthält, wird kein Attachment. Das gilt auch tiefer verschachtelt, etwa alternative[plain, related[html, img]]. Erhalten bleiben:

  • Ein Teil mit Content-Disposition: attachment bleibt ein Anhang.
  • Inline-Bilder (Content-ID, keine Attachment-Disposition) werden weiter gezählt und gemeldet.
  • Eine zweite Textfassung, die nicht unter demselben multipart/alternative liegt (etwa mixed[plain, html]), bleibt ein Anhang, wie vorher.

Bei .msg gibt es diesen Fall nicht: Plaintext und HTML sind Streams der Nachricht, keine Anhang-Storages.

Akzeptanzkriterien

  • raw derive auf testfiles/mail/gog-roundcube.eml liefert in derived.md den Newslettertext und die Warnung aus A. Belegt durch testfiles/mail/gog-roundcube.eml.expect (Silent Hill: Townfall, JETZT KAUFEN; beide stehen nur im HTML-Text). attachments ist [], im Ordner liegt keine attachment-*-Datei.
  • github-roundcube.eml und tldr-roundcube.eml: Der Text ist vorher und nachher byteweise gleich (verglichen mit einem Abzug vor der Änderung). attachments ist jetzt [], kein attachment-2 mehr.
  • Die drei *-outlook.msg: Text vorher und nachher byteweise gleich, attachments und Warnungen unverändert.
  • mail.py und msg.py wenden die Regel aus derselben Funktion an; die Schwelle steht an genau einer Stelle.
  • Unit-Tests für .eml in tools/chemenu/tests/test_raw_derive.py: Platzhalter + HTML ergibt HTML-Text und Warnung; guter Plaintext + HTML ergibt Plaintext ohne Warnung mit attachments == []; Plaintext + HTML + echter Anhang ergibt genau diesen Anhang. Dazu drei Fälle für B: HTML in related mit Inline-Bild, Textfassung mit Content-Disposition: attachment bleibt Anhang, mixed ohne alternative bleibt Anhang. make_eml hat dafür den neuen Parameter alternative=; html= verhält sich unverändert.
  • Unit-Tests für .msg in tools/chemenu/tests/test_raw_derive_binary.py: Platzhalter-Plaintext ergibt HTML-Text und Warnung; guter Plaintext ergibt Plaintext ohne Warnung.
  • Der Docstring von msg.py und die Formatbeschreibung von raw derive (raw_cmd.py) nennen die Regel; tools/CONTRACT.md und docs/reference/commands/raw-material-and-uploads.md sind neu gerendert. Die Pull-through-Tabelle ergab keine weitere Stelle.
  • Die bestehende Suite bleibt grün: pytest 2714 bestanden, 3 übersprungen; docs verify und instructions verify grün.
  • CI auf dem veröffentlichten Commit grün: Runs 632 und 633 (Commit 918157c), beide success.

Version

--patch, umgesetzt als 9.0.0-beta.20. Das ist eine Verhaltensänderung eines Filters ohne Schemaänderung. Drop-in-Test vorwärts: Kopieren von tools/ genügt, nichts zu migrieren. Rückwärts: Die alte Version liest jede damit erzeugte Quelle, denn derived.md hat dieselbe Form. Bereits angenommene Quellen unter raw/ bleiben unberührt, denn raw derive wirkt nur beim Eingang. Der laufende Kandidat steht auf MAJOR, also erhöhte --patch nur die Bump-Zählung.

Beteiligte Dateien

  • tools/chemenu/filters/mail.py: choose_text, PLAIN_MIN_SHARE, _other_versions, parse().
  • tools/chemenu/filters/msg.py: _body() und Modul-Docstring.
  • tools/chemenu/commands/raw_cmd.py: Formatbeschreibung.
  • tools/chemenu/tests/test_raw_derive.py, tools/chemenu/tests/test_raw_derive_binary.py: make_eml(alternative=) und neue Tests.
  • testfiles/mail/gog-roundcube.eml.expect: neu.
  • CHANGES.md, VERSION: über den Bump.

Bezug

  • #203: dort gefunden (Abschnitt „Abweichung im Build“, Beobachtung)
  • #199: raw derive mit den Mailfiltern
> Gefunden beim Lesen der Ableitungen in #203 (2026-10-09). Dort bewusst nicht gefixt: Echte Fehler einer Ableitung werden eigene Issues. ## Stand Erledigt am 2026-10-09. Gebaut und veröffentlicht als `9.0.0-beta.20` (Commit `918157c`). Geprüft durch `pytest` (2714 bestanden, 3 übersprungen), `docs verify`, `instructions verify` und CI auf diesem Commit (Runs 632 und 633, beide `success`). Abschlussprüfung: Keine Doku ist veraltet. `raw/CONTRACT.md` (Abschnitt Container), `docs/how-to/ingest-a-source.md` und `tools/README.md` sagen nichts darüber, wie der Mailtext gewählt wird. Die generierte Referenz `docs/reference/commands/raw-material-and-uploads.md` nennt die neue Regel bereits. ## Was passierte (vor `9.0.0-beta.20`) `testfiles/mail/gog-roundcube.eml` ist `multipart/alternative` mit zwei Teilen: | Teil | Länge | Inhalt | |---|---|---| | `text/plain` | 32 Zeichen | `Plain text version not available` | | `text/html` | 40 204 Zeichen (9 682 nach `web_capture.derive_text`) | der eigentliche Newsletter | `tools/chemenu/filters/mail.py` wählte den Textteil mit `msg.get_body(preferencelist=("plain", "html"))`. `text/plain` gewann immer, wenn es existierte. Deshalb bestand `derived.md` nur aus dem Kopf und diesem einen Satz. Ein Agent, der die Quelle ingestiert, las also eine leere Mail. Dieselbe Mail als `testfiles/mail/gog-outlook.msg` bekam den HTML-Text, weil die `.msg` keinen Plaintext-Stream hat (`__substg1.0_1000001F` fehlt), nur HTML (`10130102`). Je nach Mailprogramm entstand aus demselben Newsletter also eine volle oder eine leere Quelle. `tools/chemenu/filters/msg.py` `_body()` hatte dieselbe Regel „Plaintext, sonst HTML“. `test_raw_derive_testfiles.py` blieb dabei grün: Der Test prüft, ob die Ableitung gelingt und stabil ist, nicht, ob sie Inhalt hat. Dafür gibt es jetzt die Sidecar-Datei (siehe Kriterien). ### Zweiter Befund: die nicht gewählte Alternative wurde ein Anhang `parse()` übersprang nur den gewählten `body_part`. Jeder andere Blattteil wurde ein `Attachment`, auch die zweite Fassung desselben Texts aus `multipart/alternative`. Das traf alle drei Roundcube-Mails: Jede hatte `attachments: ['attachment-2']` (`text/html`) im Kopf, und im Container-Modus lag `attachment-2` als Datei im Ordner. ### Messung über `testfiles/mail/` (2026-10-09) | Datei | Plaintext | HTML-Text | Verhältnis | |---|---|---|---| | `github-roundcube.eml` | 8 438 | 9 457 | 0,892 | | `gog-roundcube.eml` | 32 | 9 682 | 0,003 | | `tldr-roundcube.eml` | 9 807 | 16 232 | 0,604 | | `*-outlook.msg` (alle drei) | – | 9 457 / 9 682 / 16 232 | kein Plaintext | Gemessen wurde die Länge von Plaintext und `derive_text`-Ergebnis. Beim Bauen ergaben sich für den gestrippten Plaintext der beiden Roundcube-Mails 8 353 und 9 589 Zeichen (Verhältnis 0,883 und 0,591); die Aussage bleibt: Ein guter Plaintext liegt bei 60–90 %, der Platzhalter bei 0,3 %. ## Entscheidungen (2026-10-09, vom Betreiber bestätigt, umgesetzt) ### A: Plaintext, außer er ist kürzer als 10 % des HTML-Texts Die Regel, für `.eml` und `.msg` gleich: - `plain` = Plaintext mit `\r\n`/`\r` → `\n`, gestrippt. `html_text` = `web_capture.derive_text(...)`, gestrippt. - Kein `plain` → `html_text`. Kein `html_text` → `plain`. Keines von beiden → die früheren Warnungen bleiben unverändert. - Beide da und `len(plain) < 0.10 * len(html_text)` → `html_text`, und ein `warnings`-Eintrag nennt beide Längen: „The plain text (32 characters) is under 10% of the HTML text (9682 characters); the text is taken from the HTML part.“ - Sonst → `plain`, ohne Warnung. Verworfen: **Immer HTML** (ändert den Text jeder Mail mit gutem Plaintext ohne Fehler), **Platzhaltersätze erkennen** (eine Liste in fremden Sprachen, die nie vollständig wird). Umsetzung: `mail.choose_text(plain, html_text)` liefert Text, Herkunft (HTML ja/nein) und Warnung; die Schwelle ist die Konstante `mail.PLAIN_MIN_SHARE` (0,10) und steht nur dort. `msg._body()` ruft dieselbe Funktion. `parse()` ermittelt Plaintext- und HTML-Teil getrennt über `get_body(preferencelist=("plain",))` und `("html",)`. ### B: Die anderen Fassungen des Mailtexts sind keine Anhänge Ein `text/plain`- oder `text/html`-Blattteil unter dem nächsten `multipart/alternative`, das den gewählten Textteil enthält, wird kein `Attachment`. Das gilt auch tiefer verschachtelt, etwa `alternative[plain, related[html, img]]`. Erhalten bleiben: - Ein Teil mit `Content-Disposition: attachment` bleibt ein Anhang. - Inline-Bilder (`Content-ID`, keine Attachment-Disposition) werden weiter gezählt und gemeldet. - Eine zweite Textfassung, die **nicht** unter demselben `multipart/alternative` liegt (etwa `mixed[plain, html]`), bleibt ein Anhang, wie vorher. Bei `.msg` gibt es diesen Fall nicht: Plaintext und HTML sind Streams der Nachricht, keine Anhang-Storages. ## Akzeptanzkriterien - [x] `raw derive` auf `testfiles/mail/gog-roundcube.eml` liefert in `derived.md` den Newslettertext und die Warnung aus A. Belegt durch `testfiles/mail/gog-roundcube.eml.expect` (`Silent Hill: Townfall`, `JETZT KAUFEN`; beide stehen nur im HTML-Text). `attachments` ist `[]`, im Ordner liegt keine `attachment-*`-Datei. - [x] `github-roundcube.eml` und `tldr-roundcube.eml`: Der Text ist vorher und nachher byteweise gleich (verglichen mit einem Abzug vor der Änderung). `attachments` ist jetzt `[]`, kein `attachment-2` mehr. - [x] Die drei `*-outlook.msg`: Text vorher und nachher byteweise gleich, `attachments` und Warnungen unverändert. - [x] `mail.py` und `msg.py` wenden die Regel aus derselben Funktion an; die Schwelle steht an genau einer Stelle. - [x] Unit-Tests für `.eml` in `tools/chemenu/tests/test_raw_derive.py`: Platzhalter + HTML ergibt HTML-Text und Warnung; guter Plaintext + HTML ergibt Plaintext ohne Warnung mit `attachments == []`; Plaintext + HTML + echter Anhang ergibt genau diesen Anhang. Dazu drei Fälle für B: HTML in `related` mit Inline-Bild, Textfassung mit `Content-Disposition: attachment` bleibt Anhang, `mixed` ohne `alternative` bleibt Anhang. `make_eml` hat dafür den neuen Parameter `alternative=`; `html=` verhält sich unverändert. - [x] Unit-Tests für `.msg` in `tools/chemenu/tests/test_raw_derive_binary.py`: Platzhalter-Plaintext ergibt HTML-Text und Warnung; guter Plaintext ergibt Plaintext ohne Warnung. - [x] Der Docstring von `msg.py` und die Formatbeschreibung von `raw derive` (`raw_cmd.py`) nennen die Regel; `tools/CONTRACT.md` und `docs/reference/commands/raw-material-and-uploads.md` sind neu gerendert. Die Pull-through-Tabelle ergab keine weitere Stelle. - [x] Die bestehende Suite bleibt grün: `pytest` 2714 bestanden, 3 übersprungen; `docs verify` und `instructions verify` grün. - [x] CI auf dem veröffentlichten Commit grün: Runs 632 und 633 (Commit `918157c`), beide `success`. ## Version `--patch`, umgesetzt als `9.0.0-beta.20`. Das ist eine Verhaltensänderung eines Filters ohne Schemaänderung. Drop-in-Test vorwärts: Kopieren von `tools/` genügt, nichts zu migrieren. Rückwärts: Die alte Version liest jede damit erzeugte Quelle, denn `derived.md` hat dieselbe Form. Bereits angenommene Quellen unter `raw/` bleiben unberührt, denn `raw derive` wirkt nur beim Eingang. Der laufende Kandidat steht auf MAJOR, also erhöhte `--patch` nur die Bump-Zählung. ## Beteiligte Dateien - `tools/chemenu/filters/mail.py`: `choose_text`, `PLAIN_MIN_SHARE`, `_other_versions`, `parse()`. - `tools/chemenu/filters/msg.py`: `_body()` und Modul-Docstring. - `tools/chemenu/commands/raw_cmd.py`: Formatbeschreibung. - `tools/chemenu/tests/test_raw_derive.py`, `tools/chemenu/tests/test_raw_derive_binary.py`: `make_eml(alternative=)` und neue Tests. - `testfiles/mail/gog-roundcube.eml.expect`: neu. - `CHANGES.md`, `VERSION`: über den Bump. ## Bezug - #203: dort gefunden (Abschnitt „Abweichung im Build“, Beobachtung) - #199: `raw derive` mit den Mailfiltern
torben added the prio/plannedsize/Sarea/kbkind/decision labels 2026-10-09 14:21:55 +00:00
torben added kind/build and removed kind/decision labels 2026-10-09 14:36:58 +00:00
Author
Owner

Changelog: Body gegen den Baum geprüft und entschieden. Neu: Messung über testfiles/mail/, zweiter Befund (die nicht gewählte multipart/alternative-Fassung wird in allen drei Roundcube-Mails attachment-2). Entschieden vom Betreiber: A = Plaintext, außer kürzer als 10 % des HTML-Texts, dann HTML mit Warnung; B = andere Textfassungen des Mailtexts sind keine Anhänge, hier mit behoben. Korrigiert: .msg-Tests gehören nach test_raw_derive_binary.py; Ort der Formatbeschreibung (raw_cmd.py:2172) benannt; make_eml kann noch keine Alternative bauen. Akzeptanzkriterien geschärft, Version --patch begründet. Relabel kind/decision → kind/build, weil beide Entscheidungen gefallen sind.

**Changelog:** Body gegen den Baum geprüft und entschieden. Neu: Messung über `testfiles/mail/`, zweiter Befund (die nicht gewählte `multipart/alternative`-Fassung wird in allen drei Roundcube-Mails `attachment-2`). Entschieden vom Betreiber: A = Plaintext, außer kürzer als 10 % des HTML-Texts, dann HTML mit Warnung; B = andere Textfassungen des Mailtexts sind keine Anhänge, hier mit behoben. Korrigiert: `.msg`-Tests gehören nach `test_raw_derive_binary.py`; Ort der Formatbeschreibung (`raw_cmd.py:2172`) benannt; `make_eml` kann noch keine Alternative bauen. Akzeptanzkriterien geschärft, Version `--patch` begründet. Relabel `kind/decision` → `kind/build`, weil beide Entscheidungen gefallen sind.
Author
Owner

Changelog: Gebaut und veröffentlicht als 9.0.0-beta.20 (Commit 918157c), CI grün (Runs 632, 633). Alle Akzeptanzkriterien abgehakt. Neu im Body: Umsetzung von A (mail.choose_text, mail.PLAIN_MIN_SHARE) und B (_other_versions); B gilt nur unter demselben multipart/alternative, eine Textfassung in mixed bleibt Anhang wie vorher; make_eml hat den Parameter alternative=; Messwerte des gestrippten Plaintexts (8 353 / 9 589) ergänzt. Keine Abweichung von den Entscheidungen.

**Changelog:** Gebaut und veröffentlicht als `9.0.0-beta.20` (Commit `918157c`), CI grün (Runs 632, 633). Alle Akzeptanzkriterien abgehakt. Neu im Body: Umsetzung von A (`mail.choose_text`, `mail.PLAIN_MIN_SHARE`) und B (`_other_versions`); B gilt nur unter demselben `multipart/alternative`, eine Textfassung in `mixed` bleibt Anhang wie vorher; `make_eml` hat den Parameter `alternative=`; Messwerte des gestrippten Plaintexts (8 353 / 9 589) ergänzt. Keine Abweichung von den Entscheidungen.
Author
Owner

Changelog: Abgeschlossen. „Stand“ beschreibt jetzt den Endzustand mit allen Prüfungen (pytest, docs verify, instructions verify, CI-Runs 632/633). Die Abschlussprüfung fand keine veraltete Doku: raw/CONTRACT.md, docs/how-to/ingest-a-source.md und tools/README.md beschreiben die Textwahl nicht, die generierte Referenz nennt die Regel. Kein weiterer Bump, keine weitere Veröffentlichung. Die Überschrift „Was passiert“ heißt jetzt „Was passierte (vor 9.0.0-beta.20)“.

**Changelog:** Abgeschlossen. „Stand“ beschreibt jetzt den Endzustand mit allen Prüfungen (pytest, `docs verify`, `instructions verify`, CI-Runs 632/633). Die Abschlussprüfung fand keine veraltete Doku: `raw/CONTRACT.md`, `docs/how-to/ingest-a-source.md` und `tools/README.md` beschreiben die Textwahl nicht, die generierte Referenz nennt die Regel. Kein weiterer Bump, keine weitere Veröffentlichung. Die Überschrift „Was passiert“ heißt jetzt „Was passierte (vor `9.0.0-beta.20`)“.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#204