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)
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)
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.
„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.
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.
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).
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.
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.
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.
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
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.
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:
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.
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_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.
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.
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.
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.
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.
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.
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).
Nicht Teil dieses Pakets: OCR, externe/instanzeigene Filter (Entscheidung 2), ein MCP-Tool für die Ableitung, weitere Formate (ODT, RTF, Bilder).
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.
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):
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.
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).
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.
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)
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-Instructions2026-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 Werkzeugausgabe2026-10-08 06:59:20 +00:00
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`.
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Stand (abgeschlossen)
raw derive, Ordnerform, Budget/Namen, Header,CHECK, DOCX/PPTX/XLSX/EML, Skill,raw pending/raw accept,raw/CONTRACT.md9.0.0-beta.149c0bb6b9.0.0-beta.156659ca5pypdf), MSG (olefile),requirements.txt,doctor-Zeilenimport-filter-<name>9.0.0-beta.160e6a50araw 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.178ea2795Folge-Issue zu Entwurf 11: #201.
Prüfung beim Schließen (welche Dokumente nachgezogen wurden und welche nicht):
raw pendingnannte noch „a PDF and its converted text“ als Bündel, obwohlraw/CONTRACT.mdund der Skill dieses Beispiel schon ersetzt hatten und eine Ableitung seither ein Ordner ist.docs/explanation/(keine beschreibt die Aufnahme nach Format),INSTALL.md,instructions/preflight.md(der Preflight vergleicht den sha256 vonrequirements.txtmit einem Stempel im venv und installiert die neuen Pakete beim nächsten Lauf nach),docs/reference/skills.md(Name unddescriptionvonwiki-ingestunverändert),docs/dev/development.md. Das Paket berührt keine Installationsanleitung, die Zweitlesung gegenINSTALL.mdentfällt damit.docs verify,instructions verifyund dieraw-/Contract-Tests lokal grün (310 bestanden, 14 übersprungen, weilpypdf/olefileim lokalen venv fehlen), CI grün auf8ea2795(Läufe 623 und 624).Abweichungen vom Entwurf (im Bau entschieden):
tests/fixtures/.tests/binary_formats.pybaut beide zur Testzeit (ein PDF von Hand, ein Compound File mit eigenem Schreiber), undpypdf/olefilelesen sie dann in den Tests. Nichts Binäres im Repo; die Tests, die das Paket brauchen, werden übersprungen, wo es fehlt (CI installiertrequirements.txt).filters/msg.py(erbt vonEmlFilter), nicht inmail.py, damitolefilenur beim Ableiten einer MSG importiert wird.mail.pylädtmsg.pyfür eine angehängte.msgerst bei Bedarf..md, die mit einer echten Datei kollidiert, bekommt in einem Container-2,-3; ein Ordner ohne Container-Ableitung verweigert wie im Entwurf.image/*) wird gezählt und nicht geschrieben. Eine angehängte.msgan einer EML wird wie eine angehängte EML zitiert.Message.walk()steigt inmessage/rfc822ab: Eine eigene Blattiteration (_leaves) verhindert, dass Body und Anhänge einer angehängten Mail der äußeren Mail zugeschlagen werden.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.
raw fetchab, und nur aus HTML (tools/chemenu/web_capture.py): Original byte-genau +<stem>.mdmit festem Header, beide inincoming/,raw acceptnimmt sie zusammen (raw/CONTRACT.md§ Getting a URL in). Für alle anderen Dateien gab es nichts. Ein PDF legtraw fetch„as received, no text derived“ ab.wiki-ingestsetzte 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 verschiedeneraw/-Dateien, und beide bleiben unveränderlich liegen. Seit dem Bau liefertraw derivedie Ableitung, und Schritt 1 verweist darauf.raw acceptbildet einen Bundle-Ordner ab der zweiten Datei, benannt nach dem Stamm der ersten (raw_cmd.py_plan_file_moves). Ausincoming/x.pdf+incoming/x.mdwird soraw/<YYYY>/<MM>/x/x.pdf+x.md. Ein Ordner inincoming/ist eine Quelle und wird mit seiner Struktur übernommen;raw acceptverweigert darin versteckte Einträge, Symlinks, Spezialdateien und_capture.jsonaußerhalb eines erfassten Bundles.titles.PATH_BUDGET), davon belegtraw/2026/10/schon 12. Eine Einzeldatei darf etwa 144 Zeichen Namen haben; im Bundle<stem>/<stem>.pdfsteht der Stamm doppelt, dann bleiben nur etwa 71.raw fetchhält das nur ein, weil es Stämme auf 60 Zeichen kürzt. Ein vom Menschen benannter Dateiname scheitert sonst erst beiraw accept. (Das ist der Grund für die Ordnerform mitoriginal.<ext>, Entscheidung 7.)--replaces-bundlegibt 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--replacesnimmt nur eine Datei direkt inincoming/. Eine neue Ausgabe oder eine Neuableitung eines abgeleiteten Bundles geht damit nicht ohne Handarbeit; das bleibt offen und ist Folge-Issue #201 (Entwurf 11).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 kommenpypdf(BSD-3) undolefile(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.mdmit 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 derivegibt. 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)
pypdfundolefilekommen intools/requirements.txtund werden lazy importiert (Entwurf 8). Verworfen wurde ein optionales Extra wierequirements-mcp.txt: Eine frische Instanz könnte dann kein PDF ableiten, und das Extra findet niemand von selbst..wikitool-filters.jsonnach dem Muster von #169) wurden verworfen, auch nicht als Folge-Issue.incoming/<stem>/, der nach der bestehenden Ordnerregel als Ganzes angenommen wird. „Dateien einzeln“ im Stub heißt, dass jede Datei darin einzeln abgeleitet wird.instructions/ingest-<format>.mdund eine Routing-Tabelle inwiki-ingest: Beides hätte den Agentenkontext für Formate belastet, die gerade gar nicht anstehen.raw derivelegt immer einen Ordner an, mit festen Namen darin (Entwurf 1). Eine einzelne Datei wird zuincoming/<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 alsoriginal_name:im Header. Das Pfadbudget prüft schonraw derive, bevor es etwas schreibt.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 inraw/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 einenraw/-Pfad (raw acceptberechnet ihn, die Seite referenziert ihn überraw_files:), muss die Formen also nicht kennen.Entwurf
Neues Kommando
raw derive incoming/<datei|ordner> [--name <stem>].incoming/wird zum Ordnerincoming/<stem>/. Darin liegenoriginal.<ext>, das Original mit unveränderten Bytes, hineinverschoben, undderived.md.incoming/<stem>.<ext>existiert danach nicht mehr.<stem>ist der Stamm der Datei;--nameersetzt ihn (Prüfung wie beiraw fetch --name). Für Container (ZIP, E-Mail mit Anhängen) gilt Entwurf 5.incoming/, etwa ein von Hand abgelegter: Jede Datei darin mit passendem Filter bekommt<ihr-stamm>.mddaneben. 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.mdschon → exit 1, nichts geschrieben.incoming/exakt wie vorher. Geprüft werden:raw accept: außerhalb vonincoming/→ exit 1; eine einzelne Datei in einem Unterordner → exit 1, mit beiden Auswegen;incoming/<stem>/, der schon existiert → exit 1;raw deriveden späteren Pfad unterraw/<YYYY>/<MM>/mit derselben Funktion wieraw accept(path_budget_problem_for), bei Containern bis zum tiefsten Eintrag. Ist einer zu lang → exit 1 mit dem Pfad und--nameals Ausweg.raw/schon belegt ist, ist keine Verweigerung, sondern ein WARN mit der Meldung vonraw accept: Es könnte eine neue Ausgabe sein, und diese Frage stelltraw accept, nichtraw derive.incoming/und nie inraw/: Promotion und--fidelity/--authoritybleiben beiraw accept(wie #120 Entscheidungen 1 und 13).raw fetchundraw fetch --htmlbleiben unverändert.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 aufweb_capture, für eine HTML-Datei ohne URL, etwa in einem ZIP). Jeder Filter gibt an:nameundengine(mit Version),available()ist (ob der Import gelingt),derive(bytes, name) -> Derivation: Text, Header-Felder, Warnungen undchecks(Entwurf 6).Der Kern kennt weder
typernochrichund wirftValidationError, wieweb_capture. Ein neues Format braucht ein Modul, eine Registrierung und Tests; Kommando, Header und Skill bleiben dabei gleich.Header nach #120 Entscheidung 6 (Werte YAML-gequotet, wo YAML sie sonst nicht unverändert zurückliest). Die Header-Funktion wird aus
web_capturegeteilt, nicht kopiert:derived_fromist der Dateiname im Ordner.original_nameist der Name, unter dem die Datei inincoming/lag; bei einer Datei, die nicht umbenannt wurde (in einem Ordner, als Anhang), fällt das Feld weg.enginenennt die Version, weil „gleiche Bytes ergeben denselben Text“ nur für dieselbe Engine-Version gilt. Ein reiner Standardbibliotheks-Filter schreibt nurwikitool <version>.pages; E-Mailfrom,to,cc,date,subject,message_id,attachments; XLSXsheets,hidden_sheets; PPTXslides; DOCXtracked_changes,comments.Ableitung je Format, deterministisch:
<!-- 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.text/plainbevorzugt, sonst HTML überweb_capture. Bei MSG ist dasPR_BODY, sonstPR_HTML; hat eine MSG nur einen RTF-Body, warnt das Kommando, denn einen RTF-Parser gibt es nicht. Anhänge: Entwurf 5.#; Listen; Tabellen als Markdown-Tabellen. Bei Änderungsverfolgung kommt die angenommene Fassung in den Fließtext (w:insja,w:delnein). Kommentare stehen in einem eigenen Abschnitt am Ende.## Slide N), die Sprechernotizen darunter (### Notes).## <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 inhidden_sheets. Leere Randzeilen und -spalten werden abgeschnitten. Eine Datei ergibt eine.md, nicht eine pro Blatt.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.
raw derive incoming/x.zipentpackt nachincoming/x/, leitet jede Datei darin einzeln ab (<ihr-stamm>.mddaneben) und entferntincoming/x.zip.original.*/derived.mdgibt es hier nicht, weil das ZIP entfällt.incoming/x/mitoriginal.eml/original.msg(unveränderte Bytes),derived.mdund den Anhängen unter ihren eigenen Namen. Jeder Anhang mit Filter bekommt<ihr-stamm>.mddaneben.incoming/x/<zipstem>/, und das ZIP entfällt. Ein ZIP darin → Verweigerung.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 inderived.mdder äußeren E-Mail abgeleitet. Ihre eigenen Anhänge werden nicht ausgepackt; ihre Namen stehen in diesem Abschnitt und als WARN in der Ausgabe.<stamm>.mdund wird nicht umbenannt. Ihre Anhänge werden nicht ausgepackt, sondern genannt, wie oben. Eine E-Mail wird nur auf oberster Ebene zum Container..., Symlink-Eintrag, verschlüsselter Eintrag, mehr als 1000 Einträge, eine Datei über 25 MiB entpackt (wieraw capture), mehr als 250 MiB entpackt insgesamt (Schutz gegen Zip-Bomben).__MACOSX/,.DS_Store, alles mit.am Anfang), dieraw acceptsonst verweigern würde.zipfile). Anhangsnamen werden von Pfadtrennern befreit. Doppelte Namen bekommen-2,-3vor der Endung, und die Ausgabe nennt sie. Dasselbe gilt für einen Namen, der mit einem reservierten kollidiert:original.*undderived.mdauf oberster Ordnerebene,_capture.jsonüberall.Selective Disclosure: Das Formatwissen steht in der Ausgabe, nicht im Kontext. Jeder Filter trägt höchstens drei kurze
CHECK-Zeilen auf Englisch, dieraw derivenach 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:wiki-ingestSchritt 4); Geheimnisse müssen vorraw acceptheraus (raw/CONTRACT.md§ What does not belong here).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.Kontextbudget im Skill: eine Zeile plus ein korrigiertes Beispiel.
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.“raw fetch-Paar ersetzt, denn eine Ableitung ist jetzt ein Ordner und wird als Ganzes angenommen.raw derivekommt in die Kommandoliste.Zwei mechanische Netze sorgen dafür, dass die Zeile nicht erinnert werden muss:
raw pendingnennt bei einem Kandidaten, der eine Datei mit Filter direkt inincoming/ist, dieraw derive-Zeile.raw acceptgibt 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.Abhängigkeiten werden lazy importiert, im Filtermodul und nie auf Modulebene von
cli.py. Fehlt ein Paket (venv ohne neuen Preflight), endet nurraw derivefür dieses Format mit exit 1 und dem Preflight-Hinweis; alle anderen Kommandos laufen.doctormeldet je Filter verfügbar oder nicht verfügbar, ohne zu FAILen.raw/CONTRACT.mdbekommt einen kurzen Abschnitt „Getting a document in:raw derive“ nach § Getting a URL in, nur mit den Regeln: die Ordnerform mitoriginal.<ext>/derived.mdund 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 vonraw pending) wird angepasst; dasselbe Beispiel im Kommando-Record vonraw pendingzog das Schließen nach (§ Stand, Abschluss).Nicht Teil dieses Pakets: OCR, externe/instanzeigene Filter (Entscheidung 2), ein MCP-Tool für die Ableitung, weitere Formate (ODT, RTF, Bilder).
Neue Ausgabe und Neuableitung eines abgeleiteten Bundles - nicht Teil dieses Pakets, Folge-Issue #201.
--replacesnimmt nur Dateien direkt inincoming/. Dasselbe Dokument in einer neuen Fassung (Angebot v2) trifft beiraw acceptauf den belegten Namen, und die Meldung bietet nur Umbenennen oder ein Ersetzen von Hand an.raw deriveund der ON FAILURE-Text nennen diesen Weg ehrlich, statt--replaceszu versprechen, das so nicht greift.--replaces-bundleauch für abgeleitete Bundles, analog zu erfassten.Betroffene Dateien
tools/chemenu/filters/__init__.py(Registry,Derivation, Header,checks),pdf.py,mail.py,msg.py,ooxml.py,archive.py,html_page.pytools/chemenu/web_capture.py: Header-Rendering (header(),_scalar) teilen statt kopierentools/chemenu/commands/raw_cmd.py:raw derivesamt Kommando-Record (Synopsis,network: no, Exit-Status, ON FAILURE, NEVER)path_budget_problem_for/_occupied_stems, nicht als Kopieraw pendingund das WARN inraw accepttools/chemenu/cli_contract.py:raw deriveinGROUPS(„Raw material and uploads“)tools/chemenu/commands/doctor.py: Verfügbarkeit der Filtertools/requirements.txt:pypdf,olefiletools/CONTRACT.md,docs/reference/commands/raw-material-and-uploads.md,docs/reference/commands/instance-health.mdtools/chemenu/tests/test_raw_derive.py,test_raw_derive_binary.pyund der Fixture-Baukastenbinary_formats.py(siehe Abweichungen).raw/CONTRACT.md: Entwurf 9instructions/wiki-ingest/SKILL.md: Entwurf 7tools/README.md(Modulliste),docs/how-to/ingest-a-source.md, sonst wasinstructions/dev/doc-pull-through.mdbeim Bau nenntCHANGES.mdüberversion bumpAkzeptanzkriterien
raw derive incoming/x.<ext>mit exit 0 genauincoming/x/original.<ext>(byte-identisch zur Eingabe) undincoming/x/derived.md.incoming/x.<ext>existiert danach nicht mehr,raw/ist unverändert, undraw accept incoming/xnimmt den Ordner ohne Verweigerung an.derived.md, abgesehen vonderived:.derived.mdist gültiges YAML mitderived_by,filter,engine(samt Version),derived,derived_from: original.<ext>undoriginal_name(dem ursprünglichen Dateinamen).raw/<YYYY>/<MM>/<stem>/original.<ext>über 160 Zeichen läge, endet mit exit 1, nennt den Pfad und--name, undincoming/ist unverändert. Mit--name <kurz>läuft derselbe Aufruf durch. Ein Stamm von 120 Zeichen läuft ohne--namedurch.raw/schon belegt ist, ergibt exit 0 mit WARN; der Ordner wird geschrieben.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.mdschon → exit 1, nichts geschrieben.hidden_sheets, Formelzellen zeigen ihren gespeicherten Wert.<!-- page N -->-Marker. Ein PDF ohne Textschicht ergibt exit 0 mit der Scan-Warnung.incoming/oder eine einzelne Datei in einem Unterordner → exit 1, nichts geschrieben.raw derive incoming/x.zipmit drei Dokumenten ergibtincoming/x/mit den drei Dateien unter ihren Namen, byte-identisch zu den Einträgen, und je einer<stamm>.mddazu.incoming/x.zipexistiert danach nicht mehr, und die Ausgabe nennt seinen Namen und sha256.raw accept incoming/xnimmt den Ordner an.incoming/x/mitoriginal.eml(byte-identisch),derived.md, dem PDF samt.mdundincoming/x/<zipstem>/mit beiden Dokumenten samt.md; das ZIP existiert nicht mehr.raw accept incoming/xnimmt den Ordner an.incoming/ist danach exakt wie vorher.## Attached message: …inderived.md, der Name ihres Anhangs steht dort und als WARN in der Ausgabe.<stamm>.md; ihre Anhänge werden nicht ausgepackt und als WARN genannt.<name>und<name>-2; ein Anhang namensderived.mdlandet alsderived-2.md, einer namens_capture.jsonals_capture-2.json. Die Ausgabe nennt alle drei...-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_Storein 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.raw derivefür dieses Format mit exit 1 und dem Preflight-Hinweis.raw pendingunddoctorlaufen unverändert;doctormeldet den Filter als nicht verfügbar, ohne zu FAILen.raw pendingnennt bei einer Datei mit Filter direkt inincoming/die passenderaw derive-Zeile.raw acceptderselben Datei allein endet mit exit 0, promotet sie wie bisher nachraw/<YYYY>/<MM>/<name>und gibt ein WARN mit derselben Zeile aus.incoming/quelle.mdlandet wie bisher ohne Ordner alsraw/<YYYY>/<MM>/quelle.md(Entscheidung 8; die bestehenden Tests bleiben unverändert grün).raw fetchundraw fetch --htmlerzeugen byte-identische Ausgaben wie vorher; die bestehenden Tests bleiben unverändert grün.instructions/*.mdentsteht, und der Diff voninstructions/wiki-ingest/SKILL.mdbeschränkt sich auf Entwurf 7 (Zeile in Schritt 1, Beispiel in Schritt 5, Kommandoliste).pytest,docs verifyundinstructions verifysind grün; CI ist grün auf dem letzten publizierten Commit (8ea2795, Läufe 623 und 624; davor Schnitt 30e6a50a, Läufe 621 und 622; Schnitte 1 und 2: Läufe 617 bis 620).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):
raw derivemit Ordnerform, Budget- und Namensprüfung,--name, Header,CHECK-Mechanik, DOCX/PPTX/XLSX und EML ohne Anhänge (nur Standardbibliothek), die Skill-Änderung, die Hinweise inraw pendingundraw accept, der Abschnitt inraw/CONTRACT.md.requirements.txtunddoctor-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 inraw acceptändert weder den Exit-Code noch das Verhalten, und die Bundle-Regel vonraw acceptbleibt unverändert (Entscheidung 8).Die neuen Pakete in
requirements.txtbrechen 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, undupgrade-instancelä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, jetztbeta.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 nachraw/, 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 inraw/liegt.size/L: drei Schnitte, mehrere Sitzungen.Bezug
raw fetch, das Muster für Header, Determinismus, exklusives Schreiben und--nameincoming/als Warteschlange, die OrdnerregelStub, wörtlich (vor der Ausarbeitung):
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. Labelsarea/kb,kind/decision,prio/planned,size/L;status/incomingentfernt.Import Filterto Import-Filter: `raw derive` leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, mit Format-InstructionsImport-Filter: `raw derive` leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, mit Format-Instructionsto Import-Filter: `raw derive` leitet Text aus PDF, E-Mail, Office-Dateien und ZIP ab - pluggable, Formatwissen in der WerkzeugausgabeChangelog: 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 inCHECK-Zeilen der Ausgabe vonraw derive, im Skill ändert sich eine Zeile, undraw pendingundraw 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: Pfadbudget behoben. Befund: Im Bundle
<stem>/<stem>.extsteht der Stamm doppelt, dadurch bleiben nur etwa 71 statt etwa 144 Zeichen. Entscheidung 7:raw derivelegt immerincoming/<stem>/mitoriginal.<ext>+derived.mdan, der ursprüngliche Name steht alsoriginal_name:im Header, und Budget wie Namensbelegung prüft schonraw derive(Budget → exit 1 mit--nameals 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--replacesund kommt als Folge-Issue beim Schließen. Akzeptanzkriterien entsprechend angepasst.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 infilters/msg.pystatt inmail.py; eine eingebettete Nachricht in einer MSG wird nur genannt, ein Inline-Bild nur gezählt;_leaves()inmail.pyhä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: Body auf Endstand gebracht; Pull-through im Schließen: das Bündel-Beispiel „PDF und konvertierter Text“ im Kommando-Record von
raw pendingersetzt (9.0.0-beta.17,8ea2795, Läufe 623 und 624 grün). Geschlossen.torben referenced this issue2026-10-09 21:40:56 +00:00