Ingest-Queue: Dokumente von außen einreichen, ohne dass ungeprüfter Fremdinhalt in raw/ landet #32

Closed
opened 2026-09-01 20:00:00 +00:00 by torben · 5 comments
Owner

Umgesetzt und ausgeliefert in 5.0.0-beta.11, Commit 8285218 (plus 5b916c6 für die docs/-Nachziehung). Abgespalten aus #19 (Sitzung 2026-09-01), entworfen und gebaut am 2026-09-11.

Der Anspruch, neu gefasst: Positiv-Liste statt Abwesenheit

#19s Eigenschaft war strukturell: chemenu.api und chemenu.mcp.server importieren nichts unter chemenu.commands, also existieren new, touch, xref, cite, publish im Servercode gar nicht. Ein submit-Tool macht diesen Satz falsch — „es gibt kein Tool, das schreibt" hält nicht mehr, sobald eines schreibt.

Die tragfähige Fassung ist deshalb keine Abwesenheit, sondern eine Positiv-Liste (Operator-Entscheidung 2026-09-11):

Der Serverprozess darf in genau ein Verzeichnis schreiben, und das ist im Code erzwungen.

Umgesetzt als ein Nadelöhr: chemenu.upload._write_atomic_within() löst jeden Zielpfad auf (Path.resolve(), also auch über Symlinks und ..), verweigert alles außerhalb <root>/mcp-upload/ und schreibt erst dann — Temp-Datei plus os.replace. Jeder Schreibvorgang des submit-Pfades geht durch diese eine Funktion.

Die Abwesenheitseigenschaft bleibt daneben unverändert bestehen und wird nicht aufgegeben: die Reviewer-Kommandos (upload accept/upload reject) liegen unter chemenu.commands und sind vom Server aus nicht erreichbar. Der bestehende Struktur-Test (sys.modules in einem frischen Interpreter) ist unverändert grün.

Die Entscheidungen

1. Quarantäne ist ein eigenes Top-Level-Verzeichnis mcp-upload/, gitignoriert. Nicht incoming/ und nicht darunter: raw accept toleriert und ignoriert seit #67 ein Unterverzeichnis in incoming/, ein incoming/submitted/x.pdf wäre also ungeprüft promotierbar gewesen. Im Repo und gitignoriert, nicht außerhalb — #58 Entscheidung 1 gilt hier wörtlich weiter (Beweismittel git check-ignore --no-index, keine neue Konfigurationsfläche). Kein .gitkeep: die Schreibprimitive legt das Verzeichnis selbst an, also braucht es weder einen Bootstrap-Schritt noch dist export-Seeding (das ist genau die Lücke, die #88 für incoming/ beschreibt und die hier nicht entsteht).

Die Pipeline hat damit vor raw/ zwei Stufen und zwei verschiedene Grenzen:

mcp-upload/<id>/   Fremdmaterial, niemand hat hingesehen
   ↓  wikitool upload accept <id> --confirm <token>    Grenze: Vertrauen
incoming/          wie eine eigene Ablage
   ↓  wikitool raw accept ...                          Grenze: Unveränderlichkeit
raw/<YYYY>/<MM>/ → kb/

2. Das Nadelöhr ist eine Funktion, nicht eine Konvention. tools/chemenu/upload.py (neu) importiert nur Stdlib und chemenu.config — kein typer, kein subprocess, nichts unter chemenu.commands.

3. Die Einreicher-Identität kommt aus einem Header, den die Middleware setzt — nie aus einem Tool-Argument. Context.headers (mcp 2.1.1) liefert die HTTP-Header, auf stdio None; das SDK warnt ausdrücklich, einen Header nie als Identitätszusicherung zu lesen. Umgesetzt: Headername in der Konfiguration (Default X-Forwarded-User), fehlender Header ⇒ Verweigerung ohne jeden Schreibvorgang, und das Manifest hält submitter_source neben submitter, damit der Datensatz sagt, worauf die Behauptung ruht. Ein Tool-Argument submitter wird gar nicht angeboten. Die Deployment-Pflicht (Header setzen, Client-Kopie verwerfen) steht in INSTALL-MCP.md Schritt 5 und instructions/ingest-queue.md.

4. Die menschliche Prüfung ist ein Gate mit Exit 42 — der vierte des Stacks, Upload Review Gate. upload accept <id> verweigert beim ersten Aufruf, druckt Manifest, Hash, Größe, Einreicher und die --confirm <token>-Zeile. Token wie beim Mass-Update-Gate: sha256 über Id, Dateiname, Größe, Sha256 und Einreicher, auf 12 Hexstellen. upload reject hat keinen Gate — Ablehnen braucht keine Freigabe, nur Annehmen.

5. Limits stehen in .wikitool-upload.json, und ihre Abwesenheit schaltet den Schreibpfad ab. Nicht „unbeschränkt" wie bei .wikitool-remotes.json, sondern „das submit-Tool wird gar nicht registriert" — die sichere Richtung. Eine defekte Datei ist ein Startfehler des Servers und ein FAIL im neuen doctor-Check, nie „keine Beschränkung".

6. Das Ledger ist append-only und die Buchhaltung zugleich. mcp-upload/ledger.jsonl, eine JSON-Zeile pro Ereignis (submitted/accepted/rejected). Kontingent = Summe der submitted-Ereignisse dieses Einreichers im rollierenden 24-Stunden-Fenster; kein Read-Modify-Write, also kein Wettlauf. Verweigerungen kommen nicht ins Ledger. Arbeitsteilung gegen das Manifest (Invariante 8): Manifest = was angekommen ist, Ledger = was passiert ist.

7. Ablehnung behält den Eintrag, löscht das Material. Der Ledger-Eintrag wird vor dem Löschen geschrieben, sodass eine Unterbrechung zwischen beiden den Grund trotzdem auf Platte lässt.

8. Keine Statusoberfläche für den Einreicher in diesem Paket. Die Antwort des submit-Aufrufs (Manifest mit Id) ist die Rückmeldung. Eine Statusabfrage wäre eine zweite Leseoberfläche mit eigenem Autorisierungsmodell — bei Bedarf ein eigenes Issue.

9. Die Verbindung Rohdatei ↔ Einreichung ist der Inhalts-Hash, kein neues Frontmatter-Feld. Das Ledger hält Id und sha256, die Datei ist unverändert, also ist die Zuordnung nachrechenbar. Kein submitted_by: im Type-Spec.

Umsetzungsnotizen

  • Größenprüfung vor dem Dekodieren, mit Toleranzband. Die base64-Länge ist eine obere Schranke (len*3/4), aber sie überschätzt um genau die 0–2 Padding-Zeichen. Der erste Entwurf hätte damit eine Einreichung exakt an der Grenze fälschlich abgelehnt; die Prüfung verweigert jetzt erst bei approx - 2 > max_bytes, und der Nachdekodier-Check bleibt die exakte Durchsetzung. Test für beide Richtungen (exakt an der Grenze wird angenommen, ein Byte darüber abgelehnt).
  • Konfigurations-Lesen beim Serveraufbau, nicht im Tool-Closure. read_config läuft einmal in build_server, sodass eine defekte Datei ein Startfehler ist und nicht erst beim ersten submit auffällt.
  • ValidationError statt fail() in upload.py. Die Bibliotheksgrenze aus #31: der Server bekommt Exceptions (über _guard als ToolError an den Aufrufer), die CLI übersetzt sie in fail() → ERROR-Zeile, Exit 1.
  • docs verify-Regel zu Issue-Nummern beachtet: tools/CONTRACT.md, instructions/gates.md und instructions/mcp-read-server.md nennen keine Issue-Nummern; tools/**/*.py ist davon ausgenommen (siehe issue-tracking.md) und behält seine Verweise im Code.
  • docs/-Veralterung geprüft und behoben: docs/pipeline-rationale.md behauptete, die Vertrauensgrenze sei allein die Beförderung nach raw/ und incoming/ liege „ganz auf der Nahseite" — mit einem Weg von außen beschreibt „ein Mensch hat die Datei irgendwo abgelegt" nicht mehr alles, was incoming/ erreicht. Der Absatz benennt jetzt zwei Grenzen (Vertrauen, Unveränderlichkeit). docs/why-gates-are-code.md trägt den vierten Gate samt Begründung, warum er die Form des Mass-Update-Gates wiederverwendet statt eine vierte zu erfinden. docs/ownership-and-templates.md und docs/version-model.md geprüft, unverändert gültig.
  • Kein Eintrag in der Budget-Ausnahmeliste. upload list/show sind read-only, zählen aber wie sources coverage — die Ausnahmeliste ist eine kurierte Konstante, keine „ändert das Wiki nicht"-Regel.

Akzeptanzkriterien

  • Die Schreibprimitive verweigert jeden aufgelösten Pfad außerhalb <root>/mcp-upload/ — Tests für .., absoluten Pfad, Pfadtrenner im Dateinamen und Symlink, der aus dem Verzeichnis hinausführt (test_write_primitive_refuses_a_relative_escape, ..._an_absolute_path, ..._a_symlink_escape, test_sanitize_filename_rejects_unsafe_names)
  • Nach einem Aufruf jedes Servertools inklusive submit hat sich keine Datei außerhalb <root>/mcp-upload/ geändert (test_submit_writes_only_into_mcp_upload_incoming_stays_untouched, Baum-Schnappschuss vorher/nachher plus git status --porcelain auf getrackte Dateien)
  • Der Einreichungspfad enthält keinen git-Aufruf — test_the_server_module_cannot_reach_a_write_command (frischer Interpreter, sys.modules kennt nichts unter chemenu.commands) bleibt unverändert grün
  • submit ohne Identitätsheader verweigert und schreibt nichts; Manifest trägt submitter, submitter_source, Zeit, sha256, Größe, Dateinamen, Id (test_submit_without_identity_writes_nothing, test_submit_without_identity_header_is_a_tool_error, test_submit_on_stdio_with_no_headers_at_all_is_a_tool_error)
  • submit ist nicht registriert, solange .wikitool-upload.json fehlt; eine defekte Datei ist ein Startfehler — Test für alle drei Zustände (test_submit_is_absent_without_the_upload_config, test_submit_is_present_once_armed, test_a_malformed_upload_config_refuses_to_build)
  • Deckel, Endungs-Allowlist und Kontingent werden je verweigert, ohne dass mcp-upload/ sich ändert; die Größenprüfung greift vor dem Dekodieren (test_submit_rejects_oversized_before_decoding mit gemocktem b64decode, plus die vier weiteren Größen-/Endungs-/Kontingent-Tests)
  • Ein bereits wartender sha256 wird unter Nennung der wartenden Id verweigert (test_submit_rejects_a_duplicate_pending_hash_naming_the_waiting_id); nach einer Ablehnung ist derselbe Inhalt wieder einreichbar (test_submit_allows_resubmission_after_rejection)
  • upload accept <id> ohne Token: Exit 42, Manifest und Token-Zeile im Output, nichts bewegt (test_accept_without_a_token_needs_clearance, test_accept_with_a_stale_token_needs_clearance_again)
  • upload accept --confirm <token>: Datei in incoming/, sha256 unverändert, Einreichungsverzeichnis fort, accepted-Ereignis im Ledger, raw//kb//work//reports/ unberührt (test_promote_moves_file_deletes_dir_and_ledgers_accepted, test_accept_with_the_right_token_promotes)
  • upload reject --reason: Material gelöscht, rejected-Ereignis mit Grund und sha256, nichts außerhalb mcp-upload/<id>/ angefasst; leerer Grund verweigert (test_reject_deletes_material_and_ledgers_with_reason, test_reject_requires_a_reason)
  • Ein Test beweist, dass über diesen Pfad nichts nach kb/ gelangen kann: die Kette endet in incoming/, und der Serverprozess erreicht kb/ schreibend in keiner Richtung (Positiv-Listen-Tests plus der unveränderte sys.modules-Test)
  • Eine Datei in mcp-upload/ erzeugt kein uncovered_raw_files-Finding und ist für lint/sources coverage unsichtbar — per Konstruktion (beide gehen über config.iter_raw_files(RAW_DIR)); im Promote-Test zusätzlich geprüft, dass raw/, kb/, work/, reports/ gar nicht entstehen
  • git check-ignore --no-index meldet mcp-upload/probe.pdf und .wikitool-upload.json als ignoriert, geprüft über docs_verify.REQUIRED_IGNORE_CANARIES
  • doctor berichtet den Zustand der Intake-Konfiguration und den Füllstand der Quarantäne (check_upload_intake, drei Tests: fehlend, scharf, defekt)
  • Vierter Gate in AGENTS.md § Gates und instructions/gates.md; alle Zählwörter nachgezogen („Four limits", „Four gates use it today", „four hard limits", „why the four gates", „the other three clear with a token")
  • instructions/ingest-queue.md geschrieben, aus dem wiki-ingest-Skill (Schritt 1) erreichbar, instructions verify grün — 21 Instructions statt 20
  • tools/CONTRACT.md trägt Kommando- und Fehlerkontrakt-Zeilen für alle vier upload-Kommandos; docs verify grün in beiden Richtungen (55 dokumentierte Kommandos statt 51)
  • raw/CONTRACT.md beschreibt die Quarantäne als Stufe vor incoming/; INSTALL-MCP.md nennt Opt-in (neuer Schritt 7) und Middleware-Anforderung; README.md und docs/why-gates-are-code.md nachgezogen
  • Changelog-Eintrag, MINOR auf dem 5.0.0-Kandidaten (5.0.0-beta.11), Drop-in-Test in beide Richtungen im Eintrag begründet

Verifiziert

  • tools/wikitool docs verify — OK: 55 Kommandos dokumentiert, 11 Ignore-Kanarien klar, keine Issue-Nummern in 65 ausgelieferten Dokumenten, TOCs aktuell auf 37 Referenzdateien.
  • tools/wikitool instructions verify — OK: 21 Instructions und 7 Skills gültig, 14 publizierte Kopien identisch.
  • pytest -q in tools/: 1185 passed, davon neu 49 in test_upload.py, 9 in test_upload_cmd.py, 6 in test_mcp_server.py, 3 in test_doctor.py.
  • Coverage: 87.8 % gesamt (Floor 85), chemenu/upload.py 93.1 %, chemenu/commands/upload_cmd.py 90.7 %, chemenu/mcp/server.py 100 %.
  • Manueller End-to-End-Lauf über die echte CLI gegen einen isolierten CHEMENU_ROOT: Einreichung → upload list/showupload accept ohne Token (Exit 42, Manifest und Token gedruckt) → mit Token (Datei in incoming/, Quarantäneverzeichnis fort, beide Ledger-Zeilen) → doctor meldet „submit tool armed".
  • Manueller End-to-End-Lauf über einen echten MCP-stdio-Client (das Rezept aus INSTALL-MCP.md § Verifikation): Tool-Liste zeigt alle sechs Werkzeuge, und submit verweigert dort korrekt mangels Identitätsheader (ctx.headers ist auf stdio None) — mcp-upload/ entsteht dabei gar nicht.
  • CI zu 8285218: Läufe 189 (verify) und 190 (release) grün — Tests, Coverage, Verify-Tree, Version-Gate, dist export und der Frisch-Instanz-Replay des Exports. Lauf 190 hat korrekt kein Release geschnitten: VERSION trägt -beta.11, jüngstes Release bleibt v4.7.4. Lauf 191 zu 5b916c6 (reine docs/-Prosa) lief beim Schließen noch.

Abgrenzung (eingehalten)

  • Keine zweite Leseoberfläche (Entscheidung 8), kein neues Frontmatter-Feld (Entscheidung 9).
  • Kein Rate Limiting im Prozess — gehört neben die Authentifizierung in die Middleware; das Kontingent ist Zurechenbarkeit pro Einreicher, nicht eine Rate, und ausdrücklich nicht der Iteration-Budget-Gate.
  • Kein Löschen aus raw/, keine Konvertierung, keine Umsortierung des Bestands, kein Deployment.

Was daraus offen weiterläuft

  • #37 (Container-Image): mcp-upload/ braucht dasselbe persistente Volume wie der übrige Checkout, sonst verliert eine ungeprüfte Einreichung ihre Quarantäne. Als Satz in INSTALL-MCP.md § „Was hier bewusst nicht steht" hinterlegt.
  • #88 (incoming/.gitkeep): in dieser Sitzung aufgefallen und getrennt erfasst; mcp-upload/ umgeht das Problem, weil nur das Werkzeug dort schreibt.
  • Der Korpus-Sync des Servers muss fetch && reset --hard bleiben — ein git clean -xd würde die Quarantäne löschen. Als Warnung in instructions/mcp-read-server.md Schritt 5 hinterlegt; im Baum existiert heute kein git clean.

Schließt #32.

**Umgesetzt und ausgeliefert** in `5.0.0-beta.11`, Commit `8285218` (plus `5b916c6` für die `docs/`-Nachziehung). Abgespalten aus #19 (Sitzung 2026-09-01), entworfen und gebaut am 2026-09-11. ## Der Anspruch, neu gefasst: Positiv-Liste statt Abwesenheit #19s Eigenschaft war *strukturell*: `chemenu.api` und `chemenu.mcp.server` importieren nichts unter `chemenu.commands`, also existieren `new`, `touch`, `xref`, `cite`, `publish` im Servercode gar nicht. Ein `submit`-Tool macht diesen Satz falsch — „es gibt kein Tool, das schreibt" hält nicht mehr, sobald eines schreibt. Die tragfähige Fassung ist deshalb keine Abwesenheit, sondern eine **Positiv-Liste** (Operator-Entscheidung 2026-09-11): > **Der Serverprozess darf in genau ein Verzeichnis schreiben, und das ist im Code erzwungen.** Umgesetzt als ein Nadelöhr: `chemenu.upload._write_atomic_within()` löst jeden Zielpfad auf (`Path.resolve()`, also auch über Symlinks und `..`), verweigert alles außerhalb `<root>/mcp-upload/` und schreibt erst dann — Temp-Datei plus `os.replace`. Jeder Schreibvorgang des `submit`-Pfades geht durch diese eine Funktion. Die Abwesenheitseigenschaft bleibt daneben unverändert bestehen und wird nicht aufgegeben: die Reviewer-Kommandos (`upload accept`/`upload reject`) liegen unter `chemenu.commands` und sind vom Server aus nicht erreichbar. Der bestehende Struktur-Test (`sys.modules` in einem frischen Interpreter) ist unverändert grün. ## Die Entscheidungen **1. Quarantäne ist ein eigenes Top-Level-Verzeichnis `mcp-upload/`, gitignoriert.** Nicht `incoming/` und nicht darunter: `raw accept` toleriert und ignoriert seit #67 ein Unterverzeichnis in `incoming/`, ein `incoming/submitted/x.pdf` wäre also ungeprüft promotierbar gewesen. Im Repo und gitignoriert, nicht außerhalb — #58 Entscheidung 1 gilt hier wörtlich weiter (Beweismittel `git check-ignore --no-index`, keine neue Konfigurationsfläche). Kein `.gitkeep`: die Schreibprimitive legt das Verzeichnis selbst an, also braucht es weder einen Bootstrap-Schritt noch `dist export`-Seeding (das ist genau die Lücke, die #88 für `incoming/` beschreibt und die hier nicht entsteht). Die Pipeline hat damit vor `raw/` zwei Stufen und zwei verschiedene Grenzen: ``` mcp-upload/<id>/ Fremdmaterial, niemand hat hingesehen ↓ wikitool upload accept <id> --confirm <token> Grenze: Vertrauen incoming/ wie eine eigene Ablage ↓ wikitool raw accept ... Grenze: Unveränderlichkeit raw/<YYYY>/<MM>/ → kb/ ``` **2. Das Nadelöhr ist eine Funktion, nicht eine Konvention.** `tools/chemenu/upload.py` (neu) importiert nur Stdlib und `chemenu.config` — kein `typer`, kein `subprocess`, nichts unter `chemenu.commands`. **3. Die Einreicher-Identität kommt aus einem Header, den die Middleware setzt — nie aus einem Tool-Argument.** `Context.headers` (mcp 2.1.1) liefert die HTTP-Header, auf stdio `None`; das SDK warnt ausdrücklich, einen Header nie als Identitätszusicherung zu lesen. Umgesetzt: Headername in der Konfiguration (Default `X-Forwarded-User`), fehlender Header ⇒ Verweigerung ohne jeden Schreibvorgang, und das Manifest hält `submitter_source` neben `submitter`, damit der Datensatz sagt, worauf die Behauptung ruht. Ein Tool-Argument `submitter` wird gar nicht angeboten. Die Deployment-Pflicht (Header setzen, Client-Kopie verwerfen) steht in `INSTALL-MCP.md` Schritt 5 und `instructions/ingest-queue.md`. **4. Die menschliche Prüfung ist ein Gate mit Exit 42** — der vierte des Stacks, **Upload Review Gate**. `upload accept <id>` verweigert beim ersten Aufruf, druckt Manifest, Hash, Größe, Einreicher und die `--confirm <token>`-Zeile. Token wie beim Mass-Update-Gate: sha256 über Id, Dateiname, Größe, Sha256 und Einreicher, auf 12 Hexstellen. `upload reject` hat **keinen** Gate — Ablehnen braucht keine Freigabe, nur Annehmen. **5. Limits stehen in `.wikitool-upload.json`, und ihre Abwesenheit schaltet den Schreibpfad ab.** Nicht „unbeschränkt" wie bei `.wikitool-remotes.json`, sondern „das `submit`-Tool wird gar nicht registriert" — die sichere Richtung. Eine defekte Datei ist ein Startfehler des Servers und ein `FAIL` im neuen `doctor`-Check, nie „keine Beschränkung". **6. Das Ledger ist append-only und die Buchhaltung zugleich.** `mcp-upload/ledger.jsonl`, eine JSON-Zeile pro Ereignis (`submitted`/`accepted`/`rejected`). Kontingent = Summe der `submitted`-Ereignisse dieses Einreichers im rollierenden 24-Stunden-Fenster; kein Read-Modify-Write, also kein Wettlauf. Verweigerungen kommen nicht ins Ledger. Arbeitsteilung gegen das Manifest (Invariante 8): Manifest = *was angekommen ist*, Ledger = *was passiert ist*. **7. Ablehnung behält den Eintrag, löscht das Material.** Der Ledger-Eintrag wird **vor** dem Löschen geschrieben, sodass eine Unterbrechung zwischen beiden den Grund trotzdem auf Platte lässt. **8. Keine Statusoberfläche für den Einreicher in diesem Paket.** Die Antwort des `submit`-Aufrufs (Manifest mit Id) ist die Rückmeldung. Eine Statusabfrage wäre eine zweite *Lese*oberfläche mit eigenem Autorisierungsmodell — bei Bedarf ein eigenes Issue. **9. Die Verbindung Rohdatei ↔ Einreichung ist der Inhalts-Hash, kein neues Frontmatter-Feld.** Das Ledger hält Id und sha256, die Datei ist unverändert, also ist die Zuordnung nachrechenbar. Kein `submitted_by:` im Type-Spec. ## Umsetzungsnotizen - **Größenprüfung vor dem Dekodieren, mit Toleranzband.** Die base64-Länge ist eine obere Schranke (`len*3/4`), aber sie überschätzt um genau die 0–2 Padding-Zeichen. Der erste Entwurf hätte damit eine Einreichung *exakt an der Grenze* fälschlich abgelehnt; die Prüfung verweigert jetzt erst bei `approx - 2 > max_bytes`, und der Nachdekodier-Check bleibt die exakte Durchsetzung. Test für beide Richtungen (exakt an der Grenze wird angenommen, ein Byte darüber abgelehnt). - **Konfigurations-Lesen beim Serveraufbau, nicht im Tool-Closure.** `read_config` läuft einmal in `build_server`, sodass eine defekte Datei ein Startfehler ist und nicht erst beim ersten `submit` auffällt. - **`ValidationError` statt `fail()` in `upload.py`.** Die Bibliotheksgrenze aus #31: der Server bekommt Exceptions (über `_guard` als `ToolError` an den Aufrufer), die CLI übersetzt sie in `fail()` → ERROR-Zeile, Exit 1. - **`docs verify`-Regel zu Issue-Nummern beachtet:** `tools/CONTRACT.md`, `instructions/gates.md` und `instructions/mcp-read-server.md` nennen keine Issue-Nummern; `tools/**/*.py` ist davon ausgenommen (siehe `issue-tracking.md`) und behält seine Verweise im Code. - **`docs/`-Veralterung geprüft und behoben:** `docs/pipeline-rationale.md` behauptete, die Vertrauensgrenze sei allein die Beförderung nach `raw/` und `incoming/` liege „ganz auf der Nahseite" — mit einem Weg von außen beschreibt „ein Mensch hat die Datei irgendwo abgelegt" nicht mehr alles, was `incoming/` erreicht. Der Absatz benennt jetzt zwei Grenzen (Vertrauen, Unveränderlichkeit). `docs/why-gates-are-code.md` trägt den vierten Gate samt Begründung, warum er die Form des Mass-Update-Gates wiederverwendet statt eine vierte zu erfinden. `docs/ownership-and-templates.md` und `docs/version-model.md` geprüft, unverändert gültig. - **Kein Eintrag in der Budget-Ausnahmeliste.** `upload list`/`show` sind read-only, zählen aber wie `sources coverage` — die Ausnahmeliste ist eine kurierte Konstante, keine „ändert das Wiki nicht"-Regel. ## Akzeptanzkriterien - [x] Die Schreibprimitive verweigert jeden aufgelösten Pfad außerhalb `<root>/mcp-upload/` — Tests für `..`, absoluten Pfad, Pfadtrenner im Dateinamen und Symlink, der aus dem Verzeichnis hinausführt (`test_write_primitive_refuses_a_relative_escape`, `..._an_absolute_path`, `..._a_symlink_escape`, `test_sanitize_filename_rejects_unsafe_names`) - [x] Nach einem Aufruf **jedes** Servertools inklusive `submit` hat sich keine Datei außerhalb `<root>/mcp-upload/` geändert (`test_submit_writes_only_into_mcp_upload_incoming_stays_untouched`, Baum-Schnappschuss vorher/nachher plus `git status --porcelain` auf getrackte Dateien) - [x] Der Einreichungspfad enthält keinen git-Aufruf — `test_the_server_module_cannot_reach_a_write_command` (frischer Interpreter, `sys.modules` kennt nichts unter `chemenu.commands`) bleibt unverändert grün - [x] `submit` ohne Identitätsheader verweigert und schreibt nichts; Manifest trägt `submitter`, `submitter_source`, Zeit, sha256, Größe, Dateinamen, Id (`test_submit_without_identity_writes_nothing`, `test_submit_without_identity_header_is_a_tool_error`, `test_submit_on_stdio_with_no_headers_at_all_is_a_tool_error`) - [x] `submit` ist nicht registriert, solange `.wikitool-upload.json` fehlt; eine defekte Datei ist ein Startfehler — Test für alle drei Zustände (`test_submit_is_absent_without_the_upload_config`, `test_submit_is_present_once_armed`, `test_a_malformed_upload_config_refuses_to_build`) - [x] Deckel, Endungs-Allowlist und Kontingent werden je verweigert, ohne dass `mcp-upload/` sich ändert; die Größenprüfung greift vor dem Dekodieren (`test_submit_rejects_oversized_before_decoding` mit gemocktem `b64decode`, plus die vier weiteren Größen-/Endungs-/Kontingent-Tests) - [x] Ein bereits wartender sha256 wird unter Nennung der wartenden Id verweigert (`test_submit_rejects_a_duplicate_pending_hash_naming_the_waiting_id`); nach einer Ablehnung ist derselbe Inhalt wieder einreichbar (`test_submit_allows_resubmission_after_rejection`) - [x] `upload accept <id>` ohne Token: Exit 42, Manifest und Token-Zeile im Output, nichts bewegt (`test_accept_without_a_token_needs_clearance`, `test_accept_with_a_stale_token_needs_clearance_again`) - [x] `upload accept --confirm <token>`: Datei in `incoming/`, sha256 unverändert, Einreichungsverzeichnis fort, `accepted`-Ereignis im Ledger, `raw/`/`kb/`/`work/`/`reports/` unberührt (`test_promote_moves_file_deletes_dir_and_ledgers_accepted`, `test_accept_with_the_right_token_promotes`) - [x] `upload reject --reason`: Material gelöscht, `rejected`-Ereignis mit Grund und sha256, nichts außerhalb `mcp-upload/<id>/` angefasst; leerer Grund verweigert (`test_reject_deletes_material_and_ledgers_with_reason`, `test_reject_requires_a_reason`) - [x] Ein Test beweist, dass über diesen Pfad nichts nach `kb/` gelangen kann: die Kette endet in `incoming/`, und der Serverprozess erreicht `kb/` schreibend in keiner Richtung (Positiv-Listen-Tests plus der unveränderte `sys.modules`-Test) - [x] Eine Datei in `mcp-upload/` erzeugt kein `uncovered_raw_files`-Finding und ist für `lint`/`sources coverage` unsichtbar — per Konstruktion (beide gehen über `config.iter_raw_files(RAW_DIR)`); im Promote-Test zusätzlich geprüft, dass `raw/`, `kb/`, `work/`, `reports/` gar nicht entstehen - [x] `git check-ignore --no-index` meldet `mcp-upload/probe.pdf` und `.wikitool-upload.json` als ignoriert, geprüft über `docs_verify.REQUIRED_IGNORE_CANARIES` - [x] `doctor` berichtet den Zustand der Intake-Konfiguration und den Füllstand der Quarantäne (`check_upload_intake`, drei Tests: fehlend, scharf, defekt) - [x] Vierter Gate in `AGENTS.md` § Gates und `instructions/gates.md`; alle Zählwörter nachgezogen („Four limits", „Four gates use it today", „four hard limits", „why the four gates", „the other three clear with a token") - [x] `instructions/ingest-queue.md` geschrieben, aus dem `wiki-ingest`-Skill (Schritt 1) erreichbar, `instructions verify` grün — 21 Instructions statt 20 - [x] `tools/CONTRACT.md` trägt Kommando- **und** Fehlerkontrakt-Zeilen für alle vier `upload`-Kommandos; `docs verify` grün in beiden Richtungen (55 dokumentierte Kommandos statt 51) - [x] `raw/CONTRACT.md` beschreibt die Quarantäne als Stufe vor `incoming/`; `INSTALL-MCP.md` nennt Opt-in (neuer Schritt 7) und Middleware-Anforderung; `README.md` und `docs/why-gates-are-code.md` nachgezogen - [x] Changelog-Eintrag, **MINOR** auf dem 5.0.0-Kandidaten (`5.0.0-beta.11`), Drop-in-Test in beide Richtungen im Eintrag begründet ## Verifiziert - `tools/wikitool docs verify` — OK: 55 Kommandos dokumentiert, 11 Ignore-Kanarien klar, keine Issue-Nummern in 65 ausgelieferten Dokumenten, TOCs aktuell auf 37 Referenzdateien. - `tools/wikitool instructions verify` — OK: 21 Instructions und 7 Skills gültig, 14 publizierte Kopien identisch. - `pytest -q` in `tools/`: **1185 passed**, davon neu 49 in `test_upload.py`, 9 in `test_upload_cmd.py`, 6 in `test_mcp_server.py`, 3 in `test_doctor.py`. - Coverage: **87.8 %** gesamt (Floor 85), `chemenu/upload.py` 93.1 %, `chemenu/commands/upload_cmd.py` 90.7 %, `chemenu/mcp/server.py` 100 %. - Manueller End-to-End-Lauf über die echte CLI gegen einen isolierten `CHEMENU_ROOT`: Einreichung → `upload list`/`show` → `upload accept` ohne Token (Exit 42, Manifest und Token gedruckt) → mit Token (Datei in `incoming/`, Quarantäneverzeichnis fort, beide Ledger-Zeilen) → `doctor` meldet „submit tool armed". - Manueller End-to-End-Lauf über einen echten MCP-**stdio-Client** (das Rezept aus `INSTALL-MCP.md` § Verifikation): Tool-Liste zeigt alle sechs Werkzeuge, und `submit` verweigert dort korrekt mangels Identitätsheader (`ctx.headers` ist auf stdio `None`) — `mcp-upload/` entsteht dabei gar nicht. - CI zu `8285218`: Läufe **189** (`verify`) und **190** (`release`) **grün** — Tests, Coverage, Verify-Tree, Version-Gate, `dist export` und der Frisch-Instanz-Replay des Exports. Lauf 190 hat korrekt **kein** Release geschnitten: `VERSION` trägt `-beta.11`, jüngstes Release bleibt `v4.7.4`. Lauf **191** zu `5b916c6` (reine `docs/`-Prosa) lief beim Schließen noch. ## Abgrenzung (eingehalten) - Keine zweite Leseoberfläche (Entscheidung 8), kein neues Frontmatter-Feld (Entscheidung 9). - Kein Rate Limiting im Prozess — gehört neben die Authentifizierung in die Middleware; das Kontingent ist Zurechenbarkeit pro Einreicher, nicht eine Rate, und ausdrücklich nicht der Iteration-Budget-Gate. - Kein Löschen aus `raw/`, keine Konvertierung, keine Umsortierung des Bestands, kein Deployment. ## Was daraus offen weiterläuft - **#37** (Container-Image): `mcp-upload/` braucht dasselbe persistente Volume wie der übrige Checkout, sonst verliert eine ungeprüfte Einreichung ihre Quarantäne. Als Satz in `INSTALL-MCP.md` § „Was hier bewusst nicht steht" hinterlegt. - **#88** (`incoming/.gitkeep`): in dieser Sitzung aufgefallen und getrennt erfasst; `mcp-upload/` umgeht das Problem, weil nur das Werkzeug dort schreibt. - Der Korpus-Sync des Servers muss `fetch && reset --hard` bleiben — ein `git clean -xd` würde die Quarantäne löschen. Als Warnung in `instructions/mcp-read-server.md` Schritt 5 hinterlegt; im Baum existiert heute kein `git clean`. Schließt #32.
torben added the prio/waitingsize/M labels 2026-09-01 20:00:00 +00:00
torben added the area/kbkind/build labels 2026-09-02 21:24:50 +00:00
torben added prio/planned and removed prio/waiting labels 2026-09-02 21:33:03 +00:00
Author
Owner

Changelog: prio/waitingprio/planned. Der im Body benannte Auslöser „#19 muss stehen" ist gefeuert - #19 ist geschlossen (2.4.0).

**Changelog:** `prio/waiting` → `prio/planned`. Der im Body benannte Auslöser „#19 muss stehen" ist gefeuert - #19 ist geschlossen (2.4.0).
torben added the status/blocked label 2026-09-04 12:32:42 +00:00
Author
Owner

Changelog: status/blocked gesetzt, an #16 (Triage-Sitzung 2026-09-04). Kein Body-Rewrite, nur das Label — der Body ist inhaltlich weiterhin richtig.

Die Vorbedingung #19 ist erfüllt (geschlossen mit 2.4.0, der Server läuft auf beiden Transports), damit fällt der ursprüngliche Auslöser weg. Was bleibt, ist die zweite Abhängigkeit, die der Body schon nennt: wikitool raw accept ist Geschwister zu raw rename aus #16 und teilt sich dessen Mechanik — eine Datei bewegen, ohne dass eine Referenz zwischendurch ins Leere zeigt. Die zweimal getrennt zu bauen, wäre die zweite Kopie, die auseinanderläuft.

In der Abarbeitungsreihenfolge steht #16 deshalb direkt vor diesem Issue. prio/planned, kind/build, size/M, area/kb unverändert.

**Changelog:** `status/blocked` gesetzt, an **#16** (Triage-Sitzung 2026-09-04). Kein Body-Rewrite, nur das Label — der Body ist inhaltlich weiterhin richtig. Die Vorbedingung #19 ist erfüllt (geschlossen mit 2.4.0, der Server läuft auf beiden Transports), damit fällt der ursprüngliche Auslöser weg. Was bleibt, ist die zweite Abhängigkeit, die der Body schon nennt: `wikitool raw accept` ist Geschwister zu `raw rename` aus #16 und teilt sich dessen Mechanik — eine Datei bewegen, ohne dass eine Referenz zwischendurch ins Leere zeigt. Die zweimal getrennt zu bauen, wäre die zweite Kopie, die auseinanderläuft. In der Abarbeitungsreihenfolge steht #16 deshalb direkt vor diesem Issue. `prio/planned`, `kind/build`, `size/M`, `area/kb` unverändert.
Author
Owner

Aus der Sitzung 2026-09-04 zur Ordnerorganisation: #58 kommt von der anderen Seite auf dieselbe Mechanik. Dort geht es nicht um fremd eingereichtes Material, sondern darum, dass der Nutzer seine eigenen Dateien nicht mehr von Hand nach articles//documents//notes//assets/ einsortieren soll — und dass beim Muster „PDF hochladen, nach Markdown konvertieren" zwei Dateien einer logischen Quelle entstehen, deren Zusammengehörigkeit heute ausschließlich in raw_files: steht und im Dateisystem unsichtbar ist.

Ein Mechanismus, zwei Auslöser. Vorgeschlagene Arbeitsteilung: #58 baut raw accept, dieses Issue konsumiert es und ergänzt, was nur der Fremdeinreichung eigen ist — Auth, Kontingente, Manifest mit Einreicherzurechnung, Ablehnungspfad.

Zwei Punkte, die dabei aus diesem Issue heraus zu beachten sind:

  • Der Lesezugriff scheint gegenläufig. Die Akzeptanzkriterien hier verlangen eine Quarantäne, „die kein Kommando der normalen Pipeline liest"; ein lokales incoming/ will, dass die Ingest-Sitzung hineinsieht. Auflösung in #58: die Grenze zwischen Sitzung und Pipeline-Kommando ziehen statt zwischen Sitzung und Verzeichnis — sources coverage und lint sehen den Eingang nie (sonst meldet jede unverarbeitete Ablage sofort ein Finding), ein Mensch und seine Sitzung schon. Das ist auch für dieses Issue die richtige Grenze, hier nur strenger begründet.
  • Die offene Frage „innerhalb des Repos (gitignoriert) oder außerhalb" muss für beide gleich beantwortet werden, sonst gibt es zwei Eingänge. Sie steht in #58 nochmal, mit der lokalen Abwägung daneben.

status/blocked bleibt hier korrekt — dieses Issue hängt weiterhin an #19. #58 hängt an nichts davon und ist heute baubar; genau deshalb ist es abgespalten und nicht als Kommentar hier gelandet.

Aus der Sitzung 2026-09-04 zur Ordnerorganisation: **#58 kommt von der anderen Seite auf dieselbe Mechanik.** Dort geht es nicht um fremd eingereichtes Material, sondern darum, dass der Nutzer seine eigenen Dateien nicht mehr von Hand nach `articles/`/`documents/`/`notes/`/`assets/` einsortieren soll — und dass beim Muster „PDF hochladen, nach Markdown konvertieren" zwei Dateien einer logischen Quelle entstehen, deren Zusammengehörigkeit heute ausschließlich in `raw_files:` steht und im Dateisystem unsichtbar ist. Ein Mechanismus, zwei Auslöser. Vorgeschlagene Arbeitsteilung: **#58 baut `raw accept`, dieses Issue konsumiert es** und ergänzt, was nur der Fremdeinreichung eigen ist — Auth, Kontingente, Manifest mit Einreicherzurechnung, Ablehnungspfad. Zwei Punkte, die dabei aus diesem Issue heraus zu beachten sind: - **Der Lesezugriff scheint gegenläufig.** Die Akzeptanzkriterien hier verlangen eine Quarantäne, „die kein Kommando der normalen Pipeline liest"; ein lokales `incoming/` will, dass die Ingest-Sitzung hineinsieht. Auflösung in #58: die Grenze zwischen *Sitzung* und *Pipeline-Kommando* ziehen statt zwischen Sitzung und Verzeichnis — `sources coverage` und `lint` sehen den Eingang nie (sonst meldet jede unverarbeitete Ablage sofort ein Finding), ein Mensch und seine Sitzung schon. Das ist auch für dieses Issue die richtige Grenze, hier nur strenger begründet. - **Die offene Frage „innerhalb des Repos (gitignoriert) oder außerhalb" muss für beide gleich beantwortet werden**, sonst gibt es zwei Eingänge. Sie steht in #58 nochmal, mit der lokalen Abwägung daneben. `status/blocked` bleibt hier korrekt — dieses Issue hängt weiterhin an #19. #58 hängt an nichts davon und ist heute baubar; genau deshalb ist es abgespalten und nicht als Kommentar hier gelandet.
torben added size/L and removed size/Mstatus/blocked labels 2026-09-11 06:41:45 +00:00
Author
Owner

Changelog: Body vollständig neu geschrieben (Vorbereitungssitzung 2026-09-11), status/blocked entfernt, size/Msize/L.

Fünf Stellen waren überholt und sind korrigiert statt umschrieben: die Blockade auf #19 (geschlossen mit 2.4.0), die Abhängigkeit von #31 (geschlossen), die Erwartung, raw accept gemeinsam mit #16 zu bauen (in #58 gebaut, in #67 umgestellt; #16 ist keine Abhängigkeit), die offene Frage nach dem Ort des Eingangs (von #58 Entscheidung 1 mitentschieden) und die Quarantäne-Skizze über incoming/<typ>/ (seit #67 ist incoming/ flach und ein Unterverzeichnis wird toleriert und ignoriert — eine Fremdeinreichung darunter wäre still promotierbar gewesen, was jetzt der Grund für ein eigenes Verzeichnis ist).

Vier Operator-Entscheidungen sind neu im Body, alle vom 2026-09-11:

  1. Ein Server, submit als weiteres Tool — aber mit Positiv-Liste. Das ist die inhaltliche Korrektur an #19s Formulierung: „nichts Schreibendes ist importiert" hält nicht mehr, sobald ein Tool schreibt. Die neue Fassung ist ein Nadelöhr im Code — der Prozess darf in genau ein Verzeichnis (mcp-upload/) schreiben, jeder andere aufgelöste Pfad wird verweigert. Die Abwesenheitseigenschaft bleibt daneben bestehen: die Reviewer-Kommandos liegen unter chemenu.commands und werden vom Server nicht importiert.
  2. Vierter Gate, Exit 42 auf upload accept, mit --confirm-Token wie beim Mass-Update-Gate — die Prüfung ist damit Code und nicht eine Instruction neben einem Kommando.
  3. Ablehnung behält den Ledger-Eintrag, löscht das Material.
  4. Keine Statusoberfläche für den Einreicher in diesem Paket.

Zwei Machbarkeitsfragen wurden vorab am Baum geprüft, weil das Design an ihnen hängt: mcp 2.1.1 gibt einem Tool über Context.headers die HTTP-Header (auf stdio None), warnt aber ausdrücklich, einen Header nie als Identitätszusicherung zu lesen — daher die Anforderung an die Middleware, den Header zu setzen und eine Client-Kopie zu verwerfen. Und im Baum existiert kein git clean, der dokumentierte Sync (fetch && reset --hard) lässt ignorierte Dateien stehen; die Quarantäne im Baum überlebt ihn, und die Warnung gehört in instructions/mcp-read-server.md Schritt 5.

kind/build statt kind/decision: nach diesen vier Antworten ist keine Entscheidung mehr offen. Was im Body unter „Offen, aber nicht blockierend" steht, ist Deployment (#37, Volume für die Quarantäne) und eine Messung bei der Umsetzung (Transportgrenze für große base64-Nutzlasten).

size/Msize/L, weil das Paket gegenüber der alten Skizze einen Gate, eine neue Instruction, eine Kommandogruppe und den Doku-Nachzug über acht Dateien enthält.

**Changelog:** Body vollständig neu geschrieben (Vorbereitungssitzung 2026-09-11), `status/blocked` entfernt, `size/M` → `size/L`. Fünf Stellen waren überholt und sind korrigiert statt umschrieben: die Blockade auf #19 (geschlossen mit 2.4.0), die Abhängigkeit von #31 (geschlossen), die Erwartung, `raw accept` gemeinsam mit #16 zu bauen (in #58 gebaut, in #67 umgestellt; #16 ist keine Abhängigkeit), die offene Frage nach dem Ort des Eingangs (von #58 Entscheidung 1 mitentschieden) und die Quarantäne-Skizze über `incoming/<typ>/` (seit #67 ist `incoming/` flach und ein Unterverzeichnis wird toleriert *und ignoriert* — eine Fremdeinreichung darunter wäre still promotierbar gewesen, was jetzt der Grund für ein eigenes Verzeichnis ist). Vier Operator-Entscheidungen sind neu im Body, alle vom 2026-09-11: 1. **Ein Server, `submit` als weiteres Tool — aber mit Positiv-Liste.** Das ist die inhaltliche Korrektur an #19s Formulierung: „nichts Schreibendes ist importiert" hält nicht mehr, sobald ein Tool schreibt. Die neue Fassung ist ein Nadelöhr im Code — der Prozess darf in genau ein Verzeichnis (`mcp-upload/`) schreiben, jeder andere aufgelöste Pfad wird verweigert. Die Abwesenheitseigenschaft bleibt daneben bestehen: die Reviewer-Kommandos liegen unter `chemenu.commands` und werden vom Server nicht importiert. 2. **Vierter Gate, Exit 42** auf `upload accept`, mit `--confirm`-Token wie beim Mass-Update-Gate — die Prüfung ist damit Code und nicht eine Instruction neben einem Kommando. 3. **Ablehnung behält den Ledger-Eintrag, löscht das Material.** 4. **Keine Statusoberfläche für den Einreicher** in diesem Paket. Zwei Machbarkeitsfragen wurden vorab am Baum geprüft, weil das Design an ihnen hängt: `mcp` 2.1.1 gibt einem Tool über `Context.headers` die HTTP-Header (auf stdio `None`), warnt aber ausdrücklich, einen Header nie als Identitätszusicherung zu lesen — daher die Anforderung an die Middleware, den Header zu *setzen* und eine Client-Kopie zu verwerfen. Und im Baum existiert kein `git clean`, der dokumentierte Sync (`fetch && reset --hard`) lässt ignorierte Dateien stehen; die Quarantäne im Baum überlebt ihn, und die Warnung gehört in `instructions/mcp-read-server.md` Schritt 5. `kind/build` statt `kind/decision`: nach diesen vier Antworten ist keine Entscheidung mehr offen. Was im Body unter „Offen, aber nicht blockierend" steht, ist Deployment (#37, Volume für die Quarantäne) und eine Messung bei der Umsetzung (Transportgrenze für große base64-Nutzlasten). `size/M` → `size/L`, weil das Paket gegenüber der alten Skizze einen Gate, eine neue Instruction, eine Kommandogruppe und den Doku-Nachzug über acht Dateien enthält.
Author
Owner

Changelog: Body auf den Endzustand umgeschrieben und geschlossen. Umgesetzt in 5.0.0-beta.11, Commit 8285218 (+ 5b916c6 für docs/pipeline-rationale.md).

Gegenüber der Vorbereitungsfassung von heute Morgen geändert: alle 19 Akzeptanzkriterien abgehakt und mit den Testnamen belegt, die sie prüfen; die vier Entscheidungen lesen als entschieden statt als Vorschlag; § „Umsetzungsnotizen" neu — darunter der einzige Fund, der das Design während der Umsetzung korrigiert hat (die base64-Längenprüfung überschätzt um die 0–2 Padding-Zeichen und hätte eine Einreichung exakt an der Grenze fälschlich abgelehnt; sie verweigert jetzt erst bei approx - 2 > max_bytes, der Nachdekodier-Check bleibt die exakte Durchsetzung). § „Verifiziert" nennt beide End-to-End-Läufe (CLI und echter MCP-stdio-Client) und CI-Lauf 189. § „Offen weiterläuft" verweist auf #37 (Volume für die Quarantäne) und #88.

docs/-Veralterung geprüft, ein echter Fund: docs/pipeline-rationale.md behauptete, die Vertrauensgrenze sei allein die Beförderung nach raw/ und incoming/ liege „ganz auf der Nahseite" — das beschreibt mit einem Weg von außen nicht mehr alles, was incoming/ erreicht. Der Absatz benennt jetzt zwei Grenzen. docs/why-gates-are-code.md trägt den vierten Gate; ownership-and-templates.md und version-model.md sind unverändert gültig.

**Changelog:** Body auf den Endzustand umgeschrieben und geschlossen. Umgesetzt in `5.0.0-beta.11`, Commit `8285218` (+ `5b916c6` für `docs/pipeline-rationale.md`). Gegenüber der Vorbereitungsfassung von heute Morgen geändert: alle 19 Akzeptanzkriterien abgehakt und mit den Testnamen belegt, die sie prüfen; die vier Entscheidungen lesen als entschieden statt als Vorschlag; § „Umsetzungsnotizen" neu — darunter der einzige Fund, der das Design während der Umsetzung korrigiert hat (die base64-Längenprüfung überschätzt um die 0–2 Padding-Zeichen und hätte eine Einreichung *exakt an der Grenze* fälschlich abgelehnt; sie verweigert jetzt erst bei `approx - 2 > max_bytes`, der Nachdekodier-Check bleibt die exakte Durchsetzung). § „Verifiziert" nennt beide End-to-End-Läufe (CLI und echter MCP-stdio-Client) und CI-Lauf 189. § „Offen weiterläuft" verweist auf #37 (Volume für die Quarantäne) und #88. `docs/`-Veralterung geprüft, ein echter Fund: `docs/pipeline-rationale.md` behauptete, die Vertrauensgrenze sei allein die Beförderung nach `raw/` und `incoming/` liege „ganz auf der Nahseite" — das beschreibt mit einem Weg von außen nicht mehr alles, was `incoming/` erreicht. Der Absatz benennt jetzt zwei Grenzen. `docs/why-gates-are-code.md` trägt den vierten Gate; `ownership-and-templates.md` und `version-model.md` sind unverändert gültig.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#32