Import-Filter: raw derive leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, Formatwissen in der Werkzeugausgabe #199

Closed
opened 2026-10-07 14:18:55 +00:00 by torben · 5 comments
Owner

Ausgearbeitet aus einem status/incoming-Stub am 2026-10-08 (Stand 9.0.0-beta.13, 765ebe9). Der Stub steht wörtlich im ersten Kommentar. Alle Betreiberfragen sind am 2026-10-08 entschieden (§ Entscheidungen des Betreibers). Gebaut, publiziert, CI grün und geschlossen am 2026-10-08.

Stand (abgeschlossen)

Schnitt Inhalt Version Commit CI
1 Registry, raw derive, Ordnerform, Budget/Namen, Header, CHECK, DOCX/PPTX/XLSX/EML, Skill, raw pending/raw accept, raw/CONTRACT.md 9.0.0-beta.14 9c0bb6b grün, Läufe 617 und 618
2 Container: ZIP, Mail mit Anhängen, ZIP-in-Mail, zitierte angehängte Mail, reservierte/doppelte Namen 9.0.0-beta.15 6659ca5 grün, Läufe 619 und 620
3 PDF (pypdf), MSG (olefile), requirements.txt, doctor-Zeilen import-filter-<name> 9.0.0-beta.16 0e6a50a grün, Läufe 621 und 622
Abschluss Pull-through im Schließen: Das Beispiel „a PDF and its converted text“ als Bündel-Fall im Kommando-Record von raw pending (und in dessen Docstring) ist entfernt, samt den beiden generierten Kopien (tools/CONTRACT.md, docs/reference/commands/raw-material-and-uploads.md) 9.0.0-beta.17 8ea2795 grün, Läufe 623 und 624

Folge-Issue zu Entwurf 11: #201.

Prüfung beim Schließen (welche Dokumente nachgezogen wurden und welche nicht):

  • Gefunden und behoben: Der Kommando-Record von raw pending nannte noch „a PDF and its converted text“ als Bündel, obwohl raw/CONTRACT.md und der Skill dieses Beispiel schon ersetzt hatten und eine Ableitung seither ein Ordner ist.
  • Geprüft, ohne Änderung: die Seiten unter docs/explanation/ (keine beschreibt die Aufnahme nach Format), INSTALL.md, instructions/preflight.md (der Preflight vergleicht den sha256 von requirements.txt mit einem Stempel im venv und installiert die neuen Pakete beim nächsten Lauf nach), docs/reference/skills.md (Name und description von wiki-ingest unverändert), docs/dev/development.md. Das Paket berührt keine Installationsanleitung, die Zweitlesung gegen INSTALL.md entfällt damit.
  • Verifiziert: docs verify, instructions verify und die raw-/Contract-Tests lokal grün (310 bestanden, 14 übersprungen, weil pypdf/olefile im lokalen venv fehlen), CI grün auf 8ea2795 (Läufe 623 und 624).

Abweichungen vom Entwurf (im Bau entschieden):

  • Fixtures: PDF und MSG liegen nicht als Binärdateien unter tests/fixtures/. tests/binary_formats.py baut beide zur Testzeit (ein PDF von Hand, ein Compound File mit eigenem Schreiber), und pypdf/olefile lesen sie dann in den Tests. Nichts Binäres im Repo; die Tests, die das Paket brauchen, werden übersprungen, wo es fehlt (CI installiert requirements.txt).
  • Modulaufteilung: MSG steht in filters/msg.py (erbt von EmlFilter), nicht in mail.py, damit olefile nur beim Ableiten einer MSG importiert wird. mail.py lädt msg.py für eine angehängte .msg erst bei Bedarf.
  • Doppelte Namen: Eine .md, die mit einer echten Datei kollidiert, bekommt in einem Container -2, -3; ein Ordner ohne Container-Ableitung verweigert wie im Entwurf.
  • MSG: Eine eingebettete Nachricht in einer MSG wird nur genannt (WARN), nicht gelesen. Ein Inline-Bild (Content-ID plus image/*) wird gezählt und nicht geschrieben. Eine angehängte .msg an einer EML wird wie eine angehängte EML zitiert.
  • Message.walk() steigt in message/rfc822 ab: Eine eigene Blattiteration (_leaves) verhindert, dass Body und Anhänge einer angehängten Mail der äußeren Mail zugeschlagen werden.
  • Pakete: pypdf>=5.0, olefile>=0.47.

Befund (gegen den Baum geprüft, Stand vor dem Bau, 9.0.0-beta.13)

Die ersten beiden Punkte beschreiben die Lücke, die dieses Paket geschlossen hat; die übrigen gelten weiter.

  • Text aus einem Nicht-Text-Format leitete vor dem Bau nur raw fetch ab, und nur aus HTML (tools/chemenu/web_capture.py): Original byte-genau + <stem>.md mit festem Header, beide in incoming/, raw accept nimmt sie zusammen (raw/CONTRACT.md § Getting a URL in). Für alle anderen Dateien gab es nichts. Ein PDF legt raw fetch „as received, no text derived“ ab.
  • wiki-ingest setzte die Ableitung voraus, ohne sie zu liefern. Schritt 1 sagte zu Binärdateien nur „note its presence and what it shows“, Schritt 5 nannte als Beispiel „an uploaded PDF plus its converted Markdown“. Die Konvertierung machte also jede Session selbst, mit dem, was ihr Harness gerade konnte (Claude Code liest PDFs selbst, die anderen drei Harnesses nicht gleich). Das war dieselbe Lage, die #120 für URLs behoben hat: zwei Sessions machen aus derselben Datei zwei verschiedene raw/-Dateien, und beide bleiben unveränderlich liegen. Seit dem Bau liefert raw derive die Ableitung, und Schritt 1 verweist darauf.
  • Bundle-Mechanik: raw accept bildet einen Bundle-Ordner ab der zweiten Datei, benannt nach dem Stamm der ersten (raw_cmd.py _plan_file_moves). Aus incoming/x.pdf + incoming/x.md wird so raw/<YYYY>/<MM>/x/x.pdf + x.md. Ein Ordner in incoming/ ist eine Quelle und wird mit seiner Struktur übernommen; raw accept verweigert darin versteckte Einträge, Symlinks, Spezialdateien und _capture.json außerhalb eines erfassten Bundles.
  • Pfadbudget: 160 Zeichen relativ zur Instanz (titles.PATH_BUDGET), davon belegt raw/2026/10/ schon 12. Eine Einzeldatei darf etwa 144 Zeichen Namen haben; im Bundle <stem>/<stem>.pdf steht der Stamm doppelt, dann bleiben nur etwa 71. raw fetch hält das nur ein, weil es Stämme auf 60 Zeichen kürzt. Ein vom Menschen benannter Dateiname scheitert sonst erst bei raw accept. (Das ist der Grund für die Ordnerform mit original.<ext>, Entscheidung 7.)
  • Neue Ausgabe eines Ordner-Bundles: --replaces-bundle gibt es nur für erfasste Bundles (_capture.json). Für einen gewöhnlichen Ordner sagt die Verweigerung „replaced file by file“ (raw_cmd.py _folder_collision_message), und --replaces nimmt nur eine Datei direkt in incoming/. Eine neue Ausgabe oder eine Neuableitung eines abgeleiteten Bundles geht damit nicht ohne Handarbeit; das bleibt offen und ist Folge-Issue #201 (Entwurf 11).
  • Was die Standardbibliothek kann: EML (email), DOCX/PPTX/XLSX (OOXML ist ZIP + XML: zipfile, xml.etree) und ZIP (zipfile). Was nicht ohne Paket geht: PDF und Outlook-MSG (ein OLE2-Compound-File). Dafür kommen pypdf (BSD-3) und olefile (BSD-2): beide reines Python ohne weitere Abhängigkeiten, laufen also auch auf nativem Windows, und beide sind mit der AGPL-3.0 des Stacks verträglich.

Ziel

Eine PDF-, EML-, MSG-, DOCX-, PPTX- oder XLSX-Datei oder ein ZIP wird vom Werkzeug in eine Form gebracht, in der Original und abgeleiteter Text als ein Ordner in incoming/ liegen: Original byte-unverändert + abgeleitete .md mit festem Header. raw accept übernimmt diesen Ordner danach unverändert. Die Formatlogik ist pluggable über eine Registry im Stack.

Leitprinzip: so wenig Agentenkontext wie möglich. Der Agent muss nur wissen, dass es raw derive gibt. Alles Formatspezifische - was zu prüfen ist, was fehlt, was als Nächstes kommt - sagt das Werkzeug in seiner Ausgabe, und zwar nur für das Format, das gerade abgeleitet wurde. Es gibt keine Instruction pro Format und keine Routing-Tabelle im Skill.

Entscheidungen des Betreibers (2026-10-08)

  1. Abhängigkeiten: pypdf und olefile kommen in tools/requirements.txt und werden lazy importiert (Entwurf 8). Verworfen wurde ein optionales Extra wie requirements-mcp.txt: Eine frische Instanz könnte dann kein PDF ableiten, und das Extra findet niemand von selbst.
  2. „Pluggable“ heißt nur die Code-Registry (Entwurf 2): Ein neues Format ist ein Modul im Stack. Instanzeigene externe Filter (eine .wikitool-filters.json nach dem Muster von #169) wurden verworfen, auch nicht als Folge-Issue.
  3. Ein ZIP ist eine Quelle: Es wird zum Ordner incoming/<stem>/, der nach der bestehenden Ordnerregel als Ganzes angenommen wird. „Dateien einzeln“ im Stub heißt, dass jede Datei darin einzeln abgeleitet wird.
  4. Das ZIP selbst entfällt nach erfolgreichem Entpacken. Die Einträge sind byte-identisch mit dem, was empfangen wurde; das ZIP war nur die Verpackung. Name und sha256 des ZIPs nennt die Ausgabe (Entwurf 6).
  5. Container: Eine E-Mail mit Anhängen ist ein Container wie ein ZIP. Ein ZIP in einer E-Mail darf einmal entpackt werden. Eine als Anlage angehängte E-Mail wird zitiert, nicht ausgepackt. Die genauen Regeln stehen in Entwurf 5.
  6. Instructions: Selective Disclosure statt Format-Instructions (Leitprinzip oben, Entwurf 6 und 7). Verworfen wurden die zuerst entworfenen fünf instructions/ingest-<format>.md und eine Routing-Tabelle in wiki-ingest: Beides hätte den Agentenkontext für Formate belastet, die gerade gar nicht anstehen.
  7. raw derive legt immer einen Ordner an, mit festen Namen darin (Entwurf 1). Eine einzelne Datei wird zu incoming/<stem>/original.<ext> + derived.md, damit der sprechende Name nur einmal im Pfad steht: etwa 135 Zeichen Stamm statt etwa 71. Der ursprüngliche Dateiname steht als original_name: im Header. Das Pfadbudget prüft schon raw derive, bevor es etwas schreibt.
  8. Einzelne Quellen ohne Ableitung bleiben ohne Ordner (incoming/quelle.md → raw/<YYYY>/<MM>/quelle.md, wie bisher). Entscheidung 7 folgt der bestehenden Regel „bundle directory only from the second file onward“, denn eine Ableitung ergibt immer mindestens zwei Dateien. Verworfen wurde „jede Quelle bekommt einen Ordner“: Was schon in raw/ liegt, wird nie verschoben, also würde die Änderung nichts vereinheitlichen, sondern nur eine weitere Form neben den alten Typverzeichnissen und dem Datums-Shard schaffen. Der Agent wählt ohnehin nie einen raw/-Pfad (raw accept berechnet ihn, die Seite referenziert ihn über raw_files:), muss die Formen also nicht kennen.

Entwurf

  1. Neues Kommando raw derive incoming/<datei|ordner> [--name <stem>].

    • Eine Datei direkt in incoming/ wird zum Ordner incoming/<stem>/. Darin liegen original.<ext>, das Original mit unveränderten Bytes, hineinverschoben, und derived.md. incoming/<stem>.<ext> existiert danach nicht mehr. <stem> ist der Stamm der Datei; --name ersetzt ihn (Prüfung wie bei raw fetch --name). Für Container (ZIP, E-Mail mit Anhängen) gilt Entwurf 5.
    • Ein Ordner in incoming/, etwa ein von Hand abgelegter: Jede Datei darin mit passendem Filter bekommt <ihr-stamm>.md daneben. Umbenannt wird dort nichts - der Ordnername ist nicht der Dateiname, also verdoppelt sich nichts. Dateien ohne Filter (.md, .txt, Bilder) bleiben, wie sie sind, und die Ausgabe nennt sie. Existiert eine solche .md schon → exit 1, nichts geschrieben.
    • Vor dem ersten Schreiben wird alles geprüft. Bei exit 1 ist incoming/ exakt wie vorher. Geprüft werden:
      • die Pfade wie bei raw accept: außerhalb von incoming/ → exit 1; eine einzelne Datei in einem Unterordner → exit 1, mit beiden Auswegen;
      • ein Zielordner incoming/<stem>/, der schon existiert → exit 1;
      • das Pfadbudget: Für jede Datei, die geschrieben würde, rechnet raw derive den späteren Pfad unter raw/<YYYY>/<MM>/ mit derselben Funktion wie raw accept (path_budget_problem_for), bei Containern bis zum tiefsten Eintrag. Ist einer zu lang → exit 1 mit dem Pfad und --name als Ausweg.
    • Ein Name, der unter raw/ schon belegt ist, ist keine Verweigerung, sondern ein WARN mit der Meldung von raw accept: Es könnte eine neue Ausgabe sein, und diese Frage stellt raw accept, nicht raw derive.
    • Kein Netzzugriff. Die Ausgabe landet in incoming/ und nie in raw/: Promotion und --fidelity/--authority bleiben bei raw accept (wie #120 Entscheidungen 1 und 13). raw fetch und raw fetch --html bleiben unverändert.
  2. Filter-Registry tools/chemenu/filters/, ein Modul pro Formatfamilie: pdf.py, mail.py (EML), msg.py (MSG), ooxml.py (DOCX, PPTX, XLSX), archive.py (ZIP), html_page.py (Adapter auf web_capture, für eine HTML-Datei ohne URL, etwa in einem ZIP). Jeder Filter gibt an:

    • welche Endungen er nimmt,
    • seinen name und engine (mit Version),
    • ob er available() ist (ob der Import gelingt),
    • derive(bytes, name) -> Derivation: Text, Header-Felder, Warnungen und checks (Entwurf 6).

    Der Kern kennt weder typer noch rich und wirft ValidationError, wie web_capture. Ein neues Format braucht ein Modul, eine Registrierung und Tests; Kommando, Header und Skill bleiben dabei gleich.

  3. Header nach #120 Entscheidung 6 (Werte YAML-gequotet, wo YAML sie sonst nicht unverändert zurückliest). Die Header-Funktion wird aus web_capture geteilt, nicht kopiert:

    ---
    derived_by: wikitool raw derive
    filter: pdf
    engine: "wikitool 9.1.0 + pypdf 5.4.0"
    derived: 2026-10-08T10:00:00Z
    derived_from: original.pdf
    original_name: "Angebot XYZ GmbH Netzwerkmodernisierung v3.pdf"
    title: <aus den Metadaten, sonst leer>
    <formatspezifische Felder>
    ---
    
    • derived_from ist der Dateiname im Ordner. original_name ist der Name, unter dem die Datei in incoming/ lag; bei einer Datei, die nicht umbenannt wurde (in einem Ordner, als Anhang), fällt das Feld weg.
    • engine nennt die Version, weil „gleiche Bytes ergeben denselben Text“ nur für dieselbe Engine-Version gilt. Ein reiner Standardbibliotheks-Filter schreibt nur wikitool <version>.
    • Formatspezifische Felder: PDF pages; E-Mail from, to, cc, date, subject, message_id, attachments; XLSX sheets, hidden_sheets; PPTX slides; DOCX tracked_changes, comments.
    • Die Felder sind Erfassungsmetadaten, nichts im Stack liest sie zurück.
  4. Ableitung je Format, deterministisch:

    • PDF: Text pro Seite, die Seitengrenzen als <!-- page N -->. Kein OCR. Liegt der Schnitt unter 50 Zeichen pro Seite, warnt das Kommando „wahrscheinlich gescannt, ohne Textschicht“, schreibt aber trotzdem. Ein verschlüsseltes PDF → exit 1.
    • EML/MSG: Die Kopfzeilen kommen in den Header, der Body wird Text: text/plain bevorzugt, sonst HTML über web_capture. Bei MSG ist das PR_BODY, sonst PR_HTML; hat eine MSG nur einen RTF-Body, warnt das Kommando, denn einen RTF-Parser gibt es nicht. Anhänge: Entwurf 5.
    • DOCX: Absätze; Überschriften aus den Formatvorlagen „Heading n“ als #; Listen; Tabellen als Markdown-Tabellen. Bei Änderungsverfolgung kommt die angenommene Fassung in den Fließtext (w:ins ja, w:del nein). Kommentare stehen in einem eigenen Abschnitt am Ende.
    • PPTX: Ein Abschnitt pro Folie (## Slide N), die Sprechernotizen darunter (### Notes).
    • XLSX: Jedes Blatt wird ein Abschnitt ## <Blattname> in Blattreihenfolge, mit einer Markdown-Tabelle der gespeicherten Werte; Formeln werden nicht neu berechnet. Eine Formelzelle ohne gespeicherten Wert bleibt leer, und das Kommando warnt. Versteckte Blätter kommen mit und stehen in hidden_sheets. Leere Randzeilen und -spalten werden abgeschnitten. Eine Datei ergibt eine .md, nicht eine pro Blatt.
  5. Container: ZIP und E-Mail mit Anhängen. Für alle Fälle gilt die Prüfung vor dem ersten Schreiben aus Entwurf 1 - bei exit 1 ist nichts angelegt, verschoben oder gelöscht.

    • ZIP auf oberster Ebene: raw derive incoming/x.zip entpackt nach incoming/x/, leitet jede Datei darin einzeln ab (<ihr-stamm>.md daneben) und entfernt incoming/x.zip. original.*/derived.md gibt es hier nicht, weil das ZIP entfällt.
    • E-Mail auf oberster Ebene, mit oder ohne Anhänge: Es entsteht incoming/x/ mit original.eml/original.msg (unveränderte Bytes), derived.md und den Anhängen unter ihren eigenen Namen. Jeder Anhang mit Filter bekommt <ihr-stamm>.md daneben.
    • ZIP als Anhang einer E-Mail: wird einmal entpackt, nach incoming/x/<zipstem>/, und das ZIP entfällt. Ein ZIP darin → Verweigerung.
    • ZIP in einem ZIP: Verweigerung, der Eintrag wird genannt.
    • E-Mail als Anlage einer E-Mail (message/rfc822, eine angehängte .eml/.msg, ein eingebettetes Objekt in einer MSG): wird nicht als Datei ausgepackt, sondern als Abschnitt ## Attached message: <subject> mit Kopfzeilen und zitiertem Body in derived.md der äußeren E-Mail abgeleitet. Ihre eigenen Anhänge werden nicht ausgepackt; ihre Namen stehen in diesem Abschnitt und als WARN in der Ausgabe.
    • E-Mail in einem ZIP: bekommt eine eigene <stamm>.md und wird nicht umbenannt. Ihre Anhänge werden nicht ausgepackt, sondern genannt, wie oben. Eine E-Mail wird nur auf oberster Ebene zum Container.
    • Weitere Verweigerungsgründe: absoluter Pfad, .., Symlink-Eintrag, verschlüsselter Eintrag, mehr als 1000 Einträge, eine Datei über 25 MiB entpackt (wie raw capture), mehr als 250 MiB entpackt insgesamt (Schutz gegen Zip-Bomben).
    • Nicht geschrieben, aber in der Ausgabe genannt: versteckte Einträge (__MACOSX/, .DS_Store, alles mit . am Anfang), die raw accept sonst verweigern würde.
    • Dateinamen: ZIP-Namen werden als UTF-8 gelesen, wenn das Flag gesetzt ist, sonst als cp437 (die Voreinstellung von zipfile). Anhangsnamen werden von Pfadtrennern befreit. Doppelte Namen bekommen -2, -3 vor der Endung, und die Ausgabe nennt sie. Dasselbe gilt für einen Namen, der mit einem reservierten kollidiert: original.* und derived.md auf oberster Ordnerebene, _capture.json überall.
  6. Selective Disclosure: Das Formatwissen steht in der Ausgabe, nicht im Kontext. Jeder Filter trägt höchstens drei kurze CHECK-Zeilen auf Englisch, die raw derive nach dem Erfolg ausgibt - nur für Formate, die in diesem Lauf tatsächlich abgeleitet wurden, und, wo es geht, nur wenn der Fall eintritt. Das ist der einzige Ort, an dem diese Hinweise stehen (Invariante 8). Ungefährer Inhalt, endgültig im Bau:

    • PDF: der Scan-Hinweis (nur wenn die Warnung greift); Tabellen und Abbildungen sind im Text unvollständig, dann das PDF selbst lesen.
    • Mail: zitierte frühere Nachrichten sind nicht die Worte des Absenders; eine E-Mail eröffnet oder schließt oft eine Verpflichtung (wiki-ingest Schritt 4); Geheimnisse müssen vor raw accept heraus (raw/CONTRACT.md § What does not belong here).
    • XLSX: Werte stammen aus der letzten Speicherung, Formeln wurden nicht neu berechnet; versteckte Blätter (nur wenn es welche gibt).
    • DOCX: Änderungsverfolgung und Kommentare (nur wenn vorhanden).
    • ZIP: „unpacked from x.zip (sha256 …) - name it on the source page“.

    Danach folgt die raw accept-Zeile für den Ordner. Fehlschläge tragen ihre Reaktion im ON FAILURE-Text des Kommando-Records, nach dem Tool-Error-Contract.

  7. Kontextbudget im Skill: eine Zeile plus ein korrigiertes Beispiel.

    • Schritt 1: Der Satz „If it is binary or an image, note its presence and what it shows“ wird sinngemäß zu: „A PDF, mail, Office file or ZIP: run tools/wikitool raw derive <path> first and read the .md - its output names what to check. Any other binary or image: note its presence and what it shows.“
    • Schritt 5: Das Beispiel „an uploaded PDF plus its converted Markdown“ wird durch das raw fetch-Paar ersetzt, denn eine Ableitung ist jetzt ein Ordner und wird als Ganzes angenommen.
    • raw derive kommt in die Kommandoliste.
    • Sonst ändert sich am Skill nichts: keine Tabelle, keine neue Instruction.

    Zwei mechanische Netze sorgen dafür, dass die Zeile nicht erinnert werden muss:

    • raw pending nennt bei einem Kandidaten, der eine Datei mit Filter direkt in incoming/ ist, die raw derive-Zeile.
    • raw accept gibt bei einer solchen Datei ein WARN mit derselben Zeile aus. Es verweigert nicht: exit 0 und Verhalten wie bisher, damit eine bewusst nur als Binärdatei angenommene Datei weiter möglich bleibt.
  8. Abhängigkeiten werden lazy importiert, im Filtermodul und nie auf Modulebene von cli.py. Fehlt ein Paket (venv ohne neuen Preflight), endet nur raw derive für dieses Format mit exit 1 und dem Preflight-Hinweis; alle anderen Kommandos laufen. doctor meldet je Filter verfügbar oder nicht verfügbar, ohne zu FAILen.

  9. raw/CONTRACT.md bekommt einen kurzen Abschnitt „Getting a document in: raw derive“ nach § Getting a URL in, nur mit den Regeln: die Ordnerform mit original.<ext>/derived.md und warum (Pfadbudget), abgeleiteter Text wird nie von Hand bearbeitet, die Container-Regel aus Entwurf 5, das ZIP entfällt. Keine Formathinweise - die stehen in Entwurf 6. Das Beispiel „a PDF and its converted text“ in § Getting a file in (Kandidaten von raw pending) wird angepasst; dasselbe Beispiel im Kommando-Record von raw pending zog das Schließen nach (§ Stand, Abschluss).

  10. Nicht Teil dieses Pakets: OCR, externe/instanzeigene Filter (Entscheidung 2), ein MCP-Tool für die Ableitung, weitere Formate (ODT, RTF, Bilder).

  11. Neue Ausgabe und Neuableitung eines abgeleiteten Bundles - nicht Teil dieses Pakets, Folge-Issue #201.

    • Befund: Ein gewöhnliches Ordner-Bundle wird nur Datei für Datei ersetzt, und --replaces nimmt nur Dateien direkt in incoming/. Dasselbe Dokument in einer neuen Fassung (Angebot v2) trifft bei raw accept auf den belegten Namen, und die Meldung bietet nur Umbenennen oder ein Ersetzen von Hand an.
    • Gebaut: Die Ausgabe von raw derive und der ON FAILURE-Text nennen diesen Weg ehrlich, statt --replaces zu versprechen, das so nicht greift.
    • Folge-Issue: #201. Wahrscheinlicher Kern: --replaces-bundle auch für abgeleitete Bundles, analog zu erfassten.

Betroffene Dateien

  • Neu: tools/chemenu/filters/__init__.py (Registry, Derivation, Header, checks), pdf.py, mail.py, msg.py, ooxml.py, archive.py, html_page.py
  • tools/chemenu/web_capture.py: Header-Rendering (header(), _scalar) teilen statt kopieren
  • tools/chemenu/commands/raw_cmd.py:
    • raw derive samt Kommando-Record (Synopsis, network: no, Exit-Status, ON FAILURE, NEVER)
    • Budget- und Namensprüfung durch Wiederverwendung von path_budget_problem_for/_occupied_stems, nicht als Kopie
    • der Hinweis in raw pending und das WARN in raw accept
  • tools/chemenu/cli_contract.py: raw derive in GROUPS („Raw material and uploads“)
  • tools/chemenu/commands/doctor.py: Verfügbarkeit der Filter
  • tools/requirements.txt: pypdf, olefile
  • Generiert, nicht von Hand: tools/CONTRACT.md, docs/reference/commands/raw-material-and-uploads.md, docs/reference/commands/instance-health.md
  • Neu: tools/chemenu/tests/test_raw_derive.py, test_raw_derive_binary.py und der Fixture-Baukasten binary_formats.py (siehe Abweichungen).
  • raw/CONTRACT.md: Entwurf 9
  • instructions/wiki-ingest/SKILL.md: Entwurf 7
  • tools/README.md (Modulliste), docs/how-to/ingest-a-source.md, sonst was instructions/dev/doc-pull-through.md beim Bau nennt
  • CHANGES.md über version bump

Akzeptanzkriterien

  • Für je eine Testdatei pro Format (PDF, EML ohne Anhang, MSG ohne Anhang, DOCX, PPTX, XLSX) ergibt raw derive incoming/x.<ext> mit exit 0 genau incoming/x/original.<ext> (byte-identisch zur Eingabe) und incoming/x/derived.md. incoming/x.<ext> existiert danach nicht mehr, raw/ ist unverändert, und raw accept incoming/x nimmt den Ordner ohne Verweigerung an.
  • Zwei Läufe über dieselben Bytes ergeben byte-identische derived.md, abgesehen von derived:.
  • Der Header jeder derived.md ist gültiges YAML mit derived_by, filter, engine (samt Version), derived, derived_from: original.<ext> und original_name (dem ursprünglichen Dateinamen).
  • Eine Datei, deren späterer Pfad raw/<YYYY>/<MM>/<stem>/original.<ext> über 160 Zeichen läge, endet mit exit 1, nennt den Pfad und --name, und incoming/ ist unverändert. Mit --name <kurz> läuft derselbe Aufruf durch. Ein Stamm von 120 Zeichen läuft ohne --name durch.
  • Ein Stamm, der unter raw/ schon belegt ist, ergibt exit 0 mit WARN; der Ordner wird geschrieben.
  • Ein schon existierender Zielordner incoming/x/ → exit 1, incoming/ unverändert.
  • raw derive incoming/<ordner> legt neben jede Datei mit Filter <stamm>.md, benennt nichts um und nennt die Dateien ohne Filter. Existiert eine solche .md schon → exit 1, nichts geschrieben.
  • Eine XLSX mit drei Blättern, eins davon versteckt, ergibt drei Abschnitte in Blattreihenfolge. Das versteckte Blatt steht in hidden_sheets, Formelzellen zeigen ihren gespeicherten Wert.
  • Ein dreiseitiges PDF ergibt drei <!-- page N -->-Marker. Ein PDF ohne Textschicht ergibt exit 0 mit der Scan-Warnung.
  • Eine DOCX mit Änderungsverfolgung enthält eingefügten Text und keinen gelöschten; ihre Kommentare stehen im Abschnitt am Ende.
  • Ein Pfad außerhalb von incoming/ oder eine einzelne Datei in einem Unterordner → exit 1, nichts geschrieben.
  • raw derive incoming/x.zip mit drei Dokumenten ergibt incoming/x/ mit den drei Dateien unter ihren Namen, byte-identisch zu den Einträgen, und je einer <stamm>.md dazu. incoming/x.zip existiert danach nicht mehr, und die Ausgabe nennt seinen Namen und sha256. raw accept incoming/x nimmt den Ordner an.
  • Eine EML mit einem PDF- und einem ZIP-Anhang (das ZIP mit zwei Dokumenten) ergibt incoming/x/ mit original.eml (byte-identisch), derived.md, dem PDF samt .md und incoming/x/<zipstem>/ mit beiden Dokumenten samt .md; das ZIP existiert nicht mehr. raw accept incoming/x nimmt den Ordner an.
  • Eine EML, deren ZIP-Anhang selbst ein ZIP enthält, endet mit exit 1, und incoming/ ist danach exakt wie vorher.
  • Eine EML mit einer angehängten EML (die ihrerseits einen Anhang hat) ergibt keinen Datei-Eintrag für die innere E-Mail. Deren Kopfzeilen und Body stehen als ## Attached message: … in derived.md, der Name ihres Anhangs steht dort und als WARN in der Ausgabe.
  • Eine EML in einem ZIP bekommt ihre <stamm>.md; ihre Anhänge werden nicht ausgepackt und als WARN genannt.
  • Zwei Anhänge mit gleichem Namen landen als <name> und <name>-2; ein Anhang namens derived.md landet als derived-2.md, einer namens _capture.json als _capture-2.json. Die Ausgabe nennt alle drei.
  • Ein ZIP mit einem ZIP darin, mit ..-Pfad, absolutem Pfad, Symlink-Eintrag, verschlüsseltem Eintrag oder über einer der Grenzen endet mit exit 1. incoming/ ist danach exakt wie vorher, das ZIP eingeschlossen.
  • __MACOSX/ und .DS_Store in einem ZIP werden nicht entpackt und in der Ausgabe genannt.
  • CHECK-Zeilen erscheinen nur für Formate, die im Lauf abgeleitet wurden: Ein DOCX ohne Änderungsverfolgung und Kommentare gibt keine DOCX-CHECK-Zeile aus, ein Lauf über eine XLSX keine Mail-CHECK-Zeile. Kein Filter gibt mehr als drei aus.
  • Fehlt das Paket eines Filters (Test per Monkeypatch des Imports), endet raw derive für dieses Format mit exit 1 und dem Preflight-Hinweis. raw pending und doctor laufen unverändert; doctor meldet den Filter als nicht verfügbar, ohne zu FAILen.
  • raw pending nennt bei einer Datei mit Filter direkt in incoming/ die passende raw derive-Zeile. raw accept derselben Datei allein endet mit exit 0, promotet sie wie bisher nach raw/<YYYY>/<MM>/<name> und gibt ein WARN mit derselben Zeile aus.
  • Eine einzelne incoming/quelle.md landet wie bisher ohne Ordner als raw/<YYYY>/<MM>/quelle.md (Entscheidung 8; die bestehenden Tests bleiben unverändert grün).
  • raw fetch und raw fetch --html erzeugen byte-identische Ausgaben wie vorher; die bestehenden Tests bleiben unverändert grün.
  • Kontextbudget: Kein neues instructions/*.md entsteht, und der Diff von instructions/wiki-ingest/SKILL.md beschränkt sich auf Entwurf 7 (Zeile in Schritt 1, Beispiel in Schritt 5, Kommandoliste).
  • pytest, docs verify und instructions verify sind grün; CI ist grün auf dem letzten publizierten Commit (8ea2795, Läufe 623 und 624; davor Schnitt 3 0e6a50a, Läufe 621 und 622; Schnitte 1 und 2: Läufe 617 bis 620).
  • Das Folge-Issue zu Entwurf 11 ist angelegt und hier verlinkt (#201).

Bauweise

Drei Schnitte mit je einem Publish; jeder war für sich auslieferbar. Alle drei sind publiziert, dazu der Pull-through beim Schließen (§ Stand):

  1. Registry, raw derive mit Ordnerform, Budget- und Namensprüfung, --name, Header, CHECK-Mechanik, DOCX/PPTX/XLSX und EML ohne Anhänge (nur Standardbibliothek), die Skill-Änderung, die Hinweise in raw pending und raw accept, der Abschnitt in raw/CONTRACT.md.
  2. Container: ZIP, E-Mail mit Anhängen, ZIP-in-Mail, zitierte angehängte E-Mail, die Regeln für reservierte und doppelte Namen (Entwurf 5).
  3. PDF und MSG samt requirements.txt und doctor-Zeile.

Versionsteil

--minor. Neues Kommando; nichts wird entfernt oder umbenannt, kein maschinell gelesenes Dateiformat ändert sich (der Header ist Erfassungsmetadaten), keine Content-Migration. Das WARN in raw accept ändert weder den Exit-Code noch das Verhalten, und die Bundle-Regel von raw accept bleibt unverändert (Entscheidung 8).

Die neuen Pakete in requirements.txt brechen den Drop-in-Test nicht, weil der Import lazy ist (Entwurf 8). Nach dem bloßen Kopieren läuft alles, was vorher lief, auch ohne neuen Preflight; nur die neue Ableitung für PDF und MSG braucht ihn, und upgrade-instance lässt den Preflight ohnehin laufen. Rückwärts ignoriert die alte Version die zusätzlichen Pakete im venv. Damit weicht dieser Entwurf bewusst von #120 Entscheidung 3 ab, die eine neue Abhängigkeit pauschal als Bruch der Vorwärtshälfte las. Der Unterschied ist der Lazy-Import: Dort hätte das Paket das neue Kommando selbst getragen, hier fehlt ohne Paket nur ein einzelner Filter.

Der laufende Kandidat stand bereits auf MAJOR (9.0.0-beta.13, jetzt beta.17); dieses Paket hat ihn nicht weiter eskaliert. Wird 9.0.0 vorher released, ist es ein MINOR auf 9.x.

Labels

  • area/kb: Intake nach raw/, wie #120.
  • kind/build: alle Fragen entschieden.
  • prio/planned: Bis zum Bau macht jeder Ingest einer PDF oder E-Mail eine Ableitung nach Gutdünken, die dann unveränderlich in raw/ liegt.
  • size/L: drei Schnitte, mehrere Sitzungen.

Bezug

  • #120: raw fetch, das Muster für Header, Determinismus, exklusives Schreiben und --name
  • #112: incoming/ als Warteschlange, die Ordnerregel
  • #58 und #67: die Bundle-Regel „ab der zweiten Datei“ und der Datums-Shard
  • #169: das Muster für instanzeigene externe Filter, hier bewusst nicht übernommen (Entscheidung 2)
  • #201: Folge-Issue, neue Ausgabe eines abgeleiteten Ordner-Bundles (Entwurf 11)
> Ausgearbeitet aus einem `status/incoming`-Stub am 2026-10-08 (Stand `9.0.0-beta.13`, `765ebe9`). Der Stub steht wörtlich im ersten Kommentar. Alle Betreiberfragen sind am 2026-10-08 entschieden (§ Entscheidungen des Betreibers). Gebaut, publiziert, CI grün und geschlossen am 2026-10-08. ## Stand (abgeschlossen) | Schnitt | Inhalt | Version | Commit | CI | |---|---|---|---|---| | 1 | Registry, `raw derive`, Ordnerform, Budget/Namen, Header, `CHECK`, DOCX/PPTX/XLSX/EML, Skill, `raw pending`/`raw accept`, `raw/CONTRACT.md` | `9.0.0-beta.14` | `9c0bb6b` | grün, Läufe 617 und 618 | | 2 | Container: ZIP, Mail mit Anhängen, ZIP-in-Mail, zitierte angehängte Mail, reservierte/doppelte Namen | `9.0.0-beta.15` | `6659ca5` | grün, Läufe 619 und 620 | | 3 | PDF (`pypdf`), MSG (`olefile`), `requirements.txt`, `doctor`-Zeilen `import-filter-<name>` | `9.0.0-beta.16` | `0e6a50a` | grün, Läufe 621 und 622 | | Abschluss | Pull-through im Schließen: Das Beispiel „a PDF and its converted text“ als Bündel-Fall im Kommando-Record von `raw pending` (und in dessen Docstring) ist entfernt, samt den beiden generierten Kopien (`tools/CONTRACT.md`, `docs/reference/commands/raw-material-and-uploads.md`) | `9.0.0-beta.17` | `8ea2795` | grün, Läufe 623 und 624 | Folge-Issue zu Entwurf 11: #201. **Prüfung beim Schließen (welche Dokumente nachgezogen wurden und welche nicht):** - Gefunden und behoben: Der Kommando-Record von `raw pending` nannte noch „a PDF and its converted text“ als Bündel, obwohl `raw/CONTRACT.md` und der Skill dieses Beispiel schon ersetzt hatten und eine Ableitung seither ein Ordner ist. - Geprüft, ohne Änderung: die Seiten unter `docs/explanation/` (keine beschreibt die Aufnahme nach Format), `INSTALL.md`, `instructions/preflight.md` (der Preflight vergleicht den sha256 von `requirements.txt` mit einem Stempel im venv und installiert die neuen Pakete beim nächsten Lauf nach), `docs/reference/skills.md` (Name und `description` von `wiki-ingest` unverändert), `docs/dev/development.md`. Das Paket berührt keine Installationsanleitung, die Zweitlesung gegen `INSTALL.md` entfällt damit. - Verifiziert: `docs verify`, `instructions verify` und die `raw`-/Contract-Tests lokal grün (310 bestanden, 14 übersprungen, weil `pypdf`/`olefile` im lokalen venv fehlen), CI grün auf `8ea2795` (Läufe 623 und 624). **Abweichungen vom Entwurf (im Bau entschieden):** - **Fixtures:** PDF und MSG liegen nicht als Binärdateien unter `tests/fixtures/`. `tests/binary_formats.py` baut beide zur Testzeit (ein PDF von Hand, ein Compound File mit eigenem Schreiber), und `pypdf`/`olefile` lesen sie dann in den Tests. Nichts Binäres im Repo; die Tests, die das Paket brauchen, werden übersprungen, wo es fehlt (CI installiert `requirements.txt`). - **Modulaufteilung:** MSG steht in `filters/msg.py` (erbt von `EmlFilter`), nicht in `mail.py`, damit `olefile` nur beim Ableiten einer MSG importiert wird. `mail.py` lädt `msg.py` für eine angehängte `.msg` erst bei Bedarf. - **Doppelte Namen:** Eine `.md`, die mit einer echten Datei kollidiert, bekommt in einem Container `-2`, `-3`; ein Ordner ohne Container-Ableitung verweigert wie im Entwurf. - **MSG:** Eine eingebettete Nachricht in einer MSG wird nur genannt (WARN), nicht gelesen. Ein Inline-Bild (Content-ID plus `image/*`) wird gezählt und nicht geschrieben. Eine angehängte `.msg` an einer EML wird wie eine angehängte EML zitiert. - **`Message.walk()` steigt in `message/rfc822` ab:** Eine eigene Blattiteration (`_leaves`) verhindert, dass Body und Anhänge einer angehängten Mail der äußeren Mail zugeschlagen werden. - **Pakete:** `pypdf>=5.0`, `olefile>=0.47`. ## Befund (gegen den Baum geprüft, Stand vor dem Bau, `9.0.0-beta.13`) Die ersten beiden Punkte beschreiben die Lücke, die dieses Paket geschlossen hat; die übrigen gelten weiter. - **Text aus einem Nicht-Text-Format leitete vor dem Bau nur `raw fetch` ab, und nur aus HTML** (`tools/chemenu/web_capture.py`): Original byte-genau + `<stem>.md` mit festem Header, beide in `incoming/`, `raw accept` nimmt sie zusammen (`raw/CONTRACT.md` § Getting a URL in). Für alle anderen Dateien gab es nichts. Ein PDF legt `raw fetch` „as received, no text derived“ ab. - **`wiki-ingest` setzte die Ableitung voraus, ohne sie zu liefern.** Schritt 1 sagte zu Binärdateien nur „note its presence and what it shows“, Schritt 5 nannte als Beispiel „an uploaded PDF plus its converted Markdown“. Die Konvertierung machte also jede Session selbst, mit dem, was ihr Harness gerade konnte (Claude Code liest PDFs selbst, die anderen drei Harnesses nicht gleich). Das war dieselbe Lage, die #120 für URLs behoben hat: zwei Sessions machen aus derselben Datei zwei verschiedene `raw/`-Dateien, und beide bleiben unveränderlich liegen. Seit dem Bau liefert `raw derive` die Ableitung, und Schritt 1 verweist darauf. - **Bundle-Mechanik:** `raw accept` bildet einen Bundle-Ordner ab der zweiten Datei, benannt nach dem Stamm der ersten (`raw_cmd.py` `_plan_file_moves`). Aus `incoming/x.pdf` + `incoming/x.md` wird so `raw/<YYYY>/<MM>/x/x.pdf` + `x.md`. Ein Ordner in `incoming/` ist eine Quelle und wird mit seiner Struktur übernommen; `raw accept` verweigert darin versteckte Einträge, Symlinks, Spezialdateien und `_capture.json` außerhalb eines erfassten Bundles. - **Pfadbudget:** 160 Zeichen relativ zur Instanz (`titles.PATH_BUDGET`), davon belegt `raw/2026/10/` schon 12. Eine Einzeldatei darf etwa 144 Zeichen Namen haben; im Bundle `<stem>/<stem>.pdf` steht der Stamm doppelt, dann bleiben **nur etwa 71**. `raw fetch` hält das nur ein, weil es Stämme auf 60 Zeichen kürzt. Ein vom Menschen benannter Dateiname scheitert sonst erst bei `raw accept`. (Das ist der Grund für die Ordnerform mit `original.<ext>`, Entscheidung 7.) - **Neue Ausgabe eines Ordner-Bundles:** `--replaces-bundle` gibt es nur für erfasste Bundles (`_capture.json`). Für einen gewöhnlichen Ordner sagt die Verweigerung „replaced file by file“ (`raw_cmd.py` `_folder_collision_message`), und `--replaces` nimmt nur eine Datei direkt in `incoming/`. Eine neue Ausgabe oder eine Neuableitung eines abgeleiteten Bundles geht damit nicht ohne Handarbeit; das bleibt offen und ist Folge-Issue #201 (Entwurf 11). - **Was die Standardbibliothek kann:** EML (`email`), DOCX/PPTX/XLSX (OOXML ist ZIP + XML: `zipfile`, `xml.etree`) und ZIP (`zipfile`). **Was nicht ohne Paket geht:** PDF und Outlook-MSG (ein OLE2-Compound-File). Dafür kommen `pypdf` (BSD-3) und `olefile` (BSD-2): beide reines Python ohne weitere Abhängigkeiten, laufen also auch auf nativem Windows, und beide sind mit der AGPL-3.0 des Stacks verträglich. ## Ziel Eine PDF-, EML-, MSG-, DOCX-, PPTX- oder XLSX-Datei oder ein ZIP wird **vom Werkzeug** in eine Form gebracht, in der Original und abgeleiteter Text als ein Ordner in `incoming/` liegen: Original byte-unverändert + abgeleitete `.md` mit festem Header. `raw accept` übernimmt diesen Ordner danach unverändert. Die Formatlogik ist pluggable über eine Registry im Stack. **Leitprinzip: so wenig Agentenkontext wie möglich.** Der Agent muss nur wissen, dass es `raw derive` gibt. Alles Formatspezifische - was zu prüfen ist, was fehlt, was als Nächstes kommt - sagt das Werkzeug in seiner Ausgabe, und zwar nur für das Format, das gerade abgeleitet wurde. Es gibt keine Instruction pro Format und keine Routing-Tabelle im Skill. ## Entscheidungen des Betreibers (2026-10-08) 1. **Abhängigkeiten:** `pypdf` und `olefile` kommen in `tools/requirements.txt` und werden lazy importiert (Entwurf 8). Verworfen wurde ein optionales Extra wie `requirements-mcp.txt`: Eine frische Instanz könnte dann kein PDF ableiten, und das Extra findet niemand von selbst. 2. **„Pluggable“ heißt nur die Code-Registry** (Entwurf 2): Ein neues Format ist ein Modul im Stack. Instanzeigene externe Filter (eine `.wikitool-filters.json` nach dem Muster von #169) wurden verworfen, auch nicht als Folge-Issue. 3. **Ein ZIP ist eine Quelle:** Es wird zum Ordner `incoming/<stem>/`, der nach der bestehenden Ordnerregel als Ganzes angenommen wird. „Dateien einzeln“ im Stub heißt, dass jede Datei darin einzeln abgeleitet wird. 4. **Das ZIP selbst entfällt** nach erfolgreichem Entpacken. Die Einträge sind byte-identisch mit dem, was empfangen wurde; das ZIP war nur die Verpackung. Name und sha256 des ZIPs nennt die Ausgabe (Entwurf 6). 5. **Container:** Eine E-Mail mit Anhängen ist ein Container wie ein ZIP. **Ein ZIP in einer E-Mail darf einmal entpackt werden.** Eine als Anlage angehängte E-Mail wird zitiert, nicht ausgepackt. Die genauen Regeln stehen in Entwurf 5. 6. **Instructions: Selective Disclosure statt Format-Instructions** (Leitprinzip oben, Entwurf 6 und 7). Verworfen wurden die zuerst entworfenen fünf `instructions/ingest-<format>.md` und eine Routing-Tabelle in `wiki-ingest`: Beides hätte den Agentenkontext für Formate belastet, die gerade gar nicht anstehen. 7. **`raw derive` legt immer einen Ordner an, mit festen Namen darin** (Entwurf 1). Eine einzelne Datei wird zu `incoming/<stem>/original.<ext>` + `derived.md`, damit der sprechende Name nur einmal im Pfad steht: etwa 135 Zeichen Stamm statt etwa 71. Der ursprüngliche Dateiname steht als `original_name:` im Header. Das Pfadbudget prüft schon `raw derive`, bevor es etwas schreibt. 8. **Einzelne Quellen ohne Ableitung bleiben ohne Ordner** (`incoming/quelle.md` → `raw/<YYYY>/<MM>/quelle.md`, wie bisher). Entscheidung 7 folgt der bestehenden Regel „bundle directory only from the second file onward“, denn eine Ableitung ergibt immer mindestens zwei Dateien. Verworfen wurde „jede Quelle bekommt einen Ordner“: Was schon in `raw/` liegt, wird nie verschoben, also würde die Änderung nichts vereinheitlichen, sondern nur eine weitere Form neben den alten Typverzeichnissen und dem Datums-Shard schaffen. Der Agent wählt ohnehin nie einen `raw/`-Pfad (`raw accept` berechnet ihn, die Seite referenziert ihn über `raw_files:`), muss die Formen also nicht kennen. ## Entwurf 1. **Neues Kommando `raw derive incoming/<datei|ordner> [--name <stem>]`.** - **Eine Datei direkt in `incoming/`** wird zum Ordner `incoming/<stem>/`. Darin liegen `original.<ext>`, das Original mit unveränderten Bytes, hineinverschoben, und `derived.md`. `incoming/<stem>.<ext>` existiert danach nicht mehr. `<stem>` ist der Stamm der Datei; `--name` ersetzt ihn (Prüfung wie bei `raw fetch --name`). Für Container (ZIP, E-Mail mit Anhängen) gilt Entwurf 5. - **Ein Ordner in `incoming/`**, etwa ein von Hand abgelegter: Jede Datei darin mit passendem Filter bekommt `<ihr-stamm>.md` daneben. Umbenannt wird dort nichts - der Ordnername ist nicht der Dateiname, also verdoppelt sich nichts. Dateien ohne Filter (`.md`, `.txt`, Bilder) bleiben, wie sie sind, und die Ausgabe nennt sie. Existiert eine solche `.md` schon → exit 1, nichts geschrieben. - **Vor dem ersten Schreiben wird alles geprüft.** Bei exit 1 ist `incoming/` exakt wie vorher. Geprüft werden: - die Pfade wie bei `raw accept`: außerhalb von `incoming/` → exit 1; eine einzelne Datei in einem Unterordner → exit 1, mit beiden Auswegen; - ein Zielordner `incoming/<stem>/`, der schon existiert → exit 1; - **das Pfadbudget**: Für jede Datei, die geschrieben würde, rechnet `raw derive` den späteren Pfad unter `raw/<YYYY>/<MM>/` mit derselben Funktion wie `raw accept` (`path_budget_problem_for`), bei Containern bis zum tiefsten Eintrag. Ist einer zu lang → exit 1 mit dem Pfad und `--name` als Ausweg. - **Ein Name, der unter `raw/` schon belegt ist**, ist **keine** Verweigerung, sondern ein WARN mit der Meldung von `raw accept`: Es könnte eine neue Ausgabe sein, und diese Frage stellt `raw accept`, nicht `raw derive`. - Kein Netzzugriff. Die Ausgabe landet in `incoming/` und nie in `raw/`: Promotion und `--fidelity`/`--authority` bleiben bei `raw accept` (wie #120 Entscheidungen 1 und 13). `raw fetch` und `raw fetch --html` bleiben unverändert. 2. **Filter-Registry `tools/chemenu/filters/`**, ein Modul pro Formatfamilie: `pdf.py`, `mail.py` (EML), `msg.py` (MSG), `ooxml.py` (DOCX, PPTX, XLSX), `archive.py` (ZIP), `html_page.py` (Adapter auf `web_capture`, für eine HTML-Datei ohne URL, etwa in einem ZIP). Jeder Filter gibt an: - welche Endungen er nimmt, - seinen `name` und `engine` (mit Version), - ob er `available()` ist (ob der Import gelingt), - `derive(bytes, name) -> Derivation`: Text, Header-Felder, Warnungen und `checks` (Entwurf 6). Der Kern kennt weder `typer` noch `rich` und wirft `ValidationError`, wie `web_capture`. **Ein neues Format braucht ein Modul, eine Registrierung und Tests**; Kommando, Header und Skill bleiben dabei gleich. 3. **Header** nach #120 Entscheidung 6 (Werte YAML-gequotet, wo YAML sie sonst nicht unverändert zurückliest). Die Header-Funktion wird aus `web_capture` geteilt, nicht kopiert: ``` --- derived_by: wikitool raw derive filter: pdf engine: "wikitool 9.1.0 + pypdf 5.4.0" derived: 2026-10-08T10:00:00Z derived_from: original.pdf original_name: "Angebot XYZ GmbH Netzwerkmodernisierung v3.pdf" title: <aus den Metadaten, sonst leer> <formatspezifische Felder> --- ``` - `derived_from` ist der Dateiname im Ordner. `original_name` ist der Name, unter dem die Datei in `incoming/` lag; bei einer Datei, die nicht umbenannt wurde (in einem Ordner, als Anhang), fällt das Feld weg. - `engine` nennt die Version, weil „gleiche Bytes ergeben denselben Text“ nur für dieselbe Engine-Version gilt. Ein reiner Standardbibliotheks-Filter schreibt nur `wikitool <version>`. - Formatspezifische Felder: PDF `pages`; E-Mail `from`, `to`, `cc`, `date`, `subject`, `message_id`, `attachments`; XLSX `sheets`, `hidden_sheets`; PPTX `slides`; DOCX `tracked_changes`, `comments`. - Die Felder sind Erfassungsmetadaten, nichts im Stack liest sie zurück. 4. **Ableitung je Format, deterministisch:** - **PDF:** Text pro Seite, die Seitengrenzen als `<!-- page N -->`. Kein OCR. Liegt der Schnitt unter 50 Zeichen pro Seite, warnt das Kommando „wahrscheinlich gescannt, ohne Textschicht“, schreibt aber trotzdem. Ein verschlüsseltes PDF → exit 1. - **EML/MSG:** Die Kopfzeilen kommen in den Header, der Body wird Text: `text/plain` bevorzugt, sonst HTML über `web_capture`. Bei MSG ist das `PR_BODY`, sonst `PR_HTML`; hat eine MSG nur einen RTF-Body, warnt das Kommando, denn einen RTF-Parser gibt es nicht. Anhänge: Entwurf 5. - **DOCX:** Absätze; Überschriften aus den Formatvorlagen „Heading n“ als `#`; Listen; Tabellen als Markdown-Tabellen. Bei Änderungsverfolgung kommt die angenommene Fassung in den Fließtext (`w:ins` ja, `w:del` nein). Kommentare stehen in einem eigenen Abschnitt am Ende. - **PPTX:** Ein Abschnitt pro Folie (`## Slide N`), die Sprechernotizen darunter (`### Notes`). - **XLSX:** Jedes Blatt wird ein Abschnitt `## <Blattname>` in Blattreihenfolge, mit einer Markdown-Tabelle der gespeicherten Werte; Formeln werden nicht neu berechnet. Eine Formelzelle ohne gespeicherten Wert bleibt leer, und das Kommando warnt. Versteckte Blätter kommen mit und stehen in `hidden_sheets`. Leere Randzeilen und -spalten werden abgeschnitten. Eine Datei ergibt eine `.md`, nicht eine pro Blatt. 5. **Container: ZIP und E-Mail mit Anhängen.** Für alle Fälle gilt die Prüfung vor dem ersten Schreiben aus Entwurf 1 - bei exit 1 ist nichts angelegt, verschoben oder gelöscht. - **ZIP auf oberster Ebene:** `raw derive incoming/x.zip` entpackt nach `incoming/x/`, leitet jede Datei darin einzeln ab (`<ihr-stamm>.md` daneben) und entfernt `incoming/x.zip`. `original.*`/`derived.md` gibt es hier nicht, weil das ZIP entfällt. - **E-Mail auf oberster Ebene, mit oder ohne Anhänge:** Es entsteht `incoming/x/` mit `original.eml`/`original.msg` (unveränderte Bytes), `derived.md` und den Anhängen unter ihren eigenen Namen. Jeder Anhang mit Filter bekommt `<ihr-stamm>.md` daneben. - **ZIP als Anhang einer E-Mail:** wird einmal entpackt, nach `incoming/x/<zipstem>/`, und das ZIP entfällt. Ein ZIP darin → Verweigerung. - **ZIP in einem ZIP:** Verweigerung, der Eintrag wird genannt. - **E-Mail als Anlage einer E-Mail** (`message/rfc822`, eine angehängte `.eml`/`.msg`, ein eingebettetes Objekt in einer MSG): wird nicht als Datei ausgepackt, sondern als Abschnitt `## Attached message: <subject>` mit Kopfzeilen und zitiertem Body in `derived.md` der äußeren E-Mail abgeleitet. Ihre eigenen Anhänge werden nicht ausgepackt; ihre Namen stehen in diesem Abschnitt und als WARN in der Ausgabe. - **E-Mail in einem ZIP:** bekommt eine eigene `<stamm>.md` und wird nicht umbenannt. Ihre Anhänge werden nicht ausgepackt, sondern genannt, wie oben. Eine E-Mail wird nur auf oberster Ebene zum Container. - **Weitere Verweigerungsgründe:** absoluter Pfad, `..`, Symlink-Eintrag, verschlüsselter Eintrag, mehr als 1000 Einträge, eine Datei über 25 MiB entpackt (wie `raw capture`), mehr als 250 MiB entpackt insgesamt (Schutz gegen Zip-Bomben). - **Nicht geschrieben, aber in der Ausgabe genannt:** versteckte Einträge (`__MACOSX/`, `.DS_Store`, alles mit `.` am Anfang), die `raw accept` sonst verweigern würde. - **Dateinamen:** ZIP-Namen werden als UTF-8 gelesen, wenn das Flag gesetzt ist, sonst als cp437 (die Voreinstellung von `zipfile`). Anhangsnamen werden von Pfadtrennern befreit. Doppelte Namen bekommen `-2`, `-3` vor der Endung, und die Ausgabe nennt sie. Dasselbe gilt für einen Namen, der mit einem reservierten kollidiert: `original.*` und `derived.md` auf oberster Ordnerebene, `_capture.json` überall. 6. **Selective Disclosure: Das Formatwissen steht in der Ausgabe, nicht im Kontext.** Jeder Filter trägt höchstens drei kurze `CHECK`-Zeilen auf Englisch, die `raw derive` nach dem Erfolg ausgibt - nur für Formate, die in diesem Lauf tatsächlich abgeleitet wurden, und, wo es geht, nur wenn der Fall eintritt. Das ist der **einzige Ort**, an dem diese Hinweise stehen (Invariante 8). Ungefährer Inhalt, endgültig im Bau: - **PDF:** der Scan-Hinweis (nur wenn die Warnung greift); Tabellen und Abbildungen sind im Text unvollständig, dann das PDF selbst lesen. - **Mail:** zitierte frühere Nachrichten sind nicht die Worte des Absenders; eine E-Mail eröffnet oder schließt oft eine Verpflichtung (`wiki-ingest` Schritt 4); Geheimnisse müssen vor `raw accept` heraus (`raw/CONTRACT.md` § What does not belong here). - **XLSX:** Werte stammen aus der letzten Speicherung, Formeln wurden nicht neu berechnet; versteckte Blätter (nur wenn es welche gibt). - **DOCX:** Änderungsverfolgung und Kommentare (nur wenn vorhanden). - **ZIP:** „unpacked from x.zip (sha256 …) - name it on the source page“. Danach folgt die `raw accept`-Zeile für den Ordner. Fehlschläge tragen ihre Reaktion im ON FAILURE-Text des Kommando-Records, nach dem Tool-Error-Contract. 7. **Kontextbudget im Skill: eine Zeile plus ein korrigiertes Beispiel.** - **Schritt 1:** Der Satz „If it is binary or an image, note its presence and what it shows“ wird sinngemäß zu: „A PDF, mail, Office file or ZIP: run `tools/wikitool raw derive <path>` first and read the `.md` - its output names what to check. Any other binary or image: note its presence and what it shows.“ - **Schritt 5:** Das Beispiel „an uploaded PDF plus its converted Markdown“ wird durch das `raw fetch`-Paar ersetzt, denn eine Ableitung ist jetzt ein Ordner und wird als Ganzes angenommen. - `raw derive` kommt in die Kommandoliste. - Sonst ändert sich am Skill nichts: keine Tabelle, keine neue Instruction. Zwei mechanische Netze sorgen dafür, dass die Zeile nicht erinnert werden muss: - `raw pending` nennt bei einem Kandidaten, der eine Datei mit Filter direkt in `incoming/` ist, die `raw derive`-Zeile. - `raw accept` gibt bei einer solchen Datei ein WARN mit derselben Zeile aus. Es verweigert nicht: exit 0 und Verhalten wie bisher, damit eine bewusst nur als Binärdatei angenommene Datei weiter möglich bleibt. 8. **Abhängigkeiten werden lazy importiert, im Filtermodul und nie auf Modulebene von `cli.py`.** Fehlt ein Paket (venv ohne neuen Preflight), endet nur `raw derive` für dieses Format mit exit 1 und dem Preflight-Hinweis; alle anderen Kommandos laufen. `doctor` meldet je Filter verfügbar oder nicht verfügbar, ohne zu FAILen. 9. **`raw/CONTRACT.md`** bekommt einen kurzen Abschnitt „Getting a document in: `raw derive`“ nach § Getting a URL in, nur mit den Regeln: die Ordnerform mit `original.<ext>`/`derived.md` und warum (Pfadbudget), abgeleiteter Text wird nie von Hand bearbeitet, die Container-Regel aus Entwurf 5, das ZIP entfällt. Keine Formathinweise - die stehen in Entwurf 6. Das Beispiel „a PDF and its converted text“ in § Getting a file in (Kandidaten von `raw pending`) wird angepasst; dasselbe Beispiel im Kommando-Record von `raw pending` zog das Schließen nach (§ Stand, Abschluss). 10. **Nicht Teil dieses Pakets:** OCR, externe/instanzeigene Filter (Entscheidung 2), ein MCP-Tool für die Ableitung, weitere Formate (ODT, RTF, Bilder). 11. **Neue Ausgabe und Neuableitung eines abgeleiteten Bundles - nicht Teil dieses Pakets, Folge-Issue #201.** - **Befund:** Ein gewöhnliches Ordner-Bundle wird nur Datei für Datei ersetzt, und `--replaces` nimmt nur Dateien direkt in `incoming/`. Dasselbe Dokument in einer neuen Fassung (Angebot v2) trifft bei `raw accept` auf den belegten Namen, und die Meldung bietet nur Umbenennen oder ein Ersetzen von Hand an. - **Gebaut:** Die Ausgabe von `raw derive` und der ON FAILURE-Text nennen diesen Weg ehrlich, statt `--replaces` zu versprechen, das so nicht greift. - **Folge-Issue:** #201. Wahrscheinlicher Kern: `--replaces-bundle` auch für abgeleitete Bundles, analog zu erfassten. ## Betroffene Dateien - Neu: `tools/chemenu/filters/__init__.py` (Registry, `Derivation`, Header, `checks`), `pdf.py`, `mail.py`, `msg.py`, `ooxml.py`, `archive.py`, `html_page.py` - `tools/chemenu/web_capture.py`: Header-Rendering (`header()`, `_scalar`) teilen statt kopieren - `tools/chemenu/commands/raw_cmd.py`: - `raw derive` samt Kommando-Record (Synopsis, `network: no`, Exit-Status, ON FAILURE, NEVER) - Budget- und Namensprüfung durch Wiederverwendung von `path_budget_problem_for`/`_occupied_stems`, nicht als Kopie - der Hinweis in `raw pending` und das WARN in `raw accept` - `tools/chemenu/cli_contract.py`: `raw derive` in `GROUPS` („Raw material and uploads“) - `tools/chemenu/commands/doctor.py`: Verfügbarkeit der Filter - `tools/requirements.txt`: `pypdf`, `olefile` - Generiert, nicht von Hand: `tools/CONTRACT.md`, `docs/reference/commands/raw-material-and-uploads.md`, `docs/reference/commands/instance-health.md` - Neu: `tools/chemenu/tests/test_raw_derive.py`, `test_raw_derive_binary.py` und der Fixture-Baukasten `binary_formats.py` (siehe Abweichungen). - `raw/CONTRACT.md`: Entwurf 9 - `instructions/wiki-ingest/SKILL.md`: Entwurf 7 - `tools/README.md` (Modulliste), `docs/how-to/ingest-a-source.md`, sonst was `instructions/dev/doc-pull-through.md` beim Bau nennt - `CHANGES.md` über `version bump` ## Akzeptanzkriterien - [x] Für je eine Testdatei pro Format (PDF, EML ohne Anhang, MSG ohne Anhang, DOCX, PPTX, XLSX) ergibt `raw derive incoming/x.<ext>` mit exit 0 genau `incoming/x/original.<ext>` (byte-identisch zur Eingabe) und `incoming/x/derived.md`. `incoming/x.<ext>` existiert danach nicht mehr, `raw/` ist unverändert, und `raw accept incoming/x` nimmt den Ordner ohne Verweigerung an. - [x] Zwei Läufe über dieselben Bytes ergeben byte-identische `derived.md`, abgesehen von `derived:`. - [x] Der Header jeder `derived.md` ist gültiges YAML mit `derived_by`, `filter`, `engine` (samt Version), `derived`, `derived_from: original.<ext>` und `original_name` (dem ursprünglichen Dateinamen). - [x] Eine Datei, deren späterer Pfad `raw/<YYYY>/<MM>/<stem>/original.<ext>` über 160 Zeichen läge, endet mit exit 1, nennt den Pfad und `--name`, und `incoming/` ist unverändert. Mit `--name <kurz>` läuft derselbe Aufruf durch. Ein Stamm von 120 Zeichen läuft ohne `--name` durch. - [x] Ein Stamm, der unter `raw/` schon belegt ist, ergibt exit 0 mit WARN; der Ordner wird geschrieben. - [x] Ein schon existierender Zielordner `incoming/x/` → exit 1, `incoming/` unverändert. - [x] `raw derive incoming/<ordner>` legt neben jede Datei mit Filter `<stamm>.md`, benennt nichts um und nennt die Dateien ohne Filter. Existiert eine solche `.md` schon → exit 1, nichts geschrieben. - [x] Eine XLSX mit drei Blättern, eins davon versteckt, ergibt drei Abschnitte in Blattreihenfolge. Das versteckte Blatt steht in `hidden_sheets`, Formelzellen zeigen ihren gespeicherten Wert. - [x] Ein dreiseitiges PDF ergibt drei `<!-- page N -->`-Marker. Ein PDF ohne Textschicht ergibt exit 0 mit der Scan-Warnung. - [x] Eine DOCX mit Änderungsverfolgung enthält eingefügten Text und keinen gelöschten; ihre Kommentare stehen im Abschnitt am Ende. - [x] Ein Pfad außerhalb von `incoming/` oder eine einzelne Datei in einem Unterordner → exit 1, nichts geschrieben. - [x] `raw derive incoming/x.zip` mit drei Dokumenten ergibt `incoming/x/` mit den drei Dateien unter ihren Namen, byte-identisch zu den Einträgen, und je einer `<stamm>.md` dazu. `incoming/x.zip` existiert danach nicht mehr, und die Ausgabe nennt seinen Namen und sha256. `raw accept incoming/x` nimmt den Ordner an. - [x] Eine EML mit einem PDF- und einem ZIP-Anhang (das ZIP mit zwei Dokumenten) ergibt `incoming/x/` mit `original.eml` (byte-identisch), `derived.md`, dem PDF samt `.md` und `incoming/x/<zipstem>/` mit beiden Dokumenten samt `.md`; das ZIP existiert nicht mehr. `raw accept incoming/x` nimmt den Ordner an. - [x] Eine EML, deren ZIP-Anhang selbst ein ZIP enthält, endet mit exit 1, und `incoming/` ist danach exakt wie vorher. - [x] Eine EML mit einer angehängten EML (die ihrerseits einen Anhang hat) ergibt keinen Datei-Eintrag für die innere E-Mail. Deren Kopfzeilen und Body stehen als `## Attached message: …` in `derived.md`, der Name ihres Anhangs steht dort und als WARN in der Ausgabe. - [x] Eine EML in einem ZIP bekommt ihre `<stamm>.md`; ihre Anhänge werden nicht ausgepackt und als WARN genannt. - [x] Zwei Anhänge mit gleichem Namen landen als `<name>` und `<name>-2`; ein Anhang namens `derived.md` landet als `derived-2.md`, einer namens `_capture.json` als `_capture-2.json`. Die Ausgabe nennt alle drei. - [x] Ein ZIP mit einem ZIP darin, mit `..`-Pfad, absolutem Pfad, Symlink-Eintrag, verschlüsseltem Eintrag oder über einer der Grenzen endet mit exit 1. `incoming/` ist danach exakt wie vorher, das ZIP eingeschlossen. - [x] `__MACOSX/` und `.DS_Store` in einem ZIP werden nicht entpackt und in der Ausgabe genannt. - [x] `CHECK`-Zeilen erscheinen nur für Formate, die im Lauf abgeleitet wurden: Ein DOCX ohne Änderungsverfolgung und Kommentare gibt keine DOCX-`CHECK`-Zeile aus, ein Lauf über eine XLSX keine Mail-`CHECK`-Zeile. Kein Filter gibt mehr als drei aus. - [x] Fehlt das Paket eines Filters (Test per Monkeypatch des Imports), endet `raw derive` für dieses Format mit exit 1 und dem Preflight-Hinweis. `raw pending` und `doctor` laufen unverändert; `doctor` meldet den Filter als nicht verfügbar, ohne zu FAILen. - [x] `raw pending` nennt bei einer Datei mit Filter direkt in `incoming/` die passende `raw derive`-Zeile. `raw accept` derselben Datei allein endet mit exit 0, promotet sie wie bisher nach `raw/<YYYY>/<MM>/<name>` und gibt ein WARN mit derselben Zeile aus. - [x] Eine einzelne `incoming/quelle.md` landet wie bisher ohne Ordner als `raw/<YYYY>/<MM>/quelle.md` (Entscheidung 8; die bestehenden Tests bleiben unverändert grün). - [x] `raw fetch` und `raw fetch --html` erzeugen byte-identische Ausgaben wie vorher; die bestehenden Tests bleiben unverändert grün. - [x] Kontextbudget: Kein neues `instructions/*.md` entsteht, und der Diff von `instructions/wiki-ingest/SKILL.md` beschränkt sich auf Entwurf 7 (Zeile in Schritt 1, Beispiel in Schritt 5, Kommandoliste). - [x] `pytest`, `docs verify` und `instructions verify` sind grün; CI ist grün auf dem letzten publizierten Commit (`8ea2795`, Läufe 623 und 624; davor Schnitt 3 `0e6a50a`, Läufe 621 und 622; Schnitte 1 und 2: Läufe 617 bis 620). - [x] Das Folge-Issue zu Entwurf 11 ist angelegt und hier verlinkt (#201). ## Bauweise Drei Schnitte mit je einem Publish; jeder war für sich auslieferbar. Alle drei sind publiziert, dazu der Pull-through beim Schließen (§ Stand): 1. Registry, `raw derive` mit Ordnerform, Budget- und Namensprüfung, `--name`, Header, `CHECK`-Mechanik, DOCX/PPTX/XLSX und EML ohne Anhänge (nur Standardbibliothek), die Skill-Änderung, die Hinweise in `raw pending` und `raw accept`, der Abschnitt in `raw/CONTRACT.md`. 2. Container: ZIP, E-Mail mit Anhängen, ZIP-in-Mail, zitierte angehängte E-Mail, die Regeln für reservierte und doppelte Namen (Entwurf 5). 3. PDF und MSG samt `requirements.txt` und `doctor`-Zeile. ## Versionsteil **`--minor`.** Neues Kommando; nichts wird entfernt oder umbenannt, kein maschinell gelesenes Dateiformat ändert sich (der Header ist Erfassungsmetadaten), keine Content-Migration. Das WARN in `raw accept` ändert weder den Exit-Code noch das Verhalten, und die Bundle-Regel von `raw accept` bleibt unverändert (Entscheidung 8). Die neuen Pakete in `requirements.txt` brechen den Drop-in-Test nicht, weil der Import lazy ist (Entwurf 8). Nach dem bloßen Kopieren läuft alles, was vorher lief, auch ohne neuen Preflight; nur die neue Ableitung für PDF und MSG braucht ihn, und `upgrade-instance` lässt den Preflight ohnehin laufen. Rückwärts ignoriert die alte Version die zusätzlichen Pakete im venv. Damit weicht dieser Entwurf bewusst von #120 Entscheidung 3 ab, die eine neue Abhängigkeit pauschal als Bruch der Vorwärtshälfte las. Der Unterschied ist der Lazy-Import: Dort hätte das Paket das neue Kommando selbst getragen, hier fehlt ohne Paket nur ein einzelner Filter. Der laufende Kandidat stand bereits auf MAJOR (`9.0.0-beta.13`, jetzt `beta.17`); dieses Paket hat ihn nicht weiter eskaliert. Wird 9.0.0 vorher released, ist es ein MINOR auf 9.x. ## Labels - `area/kb`: Intake nach `raw/`, wie #120. - `kind/build`: alle Fragen entschieden. - `prio/planned`: Bis zum Bau macht jeder Ingest einer PDF oder E-Mail eine Ableitung nach Gutdünken, die dann unveränderlich in `raw/` liegt. - `size/L`: drei Schnitte, mehrere Sitzungen. ## Bezug - #120: `raw fetch`, das Muster für Header, Determinismus, exklusives Schreiben und `--name` - #112: `incoming/` als Warteschlange, die Ordnerregel - #58 und #67: die Bundle-Regel „ab der zweiten Datei“ und der Datums-Shard - #169: das Muster für instanzeigene externe Filter, hier bewusst nicht übernommen (Entscheidung 2) - #201: Folge-Issue, neue Ausgabe eines abgeleiteten Ordner-Bundles (Entwurf 11)
torben added the status/incoming label 2026-10-07 14:18:55 +00:00
Author
Owner

Stub, wörtlich (vor der Ausarbeitung):

Titel: Import Filter

Wir brauchen Import-Filter und individuelle Instructions mit passendem, kompaktem Routing für:

  • PDF
  • Outlook .EML oder .MSG
  • DOCX, PPTX
  • XSLX (wir sollten auch mit mehrenen Excel-Blättern in einem File umgehen können.
  • ZIP (mit 1 Rekursionsebene: ZIP in Subdir, dann Dateien einzeln. ZIP in ZIP verweigern)
    also etwas, das pluggable ist

Changelog: Stub gegen den Baum geprüft (9.0.0-beta.13, 765ebe9) und ausgearbeitet: Befund, Entwurf (raw derive, Filter-Registry, Header, Ableitung je Format, ZIP-Regel, Routing, Format-Instructions), fünf offene Betreiberfragen, betroffene Dateien, Akzeptanzkriterien, Bauweise in drei Schnitten, Versionsteil --minor. Titel präzisiert. Labels area/kb, kind/decision, prio/planned, size/L; status/incoming entfernt.

**Stub, wörtlich (vor der Ausarbeitung):** > Titel: Import Filter > > Wir brauchen Import-Filter und individuelle Instructions mit passendem, kompaktem Routing für: > - PDF > - Outlook .EML oder .MSG > - DOCX, PPTX > - XSLX (wir sollten auch mit mehrenen Excel-Blättern in einem File umgehen können. > - ZIP (mit 1 Rekursionsebene: ZIP in Subdir, dann Dateien einzeln. ZIP in ZIP verweigern) > also etwas, das pluggable ist **Changelog:** Stub gegen den Baum geprüft (`9.0.0-beta.13`, `765ebe9`) und ausgearbeitet: Befund, Entwurf (`raw derive`, Filter-Registry, Header, Ableitung je Format, ZIP-Regel, Routing, Format-Instructions), fünf offene Betreiberfragen, betroffene Dateien, Akzeptanzkriterien, Bauweise in drei Schnitten, Versionsteil `--minor`. Titel präzisiert. Labels `area/kb`, `kind/decision`, `prio/planned`, `size/L`; `status/incoming` entfernt.
torben changed title from Import Filter to Import-Filter: `raw derive` leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, mit Format-Instructions 2026-10-08 05:48:44 +00:00
torben added prio/plannedsize/Larea/kbkind/decision and removed status/incoming labels 2026-10-08 05:48:44 +00:00
torben changed title from Import-Filter: `raw derive` leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, mit Format-Instructions to Import-Filter: `raw derive` leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, Formatwissen in der Werkzeugausgabe 2026-10-08 06:59:20 +00:00
torben added kind/build and removed kind/decision labels 2026-10-08 06:59:20 +00:00
Author
Owner

Changelog: Die fünf Betreiberfragen sind entschieden (pypdf/olefile in requirements.txt; nur Code-Registry, keine externen Filter; ZIP = eine Quelle; ZIP entfällt nach dem Entpacken; E-Mail ist ein Container, ein ZIP darin wird einmal entpackt, eine angehängte E-Mail zitiert). Neu ist das Leitprinzip Selective Disclosure: Die fünf Format-Instructions und die Routing-Tabelle sind gestrichen. Formatwissen steht jetzt nur in CHECK-Zeilen der Ausgabe von raw derive, im Skill ändert sich eine Zeile, und raw pending und raw accept (WARN) sind die mechanischen Netze. Entwurf 5 regelt die Container vollständig, ergänzt um gleichnamige Anhänge und die E-Mail im ZIP. Akzeptanzkriterien entsprechend; neu ist ein Kriterium zum Kontextbudget. kind/decision → kind/build.

**Changelog:** Die fünf Betreiberfragen sind entschieden (pypdf/olefile in `requirements.txt`; nur Code-Registry, keine externen Filter; ZIP = eine Quelle; ZIP entfällt nach dem Entpacken; E-Mail ist ein Container, ein ZIP darin wird einmal entpackt, eine angehängte E-Mail zitiert). Neu ist das Leitprinzip Selective Disclosure: Die fünf Format-Instructions und die Routing-Tabelle sind gestrichen. Formatwissen steht jetzt nur in `CHECK`-Zeilen der Ausgabe von `raw derive`, im Skill ändert sich eine Zeile, und `raw pending` und `raw accept` (WARN) sind die mechanischen Netze. Entwurf 5 regelt die Container vollständig, ergänzt um gleichnamige Anhänge und die E-Mail im ZIP. Akzeptanzkriterien entsprechend; neu ist ein Kriterium zum Kontextbudget. `kind/decision` → `kind/build`.
Author
Owner

Changelog: Pfadbudget behoben. Befund: Im Bundle <stem>/<stem>.ext steht der Stamm doppelt, dadurch bleiben nur etwa 71 statt etwa 144 Zeichen. Entscheidung 7: raw derive legt immer incoming/<stem>/ mit original.<ext> + derived.md an, der ursprüngliche Name steht als original_name: im Header, und Budget wie Namensbelegung prüft schon raw derive (Budget → exit 1 mit --name als Ausweg, Namensbelegung → WARN). Entscheidung 8: Einzelne Quellen ohne Ableitung bleiben ohne Ordner. Entwurf 5 ist auf die Ordnerform umgestellt, dazu reservierte Namen (original.*, derived.md, _capture.json). Der Skill-Diff enthält jetzt auch das Beispiel in Schritt 5. Neuer Befund und Entwurf 11: Eine neue Ausgabe eines Ordner-Bundles geht heute nicht über --replaces und kommt als Folge-Issue beim Schließen. Akzeptanzkriterien entsprechend angepasst.

**Changelog:** Pfadbudget behoben. Befund: Im Bundle `<stem>/<stem>.ext` steht der Stamm doppelt, dadurch bleiben nur etwa 71 statt etwa 144 Zeichen. Entscheidung 7: `raw derive` legt immer `incoming/<stem>/` mit `original.<ext>` + `derived.md` an, der ursprüngliche Name steht als `original_name:` im Header, und Budget wie Namensbelegung prüft schon `raw derive` (Budget → exit 1 mit `--name` als Ausweg, Namensbelegung → WARN). Entscheidung 8: Einzelne Quellen ohne Ableitung bleiben ohne Ordner. Entwurf 5 ist auf die Ordnerform umgestellt, dazu reservierte Namen (`original.*`, `derived.md`, `_capture.json`). Der Skill-Diff enthält jetzt auch das Beispiel in Schritt 5. Neuer Befund und Entwurf 11: Eine neue Ausgabe eines Ordner-Bundles geht heute nicht über `--replaces` und kommt als Folge-Issue beim Schließen. Akzeptanzkriterien entsprechend angepasst.
Author
Owner

Changelog: Bau in drei Schnitten publiziert und grün: 9.0.0-beta.14 (9c0bb6b, Läufe 617/618), 9.0.0-beta.15 (6659ca5, Läufe 619/620), 9.0.0-beta.16 (0e6a50a, Läufe 621/622). Alle Akzeptanzkriterien abgehakt. Abweichungen im Bau: PDF- und MSG-Fixtures werden zur Testzeit gebaut (tests/binary_formats.py) statt als Binärdateien abgelegt; MSG liegt in filters/msg.py statt in mail.py; eine eingebettete Nachricht in einer MSG wird nur genannt, ein Inline-Bild nur gezählt; _leaves() in mail.py hält Body und Anhänge einer angehängten Mail aus der äußeren heraus. Folge-Issue zu Entwurf 11 ist #201 und im Body verlinkt.

**Changelog:** Bau in drei Schnitten publiziert und grün: `9.0.0-beta.14` (`9c0bb6b`, Läufe 617/618), `9.0.0-beta.15` (`6659ca5`, Läufe 619/620), `9.0.0-beta.16` (`0e6a50a`, Läufe 621/622). Alle Akzeptanzkriterien abgehakt. Abweichungen im Bau: PDF- und MSG-Fixtures werden zur Testzeit gebaut (`tests/binary_formats.py`) statt als Binärdateien abgelegt; MSG liegt in `filters/msg.py` statt in `mail.py`; eine eingebettete Nachricht in einer MSG wird nur genannt, ein Inline-Bild nur gezählt; `_leaves()` in `mail.py` hält Body und Anhänge einer angehängten Mail aus der äußeren heraus. Folge-Issue zu Entwurf 11 ist #201 und im Body verlinkt.
Author
Owner

Changelog: Body auf Endstand gebracht; Pull-through im Schließen: das Bündel-Beispiel „PDF und konvertierter Text“ im Kommando-Record von raw pending ersetzt (9.0.0-beta.17, 8ea2795, Läufe 623 und 624 grün). Geschlossen.

**Changelog:** Body auf Endstand gebracht; Pull-through im Schließen: das Bündel-Beispiel „PDF und konvertierter Text“ im Kommando-Record von `raw pending` ersetzt (`9.0.0-beta.17`, `8ea2795`, Läufe 623 und 624 grün). Geschlossen.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#199