Ausgearbeitet aus einem status/incoming-Stub am 2026-10-02 (Stand 8.0.0-beta.24, c0f324f). Der Stub steht wörtlich im ersten Kommentar.
Abgeschlossen 2026-10-02. Gebaut und publiziert in 8.0.0-beta.25, Commit fba263a, verifiziert durch CI-Runs 502 und 503. Nachzug aus der Abschlussprüfung in 8.0.0-beta.26, Commit c4dcff7, verifiziert durch CI-Runs 504 und 505. Alle vier Runs grün.
Befund (gegen den Baum geprüft, Stand vor dem Bau)
Für "Nutzer nennt eine URL, die ingested werden soll" gab es weder ein Tool noch eine Prozedur:
raw accept (tools/chemenu/commands/raw_cmd.py) nimmt nur existierende Dateien unter incoming/.
raw/CONTRACT.md kannte zwei Eingänge - incoming/ (lokale Datei, § Getting a file in) und mcp-upload/ (MCP-submit) -, keinen für eine URL.
instructions/wiki-ingest/SKILL.md Schritt 1 setzte eine Datei in incoming/ oder raw/ voraus; der einzige URL-Bezug war --set source_url=<upstream URL> auf der Source-Seite (Schritt 6, types/source.md).
Eine Session baute sich deshalb jedes Mal ihre eigene Kette (curl, Zeichensatz raten, lynx/pandoc je nach Verfügbarkeit, Boilerplate per Zeilennummer schneiden, Header frei erfinden). Zwei Sessions erzeugten aus demselben Artikel verschiedene raw/-Dateien - genau das, was der Intake durch Werkzeug ausschließen soll. Der Harness-eigene Web-Fetch scheidet aus: Er liefert eine LLM-Zusammenfassung, keine Erfassung nahe am Original.
Entscheidungen (alle umgesetzt)
Ein Tool, keine reine Prozedur.wikitool raw fetch <url> lädt, behandelt den Zeichensatz, leitet Text ab und schreibt nach incoming/. Danach läuft raw accept unverändert - der Fetch landet nicht direkt in raw/, damit die Reihenfolge aus wiki-ingest (Verpflichtungsfrage vor raw accept) erhalten bleibt und --fidelity/--authority weiter genau dort erfragt werden.
Die Rohquelle ist ein Bundle aus HTML und abgeleitetem Text (vom Betreiber entschieden 2026-10-02). raw fetch schreibt incoming/<stem>.html - die Bytes exakt wie empfangen - und incoming/<stem>.md (Header + abgeleiteter Text). raw accept nimmt beide in einem Aufruf als Bundle (raw/<YYYY>/<MM>/<stem>/), beide stehen in raw_files:. Begründung: raw/CONTRACT.md verlangt "kept exactly as received", und empfangen wurde das HTML; der Text ist eine Ableitung des Tools. Nur mit dem HTML bleibt eine Behauptung in kb/ byte-genau gegen das Original prüfbar, auch wenn die Ableitung Inhalt verliert oder später verbessert wird. Gelesen und zitiert wird die .md. Verworfen: nur den Text ablegen - kleiner, aber was die Ableitung verwirft, wäre unwiederbringlich, und der Contract bräuchte eine Ausnahme von seinem Qualitätsziel.
Nur Standardbibliothek (urllib.request, html.parser, email.message für den Content-Type). Präzedenz: version.py lädt bereits per urllib. Begründung: Eine neue Abhängigkeit in tools/requirements.txt fällt durch die Vorwärtshälfte des Drop-in-Tests (instructions/dev/version-parts.md) - nach dem Kopieren von tools/ fehlt sie im venv, bis jemand pip install ausführt. Die schwächere Boilerplate-Entfernung ist hinnehmbar, weil der abgeleitete Text nicht der Beleg ist (Entscheidung 2).
Zeichensatz, deterministisch in dieser Reihenfolge: BOM -> charset im HTTP-Content-Type -> <meta charset> / <meta http-equiv> in den ersten 4 KiB -> UTF-8. Ein Label, das Python nicht kennt, wird wie ein fehlendes übersprungen. Undekodierbare Bytes werden ersetzt, nicht verworfen, und der Fall wird in der Ausgabe gemeldet. Die verwendete Kodierung steht im Header (Entscheidung 6). Bei --html (Entscheidung 14) entfällt die HTTP-Stufe.
Textableitung (HTML -> Markdown-naher Text), deterministisch: Inhaltswurzel ist <main>, sonst genau ein <article>, sonst <body>. Darunter fallen script, style, noscript, nav, header, footer, aside, form, template, svg weg. Überschriften -> #, Listen -> -/1., pre/code -> Codeblock/Backticks, Links -> [text](absolute-url) (relativ aufgelöst gegen final_url bzw. --url), Absätze durch Leerzeile. Keine Heuristik über Zeilennummern oder Textdichte. Gleiche Bytes ergeben dieselbe Ausgabe. Im Bau ergänzt, ohne die Regel zu ändern: Tabellen -> Markdown-Tabelle, blockquote -> >, Bilder ->  (data:-URIs nur als Alt-Text), Emphasis wird nicht ausgezeichnet. Folge der Regel, bewusst so gelassen: ein <header>innerhalb von <main>/<article> (oft mit dem Artikel-<h1>) fällt ebenfalls weg - der Titel steht dann nur in title:, das HTML behält ihn.
Header-Format - ein fester Block am Anfang der abgeleiteten.md-Datei, vom Tool geschrieben, nie von Hand. Die .html bleibt unberührt.
Im Bau geändert: Werte werden YAML-gequotet, wo YAML sie sonst nicht unverändert zurückliest (JSON-String-Form). Das betrifft immer charset (: in (from: ...)) und z.B. einen Titel mit : oder #. Grund: Die ----Zäune weisen den Block als Frontmatter aus; ungültiges YAML darin bricht jeden Viewer, der ihn liest. Inhaltlich bleibt das Format gleich.
Bei --html (Entscheidung 14): fetched_by: wikitool raw fetch --html, retrieved/final_url/http_status/content_type entfallen, dafür derived: <ISO-8601, UTC> (Zeitpunkt der Ableitung - wann der Mensch die Seite gespeichert hat, weiß das Tool nicht und behauptet es nicht).
Die Felder sind Erfassungsmetadaten, keine Seiten-Frontmatter: raw/ hat keine Typen (raw/CONTRACT.md Abs. 3), nichts im Stack liest diesen Block maschinell. author: wird bewusst nicht geführt - der Stub nannte ihn, aber aus HTML ist er nicht verlässlich bestimmbar; ein geratener Autor wäre eine Behauptung über die Quelle. Er gehört auf die Source-Seite, wenn die Quelle ihn trägt.
Dateiname: Stamm aus dem letzten Pfadsegment der URL (ohne Endung), sonst aus dem Hostnamen, sluggifiziert (ASCII, Kleinbuchstaben, -), auf 60 Zeichen gekürzt; --name <stem> überschreibt (wörtlich übernommen; Pfadtrenner und leer -> exit 1). Existiert in incoming/ schon <stem>.html oder <stem>.md, verweigert der Befehl (exit 1) - kein Überschreiben, keine Suffixe; die Dateien werden zusätzlich exklusiv (x-Modus) angelegt. Bei --html ist der Stamm der der übergebenen Datei, die .md entsteht neben ihr. Die Pfadbudget- und Stammkollisionsprüfung gegen raw/ bleibt raw accepts Sache; raw fetch dupliziert sie nicht.
Nicht-HTML-Antworten:text/plain, text/markdown -> Bytes unverändert als .txt/.md, ohne Ableitung. Alles andere (PDF, Bilder, ...) -> Bytes unverändert mit Endung aus dem Content-Type (Pythons eingebaute Tabelle, nicht /etc/mime.types; unbekannt oder fehlend -> .bin), ohne Ableitung; die Ausgabe sagt das. Ein Header entfällt dann (er hätte keine Datei, in die er gehört, ohne die Bytes zu verändern); url/retrieved stehen in der Erfolgsmeldung und gehen über source_url auf die Source-Seite.
Grenzen: nur http/https, auch auf jedem Redirect-Schritt (file:, ftp: u.a. -> exit 1 - ein file:-Fetch wäre ein Weg an incoming/ vorbei). Timeout 30 s für den ganzen Transfer, Größenlimit 25 MiB (Abbruch beim Überschreiten, nichts geschrieben). Eigener User-Agent mit Versionsnummer (chemenu-wikitool/<version> (raw fetch)). Keine Cookies, kein JavaScript: Eine clientseitig gerenderte Seite ergibt fast leeren Text - unter 200 Zeichen abgeleitetem Text warnt der Befehl ausdrücklich, schreibt aber trotzdem; ob die Erfassung taugt, entscheidet der Mensch.
Erfolgsmeldung nennt die geschriebenen Dateien, die nächste Zeile und den Paywall-Hinweis:
OK Fetched https://example.org/post to incoming/post.html, incoming/post.md
Next (after the commitment question in wiki-ingest):
tools/wikitool raw accept --fidelity published --authority <value> incoming/post.html incoming/post.md
and on the source page: --set source_url=https://example.org/post
If the text stops at a teaser (paywall, login, script-rendered page): save the page from a
logged-in browser to incoming/ and run tools/wikitool raw fetch --html incoming/<file>.html --url <url>
Invariante 4 bleibt unberührt:raw fetch wird nur auf eine URL angewandt, die der Nutzer selbst nennt - nie auf eine URL, die in einer Rohquelle steht. Das steht in raw/CONTRACT.md, in wiki-ingest und im NEVER-Teil des Kommando-Records.
Nicht über MCP. Der MCP-Server bekommt kein Fetch-Tool: Ein Server, der beliebige URLs im Auftrag eines entfernten Aufrufers lädt, ist ein SSRF-Vektor. Der entfernte Weg bleibt submit -> mcp-upload/. Die Begründung steht in raw/CONTRACT.md § mcp-upload.
Kein --fidelity/--authority bei raw fetch. Die Werte gehören zu raw accept (dort sind sie Pflicht); sie zweimal zu erfragen wäre eine zweite Stelle für dieselbe Entscheidung.
Paywall und Login: kein Login im Tool, stattdessen --html (vom Betreiber entschieden 2026-10-02).
raw fetch --html incoming/<stem>.html --url <url> leitet aus einer HTML-Datei, die der Mensch aus seinem eingeloggten Browser nach incoming/ gespeichert hat ("Seite speichern unter", nur HTML), dieselbe .md ab (Header siehe Entscheidung 6). Kein Netzzugriff. Die .html bleibt byte-unverändert; das Bundle ist dasselbe wie in Entscheidung 2. Deckt Paywall und clientseitig gerenderte Seiten gleichermaßen ab. --html außerhalb von incoming/ -> exit 1 (dieselbe Prüfung wie bei raw accept). --url ist Pflicht: Sie steht im Header, löst relative Links auf und geht als source_url auf die Source-Seite.
Keine Zugangsdaten, Cookies oder Login-Adapter in wikitool. Das Tool geht an jede Instanz; Zugangsdaten im Arbeitsbaum widersprechen raw/CONTRACT.md § What does not belong here, und ein Adapter pro Paywall-Site ist Wartung, die kein Wissenskompiler tragen soll. Ein Auth-Dienst entsteht außerhalb des Stacks (#87); die Andockstelle dafür ist #169, nicht dieses Issue.
Teaser-Prüfung ist ein Urteil in wiki-ingest, nicht im Tool: Ein Teaser ist allgemein nicht erkennbar und hat oft mehr als die 200 Zeichen aus Entscheidung 9. Schritt 1 liest die Quelle ohnehin vollständig; bricht der Text erkennbar ab ("Weiterlesen mit ...", Abo-Hinweis, Login-Aufforderung), stoppt die Session und schlägt dem Nutzer den --html-Weg vor, statt einen Teaser als Quelle zu ingesten.
Betroffene Dateien
Bau (fba263a):
tools/chemenu/web_capture.py (neu) - Fetch, Zeichensatz, Ableitung, Header, Stamm; Kern ohne typer/rich, wirft ValidationError/BackendError, damit #169 dort andocken kann.
tools/chemenu/commands/raw_cmd.py - neues Kommando raw fetch samt Kommando-Record (Synopsis für beide Formen, network: yes, Exit-Status, ON FAILURE, NEVER).
tools/chemenu/cli_contract.py - raw fetch in GROUPS ("Raw material and uploads"); ohne den Eintrag rendert docs contract das Kommando nicht.
tools/CONTRACT.md - aus dem Record generiert (nicht von Hand).
tools/chemenu/tests/test_raw_fetch.py (neu, 36 Tests) - gegen einen lokalen http.server (Präzedenz: test_dist_upgrade.py), kein echtes Netz.
tools/chemenu/tests/test_cli.py - die gepinnte network: yes-Menge um raw fetch ergänzt.
tools/chemenu/tests/test_portability.py - opener (urllibs OpenerDirector) in NOT_TEXT_OPEN: Der AST-Scan las opener.open(request, ...) als Text-Datei-Open ohne encoding=.
raw/CONTRACT.md - neuer Abschnitt "Getting a URL in: raw fetch" zwischen incoming/ und mcp-upload/: Bundle-Regel, Header-Format, --html und Paywall-Regel, Invariante-4-Satz; im mcp-upload/-Abschnitt der SSRF-Satz (Entscheidung 12).
instructions/wiki-ingest/SKILL.md - description/Trigger um "ingest "; Schritt 1: URL -> raw fetch, Teaser-Prüfung mit --html; Kommandoliste.
tools/README.md (Modulliste: web_capture.py), README.md (§ Adding Knowledge: URL-Ingest und --html).
CHANGES.md über version bump (Changeset "raw fetch: a sanctioned intake for a URL into incoming/ (#120)", Impact medium).
Abschlussprüfung (c4dcff7):
kb/CONTRACT.md § Provenance: Der Satz, die lokale Kopie eines externen Artikels liege "under raw/articles/", war seit dem Datums-Shard falsch und hätte genau den URL-Pfad fehlgeleitet. Jetzt: unter raw/, bei einem raw fetch mit .html und .md.
instructions/wiki-ingest/SKILL.md - eine URL als zweites Beispiel-Trigger.
CHANGES.md über version bump --patch (Impact low).
Geprüft und unverändert gelassen: docs/pipeline-rationale.md (das Argument "incoming/ hält, was ein Mensch gewählt hat; die Grenze ist die Promotion nach raw/" gilt für raw fetch unverändert, die Begründung des Änderungsinhalts trägt raw/CONTRACT.md), types/source.guidance.md, INSTALL.md, INSTALL-MCP.md, instructions/ingest-queue.md, instructions/mcp-read-server.md. Keine Installationsinstruktion berührt.
Akzeptanzkriterien
tools/wikitool raw fetch <url> gegen eine lokale HTML-Testseite schreibt incoming/<stem>.html und incoming/<stem>.md und gibt exit 0; raw/ ist danach unverändert. (test_fetch_writes_html_and_md_to_incoming_and_leaves_raw_untouched)
Die .html in incoming/ ist byte-identisch mit dem, was der Server ausgeliefert hat. (test_the_html_is_byte_identical_to_what_the_server_sent, inkl. CRLF und Nicht-ASCII-Bytes)
Zwei Läufe gegen dieselben Server-Bytes ergeben byte-identischen abgeleiteten Text (bis auf retrieved:). (test_two_fetches_of_the_same_bytes_derive_identical_text)
Eine als ISO-8859-1 ausgelieferte Seite (Charset nur im <meta>) ergibt korrekt dekodierte Umlaute, und der Header nennt charset: "iso-8859-1 (from: meta)" (gequotet, Entscheidung 6). (test_latin1_declared_only_in_meta_is_decoded)
Inhalt in nav/footer/aside/script einer Testseite erscheint nicht im abgeleiteten Text; Inhalt in <main> erscheint vollständig. (test_boilerplate_is_dropped_and_main_is_kept_whole)
raw accept mit beiden Dateien aus der Erfolgsmeldung legt das Bundle unter raw/<YYYY>/<MM>/<stem>/ an. (test_the_bundle_is_accepted_into_raw_as_one_source)
Existiert incoming/<stem>.html oder .md bereits, endet der Befehl mit exit 1 und die vorhandene Datei ist unverändert. (test_an_existing_target_is_never_overwritten, beide Fälle)
file:///etc/passwd, eine Antwort über 25 MiB (mit und ohne Content-Length) und ein Timeout enden je mit exit 1 und hinterlassen keine Datei in incoming/. (test_a_file_url_…, test_a_response_over_the_size_limit_…, test_a_slow_server_times_out; zusätzlich Redirect auf ftp: und HTTP 404)
Eine application/pdf-Antwort wird byte-identisch als .pdf abgelegt, ohne abgeleitete Datei. (test_a_pdf_is_stored_byte_identical_without_a_derivation)
raw fetch --html incoming/<stem>.html --url <url> schreibt nur incoming/<stem>.md, macht keinen Netzzugriff, die .html bleibt byte-identisch, und der abgeleitete Text gleicht dem eines Online-Abrufs derselben Bytes (bis auf den Header). (test_html_derives_only_the_md_without_network_and_matches_a_fetch - Netz per Monkeypatch gesperrt)
--html mit einem Pfad außerhalb von incoming/ oder ohne --url endet mit exit 1, ohne etwas zu schreiben. (test_html_outside_incoming_is_refused, test_html_without_url_is_refused)
Der MCP-Server bietet kein Fetch-Tool an (Tool-Liste unverändert - gehalten von test_mcp_server.py::test_the_four_tools_are_there_and_nothing_that_writes, das die Menge exakt pinnt).
raw/CONTRACT.md dokumentiert Bundle-Regel, Header-Format, --html und Paywall-Regel; wiki-ingest führt eine URL über raw fetch und enthält die Teaser-Prüfung.
pytest, docs verify, instructions verify grün; CI grün auf dem publizierten Commit. - Lokal: volle Suite hermetisch (env -i) 2095 passed, 3 skipped; docs verify, instructions verify OK. CI: Runs 502/503 auf fba263a, 504/505 auf c4dcff7, alle success.
Versionsteil
--minor - neues Kommando, Drop-in in beide Richtungen: kein Flag/Kommando entfernt oder umbenannt, keine neue Abhängigkeit (Entscheidung 3), kein maschinell gelesenes Dateiformat geändert, keine Content-Migration; ein Downgrade verliert nur raw fetch. Der laufende Kandidat stand bereits auf MAJOR; der Bump hob ihn auf 8.0.0-beta.25 (fba263a), der Patch-Bump der Abschlussprüfung auf 8.0.0-beta.26 (c4dcff7).
Bezug
#169 - externer Abrufbefehl pro Instanz (.wikitool-fetch.json), die Andockstelle für einen Auth-Dienst; baut auf diesem Issue auf, prio/waiting. Andockpunkt ist jetzt tools/chemenu/web_capture.py.
#87 - Notizen zur Infrastruktur außerhalb des Stacks (RSS-Poll, Auth-Dienst), wandert später in ein eigenes Repo. Dieses Issue übernimmt von dort nichts außer der Erkenntnis, dass Paywall-Inhalt nicht über das Tool selbst kommt.
Labels
area/kb (Intake in raw/ ist Pipeline/Provenienz), kind/build, prio/planned - bis zu diesem Bau erzeugte jeder URL-Ingest eine ad hoc geformte raw/-Datei, die unveränderlich bleibt. size/M.
> Ausgearbeitet aus einem `status/incoming`-Stub am 2026-10-02 (Stand `8.0.0-beta.24`, `c0f324f`). Der Stub steht wörtlich im ersten Kommentar.
>
> **Abgeschlossen 2026-10-02.** Gebaut und publiziert in `8.0.0-beta.25`, Commit `fba263a`, verifiziert durch CI-Runs [502](https://gitea.nehmer.net/torben/chemenu/actions/runs/502) und [503](https://gitea.nehmer.net/torben/chemenu/actions/runs/503). Nachzug aus der Abschlussprüfung in `8.0.0-beta.26`, Commit `c4dcff7`, verifiziert durch CI-Runs [504](https://gitea.nehmer.net/torben/chemenu/actions/runs/504) und [505](https://gitea.nehmer.net/torben/chemenu/actions/runs/505). Alle vier Runs grün.
## Befund (gegen den Baum geprüft, Stand vor dem Bau)
Für "Nutzer nennt eine URL, die ingested werden soll" gab es weder ein Tool noch eine Prozedur:
- `raw accept` (`tools/chemenu/commands/raw_cmd.py`) nimmt nur existierende Dateien unter `incoming/`.
- `raw/CONTRACT.md` kannte zwei Eingänge - `incoming/` (lokale Datei, § Getting a file in) und `mcp-upload/` (MCP-`submit`) -, keinen für eine URL.
- `instructions/wiki-ingest/SKILL.md` Schritt 1 setzte eine Datei in `incoming/` oder `raw/` voraus; der einzige URL-Bezug war `--set source_url=<upstream URL>` auf der Source-Seite (Schritt 6, `types/source.md`).
Eine Session baute sich deshalb jedes Mal ihre eigene Kette (curl, Zeichensatz raten, lynx/pandoc je nach Verfügbarkeit, Boilerplate per Zeilennummer schneiden, Header frei erfinden). Zwei Sessions erzeugten aus demselben Artikel verschiedene `raw/`-Dateien - genau das, was der Intake durch Werkzeug ausschließen soll. Der Harness-eigene Web-Fetch scheidet aus: Er liefert eine LLM-Zusammenfassung, keine Erfassung nahe am Original.
## Entscheidungen (alle umgesetzt)
1. **Ein Tool, keine reine Prozedur.** `wikitool raw fetch <url>` lädt, behandelt den Zeichensatz, leitet Text ab und schreibt nach `incoming/`. Danach läuft `raw accept` unverändert - der Fetch landet **nicht** direkt in `raw/`, damit die Reihenfolge aus `wiki-ingest` (Verpflichtungsfrage vor `raw accept`) erhalten bleibt und `--fidelity`/`--authority` weiter genau dort erfragt werden.
2. **Die Rohquelle ist ein Bundle aus HTML und abgeleitetem Text** (vom Betreiber entschieden 2026-10-02). `raw fetch` schreibt `incoming/<stem>.html` - die Bytes exakt wie empfangen - und `incoming/<stem>.md` (Header + abgeleiteter Text). `raw accept` nimmt beide in einem Aufruf als Bundle (`raw/<YYYY>/<MM>/<stem>/`), beide stehen in `raw_files:`. Begründung: `raw/CONTRACT.md` verlangt "kept exactly as received", und empfangen wurde das HTML; der Text ist eine Ableitung des Tools. Nur mit dem HTML bleibt eine Behauptung in `kb/` byte-genau gegen das Original prüfbar, auch wenn die Ableitung Inhalt verliert oder später verbessert wird. Gelesen und zitiert wird die `.md`. Verworfen: nur den Text ablegen - kleiner, aber was die Ableitung verwirft, wäre unwiederbringlich, und der Contract bräuchte eine Ausnahme von seinem Qualitätsziel.
3. **Nur Standardbibliothek** (`urllib.request`, `html.parser`, `email.message` für den Content-Type). Präzedenz: `version.py` lädt bereits per `urllib`. Begründung: Eine neue Abhängigkeit in `tools/requirements.txt` fällt durch die Vorwärtshälfte des Drop-in-Tests (`instructions/dev/version-parts.md`) - nach dem Kopieren von `tools/` fehlt sie im venv, bis jemand `pip install` ausführt. Die schwächere Boilerplate-Entfernung ist hinnehmbar, weil der abgeleitete Text nicht der Beleg ist (Entscheidung 2).
4. **Zeichensatz, deterministisch in dieser Reihenfolge:** BOM -> `charset` im HTTP-`Content-Type` -> `<meta charset>` / `<meta http-equiv>` in den ersten 4 KiB -> UTF-8. Ein Label, das Python nicht kennt, wird wie ein fehlendes übersprungen. Undekodierbare Bytes werden ersetzt, nicht verworfen, und der Fall wird in der Ausgabe gemeldet. Die verwendete Kodierung steht im Header (Entscheidung 6). Bei `--html` (Entscheidung 14) entfällt die HTTP-Stufe.
5. **Textableitung (HTML -> Markdown-naher Text), deterministisch:** Inhaltswurzel ist `<main>`, sonst genau ein `<article>`, sonst `<body>`. Darunter fallen `script`, `style`, `noscript`, `nav`, `header`, `footer`, `aside`, `form`, `template`, `svg` weg. Überschriften -> `#`, Listen -> `-`/`1.`, `pre`/`code` -> Codeblock/Backticks, Links -> `[text](absolute-url)` (relativ aufgelöst gegen `final_url` bzw. `--url`), Absätze durch Leerzeile. Keine Heuristik über Zeilennummern oder Textdichte. Gleiche Bytes ergeben dieselbe Ausgabe. Im Bau ergänzt, ohne die Regel zu ändern: Tabellen -> Markdown-Tabelle, `blockquote` -> `>`, Bilder -> `` (`data:`-URIs nur als Alt-Text), Emphasis wird nicht ausgezeichnet. Folge der Regel, bewusst so gelassen: ein `<header>` *innerhalb* von `<main>`/`<article>` (oft mit dem Artikel-`<h1>`) fällt ebenfalls weg - der Titel steht dann nur in `title:`, das HTML behält ihn.
6. **Header-Format** - ein fester Block am Anfang der *abgeleiteten* `.md`-Datei, vom Tool geschrieben, nie von Hand. Die `.html` bleibt unberührt.
```
---
fetched_by: wikitool raw fetch
url: <wie angefordert>
final_url: <nach Redirects>
retrieved: <ISO-8601, UTC>
http_status: 200
content_type: text/html; charset=utf-8
charset: "utf-8 (from: header|meta|bom|default)"
title: <aus <title>, sonst leer>
derived_from: <stem>.html
---
```
**Im Bau geändert:** Werte werden YAML-gequotet, wo YAML sie sonst nicht unverändert zurückliest (JSON-String-Form). Das betrifft immer `charset` (`: ` in `(from: ...)`) und z.B. einen Titel mit `: ` oder ` #`. Grund: Die `---`-Zäune weisen den Block als Frontmatter aus; ungültiges YAML darin bricht jeden Viewer, der ihn liest. Inhaltlich bleibt das Format gleich.
Bei `--html` (Entscheidung 14): `fetched_by: wikitool raw fetch --html`, `retrieved`/`final_url`/`http_status`/`content_type` entfallen, dafür `derived: <ISO-8601, UTC>` (Zeitpunkt der Ableitung - wann der Mensch die Seite gespeichert hat, weiß das Tool nicht und behauptet es nicht).
Die Felder sind Erfassungsmetadaten, keine Seiten-Frontmatter: `raw/` hat keine Typen (`raw/CONTRACT.md` Abs. 3), nichts im Stack liest diesen Block maschinell. `author:` wird bewusst **nicht** geführt - der Stub nannte ihn, aber aus HTML ist er nicht verlässlich bestimmbar; ein geratener Autor wäre eine Behauptung über die Quelle. Er gehört auf die Source-Seite, wenn die Quelle ihn trägt.
7. **Dateiname:** Stamm aus dem letzten Pfadsegment der URL (ohne Endung), sonst aus dem Hostnamen, sluggifiziert (ASCII, Kleinbuchstaben, `-`), auf 60 Zeichen gekürzt; `--name <stem>` überschreibt (wörtlich übernommen; Pfadtrenner und leer -> exit 1). Existiert in `incoming/` schon `<stem>.html` oder `<stem>.md`, verweigert der Befehl (exit 1) - kein Überschreiben, keine Suffixe; die Dateien werden zusätzlich exklusiv (`x`-Modus) angelegt. Bei `--html` ist der Stamm der der übergebenen Datei, die `.md` entsteht neben ihr. Die Pfadbudget- und Stammkollisionsprüfung gegen `raw/` bleibt `raw accept`s Sache; `raw fetch` dupliziert sie nicht.
8. **Nicht-HTML-Antworten:** `text/plain`, `text/markdown` -> Bytes unverändert als `.txt`/`.md`, ohne Ableitung. Alles andere (PDF, Bilder, ...) -> Bytes unverändert mit Endung aus dem Content-Type (Pythons eingebaute Tabelle, nicht `/etc/mime.types`; unbekannt oder fehlend -> `.bin`), ohne Ableitung; die Ausgabe sagt das. Ein Header entfällt dann (er hätte keine Datei, in die er gehört, ohne die Bytes zu verändern); `url`/`retrieved` stehen in der Erfolgsmeldung und gehen über `source_url` auf die Source-Seite.
9. **Grenzen:** nur `http`/`https`, auch auf jedem Redirect-Schritt (`file:`, `ftp:` u.a. -> exit 1 - ein `file:`-Fetch wäre ein Weg an `incoming/` vorbei). Timeout 30 s für den ganzen Transfer, Größenlimit 25 MiB (Abbruch beim Überschreiten, nichts geschrieben). Eigener `User-Agent` mit Versionsnummer (`chemenu-wikitool/<version> (raw fetch)`). Keine Cookies, kein JavaScript: Eine clientseitig gerenderte Seite ergibt fast leeren Text - unter 200 Zeichen abgeleitetem Text warnt der Befehl ausdrücklich, schreibt aber trotzdem; ob die Erfassung taugt, entscheidet der Mensch.
10. **Erfolgsmeldung** nennt die geschriebenen Dateien, die nächste Zeile und den Paywall-Hinweis:
```
OK Fetched https://example.org/post to incoming/post.html, incoming/post.md
Next (after the commitment question in wiki-ingest):
tools/wikitool raw accept --fidelity published --authority <value> incoming/post.html incoming/post.md
and on the source page: --set source_url=https://example.org/post
If the text stops at a teaser (paywall, login, script-rendered page): save the page from a
logged-in browser to incoming/ and run tools/wikitool raw fetch --html incoming/<file>.html --url <url>
```
11. **Invariante 4 bleibt unberührt:** `raw fetch` wird nur auf eine URL angewandt, die der Nutzer selbst nennt - nie auf eine URL, die in einer Rohquelle steht. Das steht in `raw/CONTRACT.md`, in `wiki-ingest` und im NEVER-Teil des Kommando-Records.
12. **Nicht über MCP.** Der MCP-Server bekommt kein Fetch-Tool: Ein Server, der beliebige URLs im Auftrag eines entfernten Aufrufers lädt, ist ein SSRF-Vektor. Der entfernte Weg bleibt `submit` -> `mcp-upload/`. Die Begründung steht in `raw/CONTRACT.md` § mcp-upload.
13. **Kein `--fidelity`/`--authority` bei `raw fetch`.** Die Werte gehören zu `raw accept` (dort sind sie Pflicht); sie zweimal zu erfragen wäre eine zweite Stelle für dieselbe Entscheidung.
14. **Paywall und Login: kein Login im Tool, stattdessen `--html`** (vom Betreiber entschieden 2026-10-02).
- `raw fetch --html incoming/<stem>.html --url <url>` leitet aus einer HTML-Datei, die der Mensch aus seinem eingeloggten Browser nach `incoming/` gespeichert hat ("Seite speichern unter", nur HTML), dieselbe `.md` ab (Header siehe Entscheidung 6). Kein Netzzugriff. Die `.html` bleibt byte-unverändert; das Bundle ist dasselbe wie in Entscheidung 2. Deckt Paywall und clientseitig gerenderte Seiten gleichermaßen ab. `--html` außerhalb von `incoming/` -> exit 1 (dieselbe Prüfung wie bei `raw accept`). `--url` ist Pflicht: Sie steht im Header, löst relative Links auf und geht als `source_url` auf die Source-Seite.
- **Keine Zugangsdaten, Cookies oder Login-Adapter in `wikitool`.** Das Tool geht an jede Instanz; Zugangsdaten im Arbeitsbaum widersprechen `raw/CONTRACT.md` § What does not belong here, und ein Adapter pro Paywall-Site ist Wartung, die kein Wissenskompiler tragen soll. Ein Auth-Dienst entsteht außerhalb des Stacks (#87); die Andockstelle dafür ist #169, nicht dieses Issue.
- **Teaser-Prüfung ist ein Urteil in `wiki-ingest`, nicht im Tool:** Ein Teaser ist allgemein nicht erkennbar und hat oft mehr als die 200 Zeichen aus Entscheidung 9. Schritt 1 liest die Quelle ohnehin vollständig; bricht der Text erkennbar ab ("Weiterlesen mit ...", Abo-Hinweis, Login-Aufforderung), stoppt die Session und schlägt dem Nutzer den `--html`-Weg vor, statt einen Teaser als Quelle zu ingesten.
## Betroffene Dateien
Bau (`fba263a`):
- `tools/chemenu/web_capture.py` (neu) - Fetch, Zeichensatz, Ableitung, Header, Stamm; Kern ohne `typer`/`rich`, wirft `ValidationError`/`BackendError`, damit #169 dort andocken kann.
- `tools/chemenu/commands/raw_cmd.py` - neues Kommando `raw fetch` samt Kommando-Record (Synopsis für beide Formen, `network: yes`, Exit-Status, ON FAILURE, NEVER).
- `tools/chemenu/cli_contract.py` - `raw fetch` in `GROUPS` ("Raw material and uploads"); ohne den Eintrag rendert `docs contract` das Kommando nicht.
- `tools/CONTRACT.md` - aus dem Record generiert (nicht von Hand).
- `tools/chemenu/tests/test_raw_fetch.py` (neu, 36 Tests) - gegen einen lokalen `http.server` (Präzedenz: `test_dist_upgrade.py`), kein echtes Netz.
- `tools/chemenu/tests/test_cli.py` - die gepinnte `network: yes`-Menge um `raw fetch` ergänzt.
- `tools/chemenu/tests/test_portability.py` - `opener` (urllibs `OpenerDirector`) in `NOT_TEXT_OPEN`: Der AST-Scan las `opener.open(request, ...)` als Text-Datei-Open ohne `encoding=`.
- `raw/CONTRACT.md` - neuer Abschnitt "Getting a URL in: `raw fetch`" zwischen `incoming/` und `mcp-upload/`: Bundle-Regel, Header-Format, `--html` und Paywall-Regel, Invariante-4-Satz; im `mcp-upload/`-Abschnitt der SSRF-Satz (Entscheidung 12).
- `instructions/wiki-ingest/SKILL.md` - `description`/Trigger um "ingest <url>"; Schritt 1: URL -> `raw fetch`, Teaser-Prüfung mit `--html`; Kommandoliste.
- `tools/README.md` (Modulliste: `web_capture.py`), `README.md` (§ Adding Knowledge: URL-Ingest und `--html`).
- `CHANGES.md` über `version bump` (Changeset "raw fetch: a sanctioned intake for a URL into incoming/ (#120)", Impact medium).
Abschlussprüfung (`c4dcff7`):
- `kb/CONTRACT.md` § Provenance: Der Satz, die lokale Kopie eines externen Artikels liege "under `raw/articles/`", war seit dem Datums-Shard falsch und hätte genau den URL-Pfad fehlgeleitet. Jetzt: unter `raw/`, bei einem `raw fetch` mit `.html` und `.md`.
- `instructions/wiki-ingest/SKILL.md` - eine URL als zweites Beispiel-Trigger.
- `CHANGES.md` über `version bump --patch` (Impact low).
- Geprüft und unverändert gelassen: `docs/pipeline-rationale.md` (das Argument "`incoming/` hält, was ein Mensch gewählt hat; die Grenze ist die Promotion nach `raw/`" gilt für `raw fetch` unverändert, die Begründung des Änderungsinhalts trägt `raw/CONTRACT.md`), `types/source.guidance.md`, `INSTALL.md`, `INSTALL-MCP.md`, `instructions/ingest-queue.md`, `instructions/mcp-read-server.md`. Keine Installationsinstruktion berührt.
## Akzeptanzkriterien
- [x] `tools/wikitool raw fetch <url>` gegen eine lokale HTML-Testseite schreibt `incoming/<stem>.html` und `incoming/<stem>.md` und gibt exit 0; `raw/` ist danach unverändert. (`test_fetch_writes_html_and_md_to_incoming_and_leaves_raw_untouched`)
- [x] Die `.html` in `incoming/` ist byte-identisch mit dem, was der Server ausgeliefert hat. (`test_the_html_is_byte_identical_to_what_the_server_sent`, inkl. CRLF und Nicht-ASCII-Bytes)
- [x] Zwei Läufe gegen dieselben Server-Bytes ergeben byte-identischen abgeleiteten Text (bis auf `retrieved:`). (`test_two_fetches_of_the_same_bytes_derive_identical_text`)
- [x] Eine als ISO-8859-1 ausgelieferte Seite (Charset nur im `<meta>`) ergibt korrekt dekodierte Umlaute, und der Header nennt `charset: "iso-8859-1 (from: meta)"` (gequotet, Entscheidung 6). (`test_latin1_declared_only_in_meta_is_decoded`)
- [x] Inhalt in `nav`/`footer`/`aside`/`script` einer Testseite erscheint nicht im abgeleiteten Text; Inhalt in `<main>` erscheint vollständig. (`test_boilerplate_is_dropped_and_main_is_kept_whole`)
- [x] `raw accept` mit beiden Dateien aus der Erfolgsmeldung legt das Bundle unter `raw/<YYYY>/<MM>/<stem>/` an. (`test_the_bundle_is_accepted_into_raw_as_one_source`)
- [x] Existiert `incoming/<stem>.html` oder `.md` bereits, endet der Befehl mit exit 1 und die vorhandene Datei ist unverändert. (`test_an_existing_target_is_never_overwritten`, beide Fälle)
- [x] `file:///etc/passwd`, eine Antwort über 25 MiB (mit und ohne `Content-Length`) und ein Timeout enden je mit exit 1 und hinterlassen keine Datei in `incoming/`. (`test_a_file_url_…`, `test_a_response_over_the_size_limit_…`, `test_a_slow_server_times_out`; zusätzlich Redirect auf `ftp:` und HTTP 404)
- [x] Eine `application/pdf`-Antwort wird byte-identisch als `.pdf` abgelegt, ohne abgeleitete Datei. (`test_a_pdf_is_stored_byte_identical_without_a_derivation`)
- [x] `raw fetch --html incoming/<stem>.html --url <url>` schreibt nur `incoming/<stem>.md`, macht keinen Netzzugriff, die `.html` bleibt byte-identisch, und der abgeleitete Text gleicht dem eines Online-Abrufs derselben Bytes (bis auf den Header). (`test_html_derives_only_the_md_without_network_and_matches_a_fetch` - Netz per Monkeypatch gesperrt)
- [x] `--html` mit einem Pfad außerhalb von `incoming/` oder ohne `--url` endet mit exit 1, ohne etwas zu schreiben. (`test_html_outside_incoming_is_refused`, `test_html_without_url_is_refused`)
- [x] Der MCP-Server bietet kein Fetch-Tool an (Tool-Liste unverändert - gehalten von `test_mcp_server.py::test_the_four_tools_are_there_and_nothing_that_writes`, das die Menge exakt pinnt).
- [x] `raw/CONTRACT.md` dokumentiert Bundle-Regel, Header-Format, `--html` und Paywall-Regel; `wiki-ingest` führt eine URL über `raw fetch` und enthält die Teaser-Prüfung.
- [x] `pytest`, `docs verify`, `instructions verify` grün; CI grün auf dem publizierten Commit. - Lokal: volle Suite hermetisch (`env -i`) 2095 passed, 3 skipped; `docs verify`, `instructions verify` OK. CI: Runs 502/503 auf `fba263a`, 504/505 auf `c4dcff7`, alle `success`.
## Versionsteil
**`--minor`** - neues Kommando, Drop-in in beide Richtungen: kein Flag/Kommando entfernt oder umbenannt, keine neue Abhängigkeit (Entscheidung 3), kein maschinell gelesenes Dateiformat geändert, keine Content-Migration; ein Downgrade verliert nur `raw fetch`. Der laufende Kandidat stand bereits auf MAJOR; der Bump hob ihn auf **`8.0.0-beta.25`** (`fba263a`), der Patch-Bump der Abschlussprüfung auf **`8.0.0-beta.26`** (`c4dcff7`).
## Bezug
- #169 - externer Abrufbefehl pro Instanz (`.wikitool-fetch.json`), die Andockstelle für einen Auth-Dienst; baut auf diesem Issue auf, `prio/waiting`. Andockpunkt ist jetzt `tools/chemenu/web_capture.py`.
- #87 - Notizen zur Infrastruktur außerhalb des Stacks (RSS-Poll, Auth-Dienst), wandert später in ein eigenes Repo. Dieses Issue übernimmt von dort nichts außer der Erkenntnis, dass Paywall-Inhalt nicht über das Tool selbst kommt.
## Labels
`area/kb` (Intake in `raw/` ist Pipeline/Provenienz), `kind/build`, `prio/planned` - bis zu diesem Bau erzeugte jeder URL-Ingest eine ad hoc geformte `raw/`-Datei, die unveränderlich bleibt. `size/M`.
Changelog: Stub ausgearbeitet und gegen den Baum (Stand 8.0.0-beta.24, c0f324f) geprüft. Befund bestätigt: Es gibt weiterhin keinen URL-Intake - raw accept nimmt nur Dateien unter incoming/, wiki-ingest setzt eine Datei voraus, nur source_url auf der Source-Seite existiert. Body neu geschrieben als Spec für wikitool raw fetch <url> (Variante "Bevorzugt" des Stubs); eine Frage offen (was als Rohquelle in raw/ landet), daher kind/decision. area/workflow -> area/kb (Intake in raw/ ist Pipeline/Provenienz, nicht Git/Publish). Labels prio/planned, size/M gesetzt, status/incoming entfernt.
Der ursprüngliche Stub, wörtlich:
Befund
raw/CONTRACT.md Abschnitt "Getting a file in: incoming/" beschreibt ausschliesslich den
Fall, dass ein Mensch bereits eine lokale Datei in incoming/ ablegt; tools/wikitool raw accept verlangt dementsprechend einen existierenden lokalen Dateipfad. Die wiki-ingest-Skill
setzt in Schritt 1 ebenfalls voraus, dass die Datei bereits in incoming/ oder raw/ liegt.
Fuer den Fall "Nutzer nennt eine URL, die ingested werden soll" gibt es weder ein Tool noch eine
dokumentierte Prozedur.
Ursache
Die Pipeline ist architektonisch auf Datei-Intake ausgelegt (incoming/ -> raw accept); ein
URL-Intake wurde offenbar nie mitgedacht. Es existiert kein wikitool raw fetch <url> o.ae.,
das eine URL laedt, Zeichensatz behandelt und das Ergebnis in ein raw/-taugliches Format
bringt.
Reproduktion
Bei einer Anfrage "Ingest " hat die Session keinen sanktionierten Tool-Call zur Verfuegung
und musste sich selbst eine Prozedur zusammenbauen:
curl -sL -A "..." "<url>" -o page.html
Zeichensatz pruefen (grep -oi charset= im HTML) statt ihn blind anzunehmen
HTML->Text-Konvertierung mit lynx -dump -nolist -assume_charset=UTF-8 -display_charset=UTF-8 page.html (verfuegbares Tool im Container; pandoc, w3m, Python bs4/readability waren nicht installiert)
Navigation/Footer/Kommentare/Tag-Wolke manuell anhand von Zeilennummern (grep -n, sed -n)
heraustrennen
Von Hand eine raw/-Datei mit einem Source:/Captured:-Header schreiben, in Anlehnung an
ein vorhandenes Beispiel (raw/CONTRACT.md selbst schreibt kein Format fuer diesen Header
vor)
Erst danach tools/wikitool raw accept --fidelity published --authority <wert> incoming/<datei> - ab hier greift der normale, dokumentierte Ablauf wieder
Jeder dieser sechs Schritte ist eine Ad-hoc-Entscheidung der Session (Tool-Wahl, Trennheuristik,
Header-Format), nicht das Ergebnis einer dokumentierten Vorgabe. Zwei Sessions, die denselben
Artikel ingesten, wuerden voraussichtlich unterschiedliche raw/-Dateien erzeugen.
Loesungsvorschlag
Bevorzugt: ein wikitool raw fetch <url> (oder aequivalent), das Fetch + Zeichensatz-
Handling + HTML->Text/Markdown-Konvertierung buendelt und das Ergebnis nach incoming/
schreibt - danach greift raw accept unveraendert. Damit bleibt "eine URL ist am Ende eine
Datei" gewahrt, ohne dass jede Session ihre eigene Toolchain waehlt.
Minimal: falls kein neues Tool gebaut wird, zumindest eine dokumentierte Prozedur (Tool-
Wahl, Header-Format Source:/Captured:/Author:) in raw/CONTRACT.md oder einer neuen instructions/ingest-url.md, damit der Ad-hoc-Teil wenigstens konvergiert.
Verworfen: den eingebauten Web-Fetch-Tool-Call der Harness direkt als Quelle zu nutzen - der
gibt die Seite nur als LLM-Zusammenfassung zurueck, nicht als Text nahe am Original. Das
widerspricht dem Qualitaetsziel von raw/CONTRACT.md ("kept exactly as received") fuer eine published-Fidelity-Erfassung.
Akzeptanzkriterien
Eine URL-Quelle laesst sich ueber einen einzigen dokumentierten/getoolten Schritt
ingesten, ohne Ad-hoc-Shell-Bastelei pro Session.
Das Header-Format der erzeugten raw/-Datei (Quelle, Erfassungsdatum) ist standardisiert
und in raw/CONTRACT.md oder einer verlinkten Instruction dokumentiert.
docs verify / instructions verify clean.
Herkunft
Beobachtet in einer privaten Instanz beim Ingest eines oeffentlichen Web-Artikels auf
ausdrueckliche URL-Anfrage des Nutzers. Kein Bezug zu Instanzinhalten - reiner
Workflow-/Tooling-Befund der Pipeline selbst.
**Changelog:** Stub ausgearbeitet und gegen den Baum (Stand `8.0.0-beta.24`, `c0f324f`) geprüft. Befund bestätigt: Es gibt weiterhin keinen URL-Intake - `raw accept` nimmt nur Dateien unter `incoming/`, `wiki-ingest` setzt eine Datei voraus, nur `source_url` auf der Source-Seite existiert. Body neu geschrieben als Spec für `wikitool raw fetch <url>` (Variante "Bevorzugt" des Stubs); eine Frage offen (was als Rohquelle in `raw/` landet), daher `kind/decision`. `area/workflow` -> `area/kb` (Intake in `raw/` ist Pipeline/Provenienz, nicht Git/Publish). Labels `prio/planned`, `size/M` gesetzt, `status/incoming` entfernt.
Der ursprüngliche Stub, wörtlich:
> ## Befund
>
> `raw/CONTRACT.md` Abschnitt "Getting a file in: incoming/" beschreibt ausschliesslich den
> Fall, dass ein Mensch bereits eine lokale Datei in `incoming/` ablegt; `tools/wikitool raw
> accept` verlangt dementsprechend einen existierenden lokalen Dateipfad. Die `wiki-ingest`-Skill
> setzt in Schritt 1 ebenfalls voraus, dass die Datei bereits in `incoming/` oder `raw/` liegt.
> Fuer den Fall "Nutzer nennt eine URL, die ingested werden soll" gibt es weder ein Tool noch eine
> dokumentierte Prozedur.
>
> ## Ursache
>
> Die Pipeline ist architektonisch auf Datei-Intake ausgelegt (`incoming/` -> `raw accept`); ein
> URL-Intake wurde offenbar nie mitgedacht. Es existiert kein `wikitool raw fetch <url>` o.ae.,
> das eine URL laedt, Zeichensatz behandelt und das Ergebnis in ein `raw/`-taugliches Format
> bringt.
>
> ## Reproduktion
>
> Bei einer Anfrage "Ingest <URL>" hat die Session keinen sanktionierten Tool-Call zur Verfuegung
> und musste sich selbst eine Prozedur zusammenbauen:
>
> 1. `curl -sL -A "..." "<url>" -o page.html`
> 2. Zeichensatz pruefen (`grep -oi charset=` im HTML) statt ihn blind anzunehmen
> 3. HTML->Text-Konvertierung mit `lynx -dump -nolist -assume_charset=UTF-8
> -display_charset=UTF-8 page.html` (verfuegbares Tool im Container; `pandoc`, `w3m`, Python
> `bs4`/`readability` waren nicht installiert)
> 4. Navigation/Footer/Kommentare/Tag-Wolke manuell anhand von Zeilennummern (`grep -n`, `sed -n`)
> heraustrennen
> 5. Von Hand eine `raw/`-Datei mit einem `Source:`/`Captured:`-Header schreiben, in Anlehnung an
> ein vorhandenes Beispiel (`raw/CONTRACT.md` selbst schreibt kein Format fuer diesen Header
> vor)
> 6. Erst danach `tools/wikitool raw accept --fidelity published --authority <wert>
> incoming/<datei>` - ab hier greift der normale, dokumentierte Ablauf wieder
>
> Jeder dieser sechs Schritte ist eine Ad-hoc-Entscheidung der Session (Tool-Wahl, Trennheuristik,
> Header-Format), nicht das Ergebnis einer dokumentierten Vorgabe. Zwei Sessions, die denselben
> Artikel ingesten, wuerden voraussichtlich unterschiedliche `raw/`-Dateien erzeugen.
>
> ## Loesungsvorschlag
>
> - **Bevorzugt:** ein `wikitool raw fetch <url>` (oder aequivalent), das Fetch + Zeichensatz-
> Handling + HTML->Text/Markdown-Konvertierung buendelt und das Ergebnis nach `incoming/`
> schreibt - danach greift `raw accept` unveraendert. Damit bleibt "eine URL ist am Ende eine
> Datei" gewahrt, ohne dass jede Session ihre eigene Toolchain waehlt.
> - **Minimal:** falls kein neues Tool gebaut wird, zumindest eine dokumentierte Prozedur (Tool-
> Wahl, Header-Format `Source:`/`Captured:`/`Author:`) in `raw/CONTRACT.md` oder einer neuen
> `instructions/ingest-url.md`, damit der Ad-hoc-Teil wenigstens konvergiert.
>
> Verworfen: den eingebauten Web-Fetch-Tool-Call der Harness direkt als Quelle zu nutzen - der
> gibt die Seite nur als LLM-Zusammenfassung zurueck, nicht als Text nahe am Original. Das
> widerspricht dem Qualitaetsziel von `raw/CONTRACT.md` ("kept exactly as received") fuer eine
> `published`-Fidelity-Erfassung.
>
> ## Akzeptanzkriterien
>
> - [ ] Eine URL-Quelle laesst sich ueber einen einzigen dokumentierten/getoolten Schritt
> ingesten, ohne Ad-hoc-Shell-Bastelei pro Session.
> - [ ] Das Header-Format der erzeugten `raw/`-Datei (Quelle, Erfassungsdatum) ist standardisiert
> und in `raw/CONTRACT.md` oder einer verlinkten Instruction dokumentiert.
> - [ ] `docs verify` / `instructions verify` clean.
>
> ## Herkunft
>
> Beobachtet in einer privaten Instanz beim Ingest eines oeffentlichen Web-Artikels auf
> ausdrueckliche URL-Anfrage des Nutzers. Kein Bezug zu Instanzinhalten - reiner
> Workflow-/Tooling-Befund der Pipeline selbst.
torben
changed title from Kein sanktionierter Weg fuer Ingest per URL - Ad-hoc-Scraping noetig to raw fetch: sanktionierter Intake für eine URL nach incoming/2026-10-02 19:28:07 +00:00
Changelog: Beide offenen Fragen vom Betreiber entschieden. Entscheidung 2 neu: Rohquelle ist das Bundle HTML + abgeleiteter Text (Variante A; B "nur Text" verworfen). Entscheidung 14 neu: Paywall/Login über raw fetch --html aus einer im eingeloggten Browser gespeicherten Seite, kein Login und keine Credentials im Tool, Teaser-Prüfung als Urteil in wiki-ingest. Header, Dateiname, Erfolgsmeldung und Kriterien entsprechend ergänzt. Andockstelle für einen externen Auth-Dienst als #169 ausgelagert (prio/waiting); #87 bleibt Notizzettel für die Infrastruktur außerhalb des Stacks. Titel umbenannt. kind/decision -> kind/build.
**Changelog:** Beide offenen Fragen vom Betreiber entschieden. Entscheidung 2 neu: Rohquelle ist das Bundle HTML + abgeleiteter Text (Variante A; B "nur Text" verworfen). Entscheidung 14 neu: Paywall/Login über `raw fetch --html` aus einer im eingeloggten Browser gespeicherten Seite, kein Login und keine Credentials im Tool, Teaser-Prüfung als Urteil in `wiki-ingest`. Header, Dateiname, Erfolgsmeldung und Kriterien entsprechend ergänzt. Andockstelle für einen externen Auth-Dienst als #169 ausgelagert (`prio/waiting`); #87 bleibt Notizzettel für die Infrastruktur außerhalb des Stacks. Titel umbenannt. `kind/decision` -> `kind/build`.
Changelog: Gebaut und publiziert (stack-build): 8.0.0-beta.25, Commit fba263a, CI-Runs 502/503 grün; alle Akzeptanzkriterien abgehakt, jeweils mit dem Test, der sie hält. Im Bau korrigiert: Header-Werte werden YAML-gequotet, wo nötig (charset: "utf-8 (from: header)"; Entscheidung 6 und Kriterium 4 angepasst). Entscheidung 5 nennt jetzt die Ergänzungen (Tabellen, Zitate, Bilder) und die bewusst gelassene Folge, dass ein <header> innerhalb von <main> mit wegfällt. Entscheidungen 7-9 präzisiert (--name wörtlich, exklusives Anlegen, Endungstabelle, Redirect-Schritte, Gesamt-Timeout, User-Agent). Betroffene Dateien um cli_contract.py (GROUPS), test_cli.py (Netzwerk-Pin), test_portability.py (opener) und README.md ergänzt.
**Changelog:** Gebaut und publiziert (`stack-build`): `8.0.0-beta.25`, Commit `fba263a`, CI-Runs 502/503 grün; alle Akzeptanzkriterien abgehakt, jeweils mit dem Test, der sie hält. Im Bau korrigiert: Header-Werte werden YAML-gequotet, wo nötig (`charset: "utf-8 (from: header)"`; Entscheidung 6 und Kriterium 4 angepasst). Entscheidung 5 nennt jetzt die Ergänzungen (Tabellen, Zitate, Bilder) und die bewusst gelassene Folge, dass ein `<header>` innerhalb von `<main>` mit wegfällt. Entscheidungen 7-9 präzisiert (`--name` wörtlich, exklusives Anlegen, Endungstabelle, Redirect-Schritte, Gesamt-Timeout, User-Agent). Betroffene Dateien um `cli_contract.py` (`GROUPS`), `test_cli.py` (Netzwerk-Pin), `test_portability.py` (`opener`) und `README.md` ergänzt.
Changelog: Abgeschlossen (stack-close). Body auf Endstand: Kopf nennt beide Commits und alle vier grünen CI-Runs, Befund in der Vergangenheit, Entscheidungen als umgesetzt markiert. Die Abschlussprüfung fand einen veralteten Satz in kb/CONTRACT.md (lokale Kopie eines externen Artikels "under raw/articles/") und korrigierte ihn mit einem eigenen Patch-Bump: 8.0.0-beta.26, c4dcff7, CI 504/505 grün. Dazu kam ein URL-Beispiel-Trigger in wiki-ingest. Geprüft und unverändert: docs/pipeline-rationale.md und die übrigen Intake-Dokumente (Liste im Body).
Übergabe:
Phase
Modell
Effort
Eigene Session?
Kontext übergelaufen oder kompaktiert?
1 Design (stack-dev)
unbekannt (nicht protokolliert)
unbekannt
ja - vor dem /clear, mit dem der Bau begann
unbekannt
2 Bau (stack-build)
Claude Opus 5.5
unbekannt - für die Session nicht sichtbar
ja (nach /clear, kalt vom Body gestartet)
nein
3 Abschluss (stack-close)
Claude Opus 5.5
unbekannt - für die Session nicht sichtbar
nein - in der Bau-Session fortgesetzt
nein
size/M.
**Changelog:** Abgeschlossen (`stack-close`). Body auf Endstand: Kopf nennt beide Commits und alle vier grünen CI-Runs, Befund in der Vergangenheit, Entscheidungen als umgesetzt markiert. Die Abschlussprüfung fand einen veralteten Satz in `kb/CONTRACT.md` (lokale Kopie eines externen Artikels "under `raw/articles/`") und korrigierte ihn mit einem eigenen Patch-Bump: `8.0.0-beta.26`, `c4dcff7`, CI 504/505 grün. Dazu kam ein URL-Beispiel-Trigger in `wiki-ingest`. Geprüft und unverändert: `docs/pipeline-rationale.md` und die übrigen Intake-Dokumente (Liste im Body).
**Übergabe:**
| Phase | Modell | Effort | Eigene Session? | Kontext übergelaufen oder kompaktiert? |
|---|---|---|---|---|
| 1 Design (`stack-dev`) | unbekannt (nicht protokolliert) | unbekannt | ja - vor dem `/clear`, mit dem der Bau begann | unbekannt |
| 2 Bau (`stack-build`) | Claude Opus 5.5 | unbekannt - für die Session nicht sichtbar | ja (nach `/clear`, kalt vom Body gestartet) | nein |
| 3 Abschluss (`stack-close`) | Claude Opus 5.5 | unbekannt - für die Session nicht sichtbar | nein - in der Bau-Session fortgesetzt | nein |
`size/M`.
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.
Befund (gegen den Baum geprüft, Stand vor dem Bau)
Für "Nutzer nennt eine URL, die ingested werden soll" gab es weder ein Tool noch eine Prozedur:
raw accept(tools/chemenu/commands/raw_cmd.py) nimmt nur existierende Dateien unterincoming/.raw/CONTRACT.mdkannte zwei Eingänge -incoming/(lokale Datei, § Getting a file in) undmcp-upload/(MCP-submit) -, keinen für eine URL.instructions/wiki-ingest/SKILL.mdSchritt 1 setzte eine Datei inincoming/oderraw/voraus; der einzige URL-Bezug war--set source_url=<upstream URL>auf der Source-Seite (Schritt 6,types/source.md).Eine Session baute sich deshalb jedes Mal ihre eigene Kette (curl, Zeichensatz raten, lynx/pandoc je nach Verfügbarkeit, Boilerplate per Zeilennummer schneiden, Header frei erfinden). Zwei Sessions erzeugten aus demselben Artikel verschiedene
raw/-Dateien - genau das, was der Intake durch Werkzeug ausschließen soll. Der Harness-eigene Web-Fetch scheidet aus: Er liefert eine LLM-Zusammenfassung, keine Erfassung nahe am Original.Entscheidungen (alle umgesetzt)
wikitool raw fetch <url>lädt, behandelt den Zeichensatz, leitet Text ab und schreibt nachincoming/. Danach läuftraw acceptunverändert - der Fetch landet nicht direkt inraw/, damit die Reihenfolge auswiki-ingest(Verpflichtungsfrage vorraw accept) erhalten bleibt und--fidelity/--authorityweiter genau dort erfragt werden.raw fetchschreibtincoming/<stem>.html- die Bytes exakt wie empfangen - undincoming/<stem>.md(Header + abgeleiteter Text).raw acceptnimmt beide in einem Aufruf als Bundle (raw/<YYYY>/<MM>/<stem>/), beide stehen inraw_files:. Begründung:raw/CONTRACT.mdverlangt "kept exactly as received", und empfangen wurde das HTML; der Text ist eine Ableitung des Tools. Nur mit dem HTML bleibt eine Behauptung inkb/byte-genau gegen das Original prüfbar, auch wenn die Ableitung Inhalt verliert oder später verbessert wird. Gelesen und zitiert wird die.md. Verworfen: nur den Text ablegen - kleiner, aber was die Ableitung verwirft, wäre unwiederbringlich, und der Contract bräuchte eine Ausnahme von seinem Qualitätsziel.urllib.request,html.parser,email.messagefür den Content-Type). Präzedenz:version.pylädt bereits perurllib. Begründung: Eine neue Abhängigkeit intools/requirements.txtfällt durch die Vorwärtshälfte des Drop-in-Tests (instructions/dev/version-parts.md) - nach dem Kopieren vontools/fehlt sie im venv, bis jemandpip installausführt. Die schwächere Boilerplate-Entfernung ist hinnehmbar, weil der abgeleitete Text nicht der Beleg ist (Entscheidung 2).charsetim HTTP-Content-Type-><meta charset>/<meta http-equiv>in den ersten 4 KiB -> UTF-8. Ein Label, das Python nicht kennt, wird wie ein fehlendes übersprungen. Undekodierbare Bytes werden ersetzt, nicht verworfen, und der Fall wird in der Ausgabe gemeldet. Die verwendete Kodierung steht im Header (Entscheidung 6). Bei--html(Entscheidung 14) entfällt die HTTP-Stufe.<main>, sonst genau ein<article>, sonst<body>. Darunter fallenscript,style,noscript,nav,header,footer,aside,form,template,svgweg. Überschriften ->#, Listen ->-/1.,pre/code-> Codeblock/Backticks, Links ->[text](absolute-url)(relativ aufgelöst gegenfinal_urlbzw.--url), Absätze durch Leerzeile. Keine Heuristik über Zeilennummern oder Textdichte. Gleiche Bytes ergeben dieselbe Ausgabe. Im Bau ergänzt, ohne die Regel zu ändern: Tabellen -> Markdown-Tabelle,blockquote->>, Bilder ->(data:-URIs nur als Alt-Text), Emphasis wird nicht ausgezeichnet. Folge der Regel, bewusst so gelassen: ein<header>innerhalb von<main>/<article>(oft mit dem Artikel-<h1>) fällt ebenfalls weg - der Titel steht dann nur intitle:, das HTML behält ihn..md-Datei, vom Tool geschrieben, nie von Hand. Die.htmlbleibt unberührt.charset(:in(from: ...)) und z.B. einen Titel mit:oder#. Grund: Die----Zäune weisen den Block als Frontmatter aus; ungültiges YAML darin bricht jeden Viewer, der ihn liest. Inhaltlich bleibt das Format gleich.Bei
--html(Entscheidung 14):fetched_by: wikitool raw fetch --html,retrieved/final_url/http_status/content_typeentfallen, dafürderived: <ISO-8601, UTC>(Zeitpunkt der Ableitung - wann der Mensch die Seite gespeichert hat, weiß das Tool nicht und behauptet es nicht).Die Felder sind Erfassungsmetadaten, keine Seiten-Frontmatter:
raw/hat keine Typen (raw/CONTRACT.mdAbs. 3), nichts im Stack liest diesen Block maschinell.author:wird bewusst nicht geführt - der Stub nannte ihn, aber aus HTML ist er nicht verlässlich bestimmbar; ein geratener Autor wäre eine Behauptung über die Quelle. Er gehört auf die Source-Seite, wenn die Quelle ihn trägt.-), auf 60 Zeichen gekürzt;--name <stem>überschreibt (wörtlich übernommen; Pfadtrenner und leer -> exit 1). Existiert inincoming/schon<stem>.htmloder<stem>.md, verweigert der Befehl (exit 1) - kein Überschreiben, keine Suffixe; die Dateien werden zusätzlich exklusiv (x-Modus) angelegt. Bei--htmlist der Stamm der der übergebenen Datei, die.mdentsteht neben ihr. Die Pfadbudget- und Stammkollisionsprüfung gegenraw/bleibtraw accepts Sache;raw fetchdupliziert sie nicht.text/plain,text/markdown-> Bytes unverändert als.txt/.md, ohne Ableitung. Alles andere (PDF, Bilder, ...) -> Bytes unverändert mit Endung aus dem Content-Type (Pythons eingebaute Tabelle, nicht/etc/mime.types; unbekannt oder fehlend ->.bin), ohne Ableitung; die Ausgabe sagt das. Ein Header entfällt dann (er hätte keine Datei, in die er gehört, ohne die Bytes zu verändern);url/retrievedstehen in der Erfolgsmeldung und gehen übersource_urlauf die Source-Seite.http/https, auch auf jedem Redirect-Schritt (file:,ftp:u.a. -> exit 1 - einfile:-Fetch wäre ein Weg anincoming/vorbei). Timeout 30 s für den ganzen Transfer, Größenlimit 25 MiB (Abbruch beim Überschreiten, nichts geschrieben). EigenerUser-Agentmit Versionsnummer (chemenu-wikitool/<version> (raw fetch)). Keine Cookies, kein JavaScript: Eine clientseitig gerenderte Seite ergibt fast leeren Text - unter 200 Zeichen abgeleitetem Text warnt der Befehl ausdrücklich, schreibt aber trotzdem; ob die Erfassung taugt, entscheidet der Mensch.raw fetchwird nur auf eine URL angewandt, die der Nutzer selbst nennt - nie auf eine URL, die in einer Rohquelle steht. Das steht inraw/CONTRACT.md, inwiki-ingestund im NEVER-Teil des Kommando-Records.submit->mcp-upload/. Die Begründung steht inraw/CONTRACT.md§ mcp-upload.--fidelity/--authoritybeiraw fetch. Die Werte gehören zuraw accept(dort sind sie Pflicht); sie zweimal zu erfragen wäre eine zweite Stelle für dieselbe Entscheidung.--html(vom Betreiber entschieden 2026-10-02).raw fetch --html incoming/<stem>.html --url <url>leitet aus einer HTML-Datei, die der Mensch aus seinem eingeloggten Browser nachincoming/gespeichert hat ("Seite speichern unter", nur HTML), dieselbe.mdab (Header siehe Entscheidung 6). Kein Netzzugriff. Die.htmlbleibt byte-unverändert; das Bundle ist dasselbe wie in Entscheidung 2. Deckt Paywall und clientseitig gerenderte Seiten gleichermaßen ab.--htmlaußerhalb vonincoming/-> exit 1 (dieselbe Prüfung wie beiraw accept).--urlist Pflicht: Sie steht im Header, löst relative Links auf und geht alssource_urlauf die Source-Seite.wikitool. Das Tool geht an jede Instanz; Zugangsdaten im Arbeitsbaum widersprechenraw/CONTRACT.md§ What does not belong here, und ein Adapter pro Paywall-Site ist Wartung, die kein Wissenskompiler tragen soll. Ein Auth-Dienst entsteht außerhalb des Stacks (#87); die Andockstelle dafür ist #169, nicht dieses Issue.wiki-ingest, nicht im Tool: Ein Teaser ist allgemein nicht erkennbar und hat oft mehr als die 200 Zeichen aus Entscheidung 9. Schritt 1 liest die Quelle ohnehin vollständig; bricht der Text erkennbar ab ("Weiterlesen mit ...", Abo-Hinweis, Login-Aufforderung), stoppt die Session und schlägt dem Nutzer den--html-Weg vor, statt einen Teaser als Quelle zu ingesten.Betroffene Dateien
Bau (
fba263a):tools/chemenu/web_capture.py(neu) - Fetch, Zeichensatz, Ableitung, Header, Stamm; Kern ohnetyper/rich, wirftValidationError/BackendError, damit #169 dort andocken kann.tools/chemenu/commands/raw_cmd.py- neues Kommandoraw fetchsamt Kommando-Record (Synopsis für beide Formen,network: yes, Exit-Status, ON FAILURE, NEVER).tools/chemenu/cli_contract.py-raw fetchinGROUPS("Raw material and uploads"); ohne den Eintrag rendertdocs contractdas Kommando nicht.tools/CONTRACT.md- aus dem Record generiert (nicht von Hand).tools/chemenu/tests/test_raw_fetch.py(neu, 36 Tests) - gegen einen lokalenhttp.server(Präzedenz:test_dist_upgrade.py), kein echtes Netz.tools/chemenu/tests/test_cli.py- die gepinntenetwork: yes-Menge umraw fetchergänzt.tools/chemenu/tests/test_portability.py-opener(urllibsOpenerDirector) inNOT_TEXT_OPEN: Der AST-Scan lasopener.open(request, ...)als Text-Datei-Open ohneencoding=.raw/CONTRACT.md- neuer Abschnitt "Getting a URL in:raw fetch" zwischenincoming/undmcp-upload/: Bundle-Regel, Header-Format,--htmlund Paywall-Regel, Invariante-4-Satz; immcp-upload/-Abschnitt der SSRF-Satz (Entscheidung 12).instructions/wiki-ingest/SKILL.md-description/Trigger um "ingest "; Schritt 1: URL ->raw fetch, Teaser-Prüfung mit--html; Kommandoliste.tools/README.md(Modulliste:web_capture.py),README.md(§ Adding Knowledge: URL-Ingest und--html).CHANGES.mdüberversion bump(Changeset "raw fetch: a sanctioned intake for a URL into incoming/ (#120)", Impact medium).Abschlussprüfung (
c4dcff7):kb/CONTRACT.md§ Provenance: Der Satz, die lokale Kopie eines externen Artikels liege "underraw/articles/", war seit dem Datums-Shard falsch und hätte genau den URL-Pfad fehlgeleitet. Jetzt: unterraw/, bei einemraw fetchmit.htmlund.md.instructions/wiki-ingest/SKILL.md- eine URL als zweites Beispiel-Trigger.CHANGES.mdüberversion bump --patch(Impact low).docs/pipeline-rationale.md(das Argument "incoming/hält, was ein Mensch gewählt hat; die Grenze ist die Promotion nachraw/" gilt fürraw fetchunverändert, die Begründung des Änderungsinhalts trägtraw/CONTRACT.md),types/source.guidance.md,INSTALL.md,INSTALL-MCP.md,instructions/ingest-queue.md,instructions/mcp-read-server.md. Keine Installationsinstruktion berührt.Akzeptanzkriterien
tools/wikitool raw fetch <url>gegen eine lokale HTML-Testseite schreibtincoming/<stem>.htmlundincoming/<stem>.mdund gibt exit 0;raw/ist danach unverändert. (test_fetch_writes_html_and_md_to_incoming_and_leaves_raw_untouched).htmlinincoming/ist byte-identisch mit dem, was der Server ausgeliefert hat. (test_the_html_is_byte_identical_to_what_the_server_sent, inkl. CRLF und Nicht-ASCII-Bytes)retrieved:). (test_two_fetches_of_the_same_bytes_derive_identical_text)<meta>) ergibt korrekt dekodierte Umlaute, und der Header nenntcharset: "iso-8859-1 (from: meta)"(gequotet, Entscheidung 6). (test_latin1_declared_only_in_meta_is_decoded)nav/footer/aside/scripteiner Testseite erscheint nicht im abgeleiteten Text; Inhalt in<main>erscheint vollständig. (test_boilerplate_is_dropped_and_main_is_kept_whole)raw acceptmit beiden Dateien aus der Erfolgsmeldung legt das Bundle unterraw/<YYYY>/<MM>/<stem>/an. (test_the_bundle_is_accepted_into_raw_as_one_source)incoming/<stem>.htmloder.mdbereits, endet der Befehl mit exit 1 und die vorhandene Datei ist unverändert. (test_an_existing_target_is_never_overwritten, beide Fälle)file:///etc/passwd, eine Antwort über 25 MiB (mit und ohneContent-Length) und ein Timeout enden je mit exit 1 und hinterlassen keine Datei inincoming/. (test_a_file_url_…,test_a_response_over_the_size_limit_…,test_a_slow_server_times_out; zusätzlich Redirect aufftp:und HTTP 404)application/pdf-Antwort wird byte-identisch als.pdfabgelegt, ohne abgeleitete Datei. (test_a_pdf_is_stored_byte_identical_without_a_derivation)raw fetch --html incoming/<stem>.html --url <url>schreibt nurincoming/<stem>.md, macht keinen Netzzugriff, die.htmlbleibt byte-identisch, und der abgeleitete Text gleicht dem eines Online-Abrufs derselben Bytes (bis auf den Header). (test_html_derives_only_the_md_without_network_and_matches_a_fetch- Netz per Monkeypatch gesperrt)--htmlmit einem Pfad außerhalb vonincoming/oder ohne--urlendet mit exit 1, ohne etwas zu schreiben. (test_html_outside_incoming_is_refused,test_html_without_url_is_refused)test_mcp_server.py::test_the_four_tools_are_there_and_nothing_that_writes, das die Menge exakt pinnt).raw/CONTRACT.mddokumentiert Bundle-Regel, Header-Format,--htmlund Paywall-Regel;wiki-ingestführt eine URL überraw fetchund enthält die Teaser-Prüfung.pytest,docs verify,instructions verifygrün; CI grün auf dem publizierten Commit. - Lokal: volle Suite hermetisch (env -i) 2095 passed, 3 skipped;docs verify,instructions verifyOK. CI: Runs 502/503 auffba263a, 504/505 aufc4dcff7, allesuccess.Versionsteil
--minor- neues Kommando, Drop-in in beide Richtungen: kein Flag/Kommando entfernt oder umbenannt, keine neue Abhängigkeit (Entscheidung 3), kein maschinell gelesenes Dateiformat geändert, keine Content-Migration; ein Downgrade verliert nurraw fetch. Der laufende Kandidat stand bereits auf MAJOR; der Bump hob ihn auf8.0.0-beta.25(fba263a), der Patch-Bump der Abschlussprüfung auf8.0.0-beta.26(c4dcff7).Bezug
.wikitool-fetch.json), die Andockstelle für einen Auth-Dienst; baut auf diesem Issue auf,prio/waiting. Andockpunkt ist jetzttools/chemenu/web_capture.py.Labels
area/kb(Intake inraw/ist Pipeline/Provenienz),kind/build,prio/planned- bis zu diesem Bau erzeugte jeder URL-Ingest eine ad hoc geformteraw/-Datei, die unveränderlich bleibt.size/M.Changelog: Stub ausgearbeitet und gegen den Baum (Stand
8.0.0-beta.24,c0f324f) geprüft. Befund bestätigt: Es gibt weiterhin keinen URL-Intake -raw acceptnimmt nur Dateien unterincoming/,wiki-ingestsetzt eine Datei voraus, nursource_urlauf der Source-Seite existiert. Body neu geschrieben als Spec fürwikitool raw fetch <url>(Variante "Bevorzugt" des Stubs); eine Frage offen (was als Rohquelle inraw/landet), daherkind/decision.area/workflow->area/kb(Intake inraw/ist Pipeline/Provenienz, nicht Git/Publish). Labelsprio/planned,size/Mgesetzt,status/incomingentfernt.Der ursprüngliche Stub, wörtlich:
Kein sanktionierter Weg fuer Ingest per URL - Ad-hoc-Scraping noetigto raw fetch: sanktionierter Intake für eine URL nach incoming/Changelog: Beide offenen Fragen vom Betreiber entschieden. Entscheidung 2 neu: Rohquelle ist das Bundle HTML + abgeleiteter Text (Variante A; B "nur Text" verworfen). Entscheidung 14 neu: Paywall/Login über
raw fetch --htmlaus einer im eingeloggten Browser gespeicherten Seite, kein Login und keine Credentials im Tool, Teaser-Prüfung als Urteil inwiki-ingest. Header, Dateiname, Erfolgsmeldung und Kriterien entsprechend ergänzt. Andockstelle für einen externen Auth-Dienst als #169 ausgelagert (prio/waiting); #87 bleibt Notizzettel für die Infrastruktur außerhalb des Stacks. Titel umbenannt.kind/decision->kind/build.Changelog: Gebaut und publiziert (
stack-build):8.0.0-beta.25, Commitfba263a, CI-Runs 502/503 grün; alle Akzeptanzkriterien abgehakt, jeweils mit dem Test, der sie hält. Im Bau korrigiert: Header-Werte werden YAML-gequotet, wo nötig (charset: "utf-8 (from: header)"; Entscheidung 6 und Kriterium 4 angepasst). Entscheidung 5 nennt jetzt die Ergänzungen (Tabellen, Zitate, Bilder) und die bewusst gelassene Folge, dass ein<header>innerhalb von<main>mit wegfällt. Entscheidungen 7-9 präzisiert (--namewörtlich, exklusives Anlegen, Endungstabelle, Redirect-Schritte, Gesamt-Timeout, User-Agent). Betroffene Dateien umcli_contract.py(GROUPS),test_cli.py(Netzwerk-Pin),test_portability.py(opener) undREADME.mdergänzt.Changelog: Abgeschlossen (
stack-close). Body auf Endstand: Kopf nennt beide Commits und alle vier grünen CI-Runs, Befund in der Vergangenheit, Entscheidungen als umgesetzt markiert. Die Abschlussprüfung fand einen veralteten Satz inkb/CONTRACT.md(lokale Kopie eines externen Artikels "underraw/articles/") und korrigierte ihn mit einem eigenen Patch-Bump:8.0.0-beta.26,c4dcff7, CI 504/505 grün. Dazu kam ein URL-Beispiel-Trigger inwiki-ingest. Geprüft und unverändert:docs/pipeline-rationale.mdund die übrigen Intake-Dokumente (Liste im Body).Übergabe:
stack-dev)/clear, mit dem der Bau begannstack-build)/clear, kalt vom Body gestartet)stack-close)size/M.