Teil A aus dem Design #171 (dort Entscheidungen und verworfene Alternativen). Folge-Issues: #178 (Intake-Lauf, baut hierauf auf - nutzt raw status --json und raw capture --update), #179 (Leitlinien-Export, definiert den Export-Kopf, den diese Erfassung ausschließt). Gegen den Baum geprüft am 2026-10-04 (Stand 8ff22ad).
Abgeschlossen (2026-10-05). Gebaut und publiziert als 8.0.0-beta.41 in Commit 311c8ee, verifiziert durch CI-Lauf 545 (Jobs verify und pwsh grün; Release-Lauf 546 grün). Lokal vorher: pytest 2272 passed / 3 skipped, die Raw-Suiten zusätzlich gegen eine leere Maschine (env -i), docs verify, instructions verify. In der Abschlussphase kam eine Nachziehung dazu (8.0.0-beta.42, Commit 8ce202a, CI-Lauf 547 und Release-Lauf 548 grün): Die ON-FAILURE-Reaktion von raw accept incoming/<folder> bei belegtem Namen sagte noch "rename the folder - there is no --replaces for a folder" und hätte eine neue Fassung eines erfassten Bundles damit zur zweiten, getrennten Quelle gemacht. Sie nennt jetzt --replaces-bundle, wenn ein erfasstes Bundle den Namen hält. raw/CONTRACT.md § "Getting a file in" sagt jetzt, dass ein erfasster Ordner die Capture-Felder aus seinem Manifest nimmt. Keine docs/-Seite wurde veraltet: docs/pipeline-rationale.md begründet die Vertrauensgrenze an der Promotion nach raw/, und raw capture landet wie jede andere Aufnahme in incoming/. Kommandonamen wie entworfen (raw capture, raw status, raw accept --replaces-bundle). Was beim Bau über den Entwurf hinaus präzisiert oder ergänzt wurde, steht an seiner Stelle und ist mit (Bau) markiert.
Problem
Doku aus einem Git-Repository kam nur von Hand in eine Instanz: Dateien nach incoming/ kopieren, umbenennen, um der globalen Stem-Regel auszuweichen, pro Unterordner ein Bundle. Nichts hielt fest, aus welchem Repo und Commit ein Bundle stammte, und nichts meldete, wenn sich das Repo seitdem geändert hatte. Ein Ordner-Bundle ließ sich außerdem nicht als Ganzes durch eine neue Fassung ersetzen (raw_cmd._folder_collision_message: "A folder has no --replaces"; --page scheiterte an verschachtelten Bundles, weil _plan_file_moves alle raw_files: in einem Verzeichnis verlangt).
Befund nebenbei, mitbehoben:config.RAW_IGNORE_NAMES ({".gitkeep", ".DS_Store", "CONTRACT.md"}) wurde in config.iter_raw_files per Dateiname in jeder Tiefe angewandt. Gemeint war nur raw/CONTRACT.md, getroffen wurde jede CONTRACT.md unter raw/. Behoben über config.RAW_IGNORE_TOP_LEVEL (Punkt 4).
Entwurf (umgesetzt)
1. raw capture <repo-url> --ref <ref-regel> --path <glob>... --name <bundle> --fidelity <v> --authority <v> - Kern in tools/chemenu/repo_capture.py
Ref-Regel: ein Branchname (main) oder ein Tag-Muster (v*, erkannt an *?[ → das jüngste passende Tag nach Versionssortierung, -v:refname-Semantik ohne Suffix-Konfiguration). Aufgelöst per git ls-remote --heads --tags zu genau einem Commit; bei einem annotierten Tag der gepeelte Commit (^{}-Zeile).
Holt diesen Commit per Ref-Name flach (--depth 1, Refspec +<ref>:refs/wikitool/fetched) in einen gitignorierten Cache: config.ROOT/tools/.wikitool_capture/<sha256 der URL>/, ein Bare-Repo pro URL, Zugriff unter chemenu.filelock (Lockdatei <hash>.lock daneben). Nie per SHA. Alle Git-Aufrufe laufen mit --git-dir=<cache>, damit weder ein umgebendes Repo noch ein geerbtes GIT_DIR greift.
Dateien werden als Blobs gelesen (git ls-tree -r -z --long + git cat-file --batch), nie über einen Arbeitsbaum - byte-identisch zum Blob.
Glob-Abgleich in Python (repo_capture.glob_matches): */?/[...] überspannen kein /, ** als ganzes Segment beliebig viele Verzeichnisse (auch null), sonst wie *. (Bau) Wie Gits Pathspec matcht ein Glob ohne Wildcard zusätzlich als führendes Verzeichnis (docs trifft docs/x/b.md), einer mit Wildcard nicht (docs/x* trifft nichts darunter) - gegen git ls-files ':(glob)…' geprüft, und der Test vergleicht jede Musterzeile mit Git selbst.
Schreibt die passenden Dateien mit ihren Repo-Pfaden nach incoming/<bundle>/. Refusals vor jedem Schreiben: incoming/<bundle> existiert; nichts passt; Pfadbudget am späteren Ziel (raw/<YYYY>/<MM>/<bundle>/…, bei --update die bestehende Bundle-Adresse) überschritten - mit den betroffenen Pfaden. (Bau) Zusätzlich: ist <bundle> unter raw/ schon belegt (Erstaufruf), Refusal sofort statt erst beim Accept; ist der Halter ein erfasstes Bundle, nennt sie --update + --replaces-bundle. (Bau) Geschrieben wird in ein verstecktes Staging-Verzeichnis neben dem Ziel und per rename an seinen Platz - ganz oder gar nicht; raw pending überspringt das versteckte Verzeichnis.
Schreibt incoming/<bundle>/_capture.json: schema (1), repo, ref, commit, paths, captured (ISO-8601 UTC, Z), fidelity, authority, files (sortiert). Einzige Deklaration, dass und wie die Instanz dieses Repo verfolgt.
Ausschlüsse, mechanisch, je namentlich ausgegeben (nur unter den Pfaden, die auf einen Glob passen): erste Zeile beginnt mit <!-- wikitool:export (ein führendes UTF-8-BOM wird toleriert); Symlinks (120000); Submodule (160000); über web_capture.MAX_BYTES (aus ls-tree --long, ohne den Blob zu lesen); Pfade mit einem .-Segment; jede Datei namens _capture.json; Git-LFS-Zeiger; (Bau) Pfade, die kein UTF-8 sind.
Keine Zugangsdaten in wikitool: Git läuft über toolpaths.git() mit der Konfiguration und den Credential-Helfern des Hosts. Erlaubt nur ssh://, https:// und user@host:pfad; (Bau) eine URL mit Passwort im Userinfo wird bei https://undssh:// abgewiesen, ebenso eine URL mit - am Anfang oder Steuerzeichen. GIT_ALLOW_PROTOCOL wird bei jedem Aufruf aus repo_capture.ALLOWED_SCHEMES gesetzt und überschreibt die Umgebung. Unbeaufsichtigt: GIT_TERMINAL_PROMPT=0, stdin geschlossen, 120 s Timeout je Aufruf; (Bau) dazu -c core.askPass= und GIT_ASKPASS/SSH_ASKPASS entfernt (sonst fiele Git auf ein Askpass-Programm zurück, z. B. das eines Editors), SSH_ASKPASS_REQUIRE=never, GCM_INTERACTIVE=never, und auf POSIX eine eigene Session (kein steuerndes Terminal für ssh); beim Timeout wird die ganze Prozessgruppe beendet, sonst hielte ein ssh-Enkel die Pipe offen.
--update <raw-bundle> liest URL, Ref-Regel und Globs aus dem Manifest, erfasst nach incoming/<gleicher Name>/; --fidelity/--authority optional (Vorgabe: altes Manifest), angegeben überschreiben sie. Dieselbe URL-Prüfung wie beim Erstaufruf (read_manifest prüft URL, Ref-Regel, Globs und files-Pfade). URL/--ref/--path/--name neben --update sind eine Refusal.
2. raw accept für ein erfasstes Bundle
Erstaufnahme: raw accept incoming/<bundle>. Mit _capture.json kommen fidelity/authoritynur aus dem Manifest; Flags auf der Kommandozeile sind eine Refusal. (Bau) Der Ordner muss genau die Dateien des Manifests enthalten (sonst Refusal "no longer matches", auch in raw pending); die new source-Folgezeile lässt _capture.json aus raw_files= weg und nennt --file <pfad im bundle>. (Bau) Eine Datei _capture.json als lose Datei in incoming/ oder tiefer als an der Spitze eines Ordners ist eine Refusal (reservierter Name).
raw accept incoming/<bundle> --replaces-bundle <raw-bundle> ersetzt das Bundle an seiner bestehenden Adresse. Refusal ohne jede Änderung unter raw/ oder in incoming/, wenn: eine Seite kein _capture.json hat oder es nicht lesbar ist; die repo-URLs abweichen; der Ordnername abweicht; --page, --replaces(Bau) oder --fidelity/--authority mitgegeben ist; (Bau) der Ordner nicht zu seinem Manifest passt oder eine Seite versteckte Einträge/Symlinks enthält; ein neuer Pfad über dem Budget liegt; eine Datei des Bundles mehr als eine besitzende Quellseite hat.
Ablauf: D löschen → leer gewordene Verzeichnisse mit rmdir (nie rekursiv) → A/M per os.replace → unveränderte Dateien in incoming/ löschen → Manifest ersetzen → incoming/<bundle>/ per rmdir. Invariante: danach genau die Dateien des neuen Manifests plus _capture.json; incoming/<bundle>/ entfernt. Nicht atomar; ein Abbruch mittendrin wird gemeldet, nicht fortgesetzt.
raw_files: bleibt unverändert. Abweichende fidelity/authority werden auf den besitzenden Quellseiten überschrieben (_overwrite_capture_fields_on_page).
Ausgabe: alt → neu, Zählung, je Datei A/M/D gruppiert nach besitzender Quellseite mit deren zitierenden Seiten (neue Dateien: "not covered by any source page"). Folgezeilen in der echten Syntax (Bau: der Entwurf nannte touch "<Quellseite>"): D → tools/wikitool touch --page "<Quellseite>" --remove raw_files=<pfad>; A → touch --page "<einzige Quellseite des Bundles | <Source page>>" --add raw_files=<pfad> oder eine neue Quellseite; bleibt eine Seite ohne raw_files:, Verweis auf page-lifecycle.md § Delete.
--replaces und --page verweigern ein Ziel innerhalb eines erfassten Bundles (repo_capture.captured_bundle_of: nächstes Verzeichnis unter raw/ mit _capture.json). Die Refusal nennt raw capture --update + --replaces-bundle. Die ON-FAILURE-Reaktion bei belegtem Ordnernamen nennt für ein erfasstes Bundle ebenfalls --replaces-bundle (Nachziehung 8ce202a).
3. raw status (nur lesend, schreibt nur in den Cache)
Liest jedes _capture.json unter raw/ (versteckte Pfade ausgenommen), löst je Repo die Ref-Regel per ls-remote auf. Nur bei abweichendem Commit wird der neue geholt und mit repo_capture.select_files - derselben Funktion wie raw capture - gegen den Bundle-Inhalt unter raw/ abgeglichen.
Ausgabe je geändertem Repo alt → neu, A/M/D gruppiert wie oben, plus die Zeilen raw capture --update / raw accept … --replaces-bundle. Unveränderte Repos (auch bewegter Commit ohne Änderung in den Globs) nur als Zählung "N unchanged". Fehlerzeilen für nicht erreichbar / Manifest oder URL abgewiesen / kein passendes Ref; Exit 0.
--json: je Bundle bundle, repo, ref, old, new, changed, files ([{path, status}], Pfade relativ zur Instanz, raw/…), error.
4. Abdeckung:config.RAW_IGNORE_NAMES = {".gitkeep", ".DS_Store", "_capture.json"} (jede Tiefe); neu config.RAW_IGNORE_TOP_LEVEL = {"CONTRACT.md"}, nur direkt in raw/.
Akzeptanzkriterien
Belegt durch tools/chemenu/tests/test_raw_capture.py (54 Tests), grün lokal und in CI-Lauf 545 (und erneut in 547).
Nach raw capture + raw accept enthält das Bundle genau die passenden, nicht ausgeschlossenen Dateien zum Manifest-Commit, byte-identisch zum Blob (auch mit core.autocrlf=true), mit Repo-Pfaden, plus _capture.json; *.yaml neben docs/**/*.md fehlt. docs/**/*.md trifft docs/a.md und docs/x/y/b.md, docs/*.md trifft docs/x/b.md nicht.
Nach --replaces-bundle dasselbe für den neuen Commit; gelöschte Dateien und leere Verzeichnisse weg; Bundle-Pfad unverändert; Manifest nennt den neuen Commit; kein raw_files: verändert.
--replaces-bundle mit abweichender repo-URL, abweichendem Ordnernamen oder einem Bundle ohne _capture.json wird abgewiesen, ohne Änderung unter raw/ oder in incoming/.
raw accept --replaces und --page mit einem Ziel in einem erfassten Bundle werden abgewiesen; außerhalb unverändert.
Export-Kopf, .github/, Symlink, Submodul, LFS-Zeiger und Repo-Datei _capture.json werden nie erfasst und je namentlich gemeldet; raw status meldet keine davon als A.
ext::/fd::/file:// und https:// mit Passwort werden von raw capture, --update und raw status abgewiesen; ein ext::-Transport führt nachweislich keinen Befehl aus (URL-Ebene und Git-Ebene getestet, mit Kontrolltest, dass derselbe Befehl bei freigegebenem ext läuft).
Kein Kommando liest oder schreibt Zugangsdaten; nach einer abgewiesenen Token-URL steht das Token in keiner Datei. Ein Repo, das nach Zugangsdaten fragt (lokaler HTTP-401-Server), blockiert nicht und erscheint als "not reachable: git ls-remote failed: … could not read Username … terminal prompts disabled".
raw status: korrekte A/M/D-Liste, nach --replaces-bundle still; Änderungen außerhalb der Globs still; Tag-Muster folgt nur neuen Tags; nicht erreichbares Repo ist eine Zeile neben den übrigen.
_capture.json nicht in sources coverage; CONTRACT.md in einem Bundle erscheint, solange unabgedeckt; raw/CONTRACT.md weiterhin nicht.
Ohne _capture.json unter raw/ verhält sich jedes bestehende Kommando unverändert - bestehende Verhaltens-Tests grün ohne Anpassung. (Bau) Angepasst wurden nur zwei Register, die jedes neue Kommando nachziehen muss: die feste Menge der Netz-Kommandos in test_cli.py und cli_contract.GROUPS.
Tests gegen lokale Repositories über file://, freigeschaltet nur durch die Fixture (monkeypatch auf repo_capture.ALLOWED_SCHEMES). pytest, docs verify, instructions verify grün; CI grün (Lauf 545).
raw status an einem echten Repo ausprobiert, an diesem Repo selbst, in einem Scratch-Root (CHEMENU_ROOT, nicht in der Instanz): raw capture ssh://git@gitea.nehmer.net:10101/torben/chemenu.git --ref 'v*' --path README.md --path 'docs/**' --path raw/CONTRACT.md --name chemenu-docs löste v* zu v7.0.0 (4446424e) auf und erfasste 9 Dateien; README.md, raw/CONTRACT.md, docs/version-model.md per cmp byte-identisch zu git show v7.0.0:<datei>. Nach raw accept meldete raw status "1 unchanged" (old = new = 4446424e, ~0,7 s). Eine echte Drift am echten Repo war nicht vorführbar, weil kein neueres Release-Tag existiert; der Drift-Pfad ist durch die Tests belegt.
tools/chemenu/commands/raw_cmd.py: raw capture, raw status, --replaces-bundle (als Annotated-Option, damit direkte Aufrufe der bestehenden Tests ein echtes None sehen), Manifest-Pfad beim Accept, Refusals für --replaces/--page, reservierter Name, raw pending prüft erfasste Ordner gegen ihr Manifest, Records.
.gitignore (/tools/.wikitool_capture/), Kanarie in docs_verify.py; (Bau)dist_cmd.TOOLS_EXCLUDE_DIRS um .wikitool_capture ergänzt, sonst exportierte dist export den Cache.
raw/CONTRACT.md: Abschnitt "Getting a repository in: raw capture", Capture fields, Rules § Replaceable as a whole, § Getting a file in (Capture-Felder eines erfassten Ordners).
instructions/wiki-ingest/SKILL.md: Schritt 1 (Large-Tree-Abzweig) und 5 (erfasster Ordner ohne Flags), Schritt 7 (--file mit bundle-relativem Pfad), Entscheidungspunkt "neue Fassung eines erfassten Bundles", Kommandoliste.
--minor: neue Kommandos, neue optionale Manifestdatei, ein bisher abgewiesener raw accept-Aufruf wird erlaubt. Drop-in in beide Richtungen: ohne _capture.json kein geändertes Verhalten; der verankerte CONTRACT.md-Ausschluss kann einen neuen Abdeckungsbefund über eine bisher übersehene Datei erzeugen; eine ältere Version meldet _capture.json als unabgedeckt. Gelandet als Bumps im laufenden Kandidaten: 8.0.0-beta.41 (311c8ee, das Feature) und 8.0.0-beta.42 (8ce202a, --patch, die Nachziehung aus der Abschlussphase), je mit Eintrag in CHANGES.md.
Teil A aus dem Design #171 (dort Entscheidungen und verworfene Alternativen). Folge-Issues: #178 (Intake-Lauf, baut hierauf auf - nutzt `raw status --json` und `raw capture --update`), #179 (Leitlinien-Export, definiert den Export-Kopf, den diese Erfassung ausschließt). Gegen den Baum geprüft am 2026-10-04 (Stand `8ff22ad`).
**Abgeschlossen (2026-10-05).** Gebaut und publiziert als `8.0.0-beta.41` in Commit `311c8ee`, **verifiziert durch CI-Lauf [545](https://gitea.nehmer.net/torben/chemenu/actions/runs/545)** (Jobs `verify` und `pwsh` grün; Release-Lauf [546](https://gitea.nehmer.net/torben/chemenu/actions/runs/546) grün). Lokal vorher: `pytest` 2272 passed / 3 skipped, die Raw-Suiten zusätzlich gegen eine leere Maschine (`env -i`), `docs verify`, `instructions verify`. In der Abschlussphase kam eine Nachziehung dazu (`8.0.0-beta.42`, Commit `8ce202a`, CI-Lauf [547](https://gitea.nehmer.net/torben/chemenu/actions/runs/547) und Release-Lauf [548](https://gitea.nehmer.net/torben/chemenu/actions/runs/548) grün): Die ON-FAILURE-Reaktion von `raw accept incoming/<folder>` bei belegtem Namen sagte noch "rename the folder - there is no `--replaces` for a folder" und hätte eine neue Fassung eines erfassten Bundles damit zur zweiten, getrennten Quelle gemacht. Sie nennt jetzt `--replaces-bundle`, wenn ein erfasstes Bundle den Namen hält. `raw/CONTRACT.md` § "Getting a file in" sagt jetzt, dass ein erfasster Ordner die Capture-Felder aus seinem Manifest nimmt. Keine `docs/`-Seite wurde veraltet: `docs/pipeline-rationale.md` begründet die Vertrauensgrenze an der Promotion nach `raw/`, und `raw capture` landet wie jede andere Aufnahme in `incoming/`. Kommandonamen wie entworfen (`raw capture`, `raw status`, `raw accept --replaces-bundle`). Was beim Bau über den Entwurf hinaus präzisiert oder ergänzt wurde, steht an seiner Stelle und ist mit *(Bau)* markiert.
## Problem
Doku aus einem Git-Repository kam nur von Hand in eine Instanz: Dateien nach `incoming/` kopieren, umbenennen, um der globalen Stem-Regel auszuweichen, pro Unterordner ein Bundle. Nichts hielt fest, aus welchem Repo und Commit ein Bundle stammte, und nichts meldete, wenn sich das Repo seitdem geändert hatte. Ein Ordner-Bundle ließ sich außerdem nicht als Ganzes durch eine neue Fassung ersetzen (`raw_cmd._folder_collision_message`: "A folder has no --replaces"; `--page` scheiterte an verschachtelten Bundles, weil `_plan_file_moves` alle `raw_files:` in einem Verzeichnis verlangt).
**Befund nebenbei, mitbehoben:** `config.RAW_IGNORE_NAMES` (`{".gitkeep", ".DS_Store", "CONTRACT.md"}`) wurde in `config.iter_raw_files` per Dateiname in jeder Tiefe angewandt. Gemeint war nur `raw/CONTRACT.md`, getroffen wurde jede `CONTRACT.md` unter `raw/`. Behoben über `config.RAW_IGNORE_TOP_LEVEL` (Punkt 4).
## Entwurf (umgesetzt)
**1. `raw capture <repo-url> --ref <ref-regel> --path <glob>... --name <bundle> --fidelity <v> --authority <v>`** - Kern in `tools/chemenu/repo_capture.py`
- Ref-Regel: ein Branchname (`main`) oder ein Tag-Muster (`v*`, erkannt an `*?[` → das jüngste passende Tag nach Versionssortierung, `-v:refname`-Semantik ohne Suffix-Konfiguration). Aufgelöst per `git ls-remote --heads --tags` zu genau einem Commit; bei einem annotierten Tag der gepeelte Commit (`^{}`-Zeile).
- Holt diesen Commit **per Ref-Name** flach (`--depth 1`, Refspec `+<ref>:refs/wikitool/fetched`) in einen gitignorierten Cache: `config.ROOT/tools/.wikitool_capture/<sha256 der URL>/`, ein Bare-Repo pro URL, Zugriff unter `chemenu.filelock` (Lockdatei `<hash>.lock` daneben). Nie per SHA. Alle Git-Aufrufe laufen mit `--git-dir=<cache>`, damit weder ein umgebendes Repo noch ein geerbtes `GIT_DIR` greift.
- **Dateien werden als Blobs gelesen** (`git ls-tree -r -z --long` + `git cat-file --batch`), nie über einen Arbeitsbaum - byte-identisch zum Blob.
- **Glob-Abgleich in Python** (`repo_capture.glob_matches`): `*`/`?`/`[...]` überspannen kein `/`, `**` als ganzes Segment beliebig viele Verzeichnisse (auch null), sonst wie `*`. *(Bau)* Wie Gits Pathspec matcht ein Glob **ohne** Wildcard zusätzlich als führendes Verzeichnis (`docs` trifft `docs/x/b.md`), einer mit Wildcard nicht (`docs/x*` trifft nichts darunter) - gegen `git ls-files ':(glob)…'` geprüft, und der Test vergleicht jede Musterzeile mit Git selbst.
- Schreibt die passenden Dateien mit ihren Repo-Pfaden nach `incoming/<bundle>/`. Refusals vor jedem Schreiben: `incoming/<bundle>` existiert; nichts passt; Pfadbudget am späteren Ziel (`raw/<YYYY>/<MM>/<bundle>/…`, bei `--update` die bestehende Bundle-Adresse) überschritten - mit den betroffenen Pfaden. *(Bau)* Zusätzlich: ist `<bundle>` unter `raw/` schon belegt (Erstaufruf), Refusal sofort statt erst beim Accept; ist der Halter ein erfasstes Bundle, nennt sie `--update` + `--replaces-bundle`. *(Bau)* Geschrieben wird in ein verstecktes Staging-Verzeichnis neben dem Ziel und per `rename` an seinen Platz - ganz oder gar nicht; `raw pending` überspringt das versteckte Verzeichnis.
- Schreibt `incoming/<bundle>/_capture.json`: `schema` (1), `repo`, `ref`, `commit`, `paths`, `captured` (ISO-8601 UTC, `Z`), `fidelity`, `authority`, `files` (sortiert). Einzige Deklaration, dass und wie die Instanz dieses Repo verfolgt.
- **Ausschlüsse, mechanisch, je namentlich ausgegeben** (nur unter den Pfaden, die auf einen Glob passen): erste Zeile beginnt mit `<!-- wikitool:export` (ein führendes UTF-8-BOM wird toleriert); Symlinks (`120000`); Submodule (`160000`); über `web_capture.MAX_BYTES` (aus `ls-tree --long`, ohne den Blob zu lesen); Pfade mit einem `.`-Segment; jede Datei namens `_capture.json`; Git-LFS-Zeiger; *(Bau)* Pfade, die kein UTF-8 sind.
- **Keine Zugangsdaten in `wikitool`:** Git läuft über `toolpaths.git()` mit der Konfiguration und den Credential-Helfern des Hosts. Erlaubt nur `ssh://`, `https://` und `user@host:pfad`; *(Bau)* eine URL mit Passwort im Userinfo wird bei `https://` **und** `ssh://` abgewiesen, ebenso eine URL mit `-` am Anfang oder Steuerzeichen. `GIT_ALLOW_PROTOCOL` wird bei jedem Aufruf aus `repo_capture.ALLOWED_SCHEMES` gesetzt und überschreibt die Umgebung. Unbeaufsichtigt: `GIT_TERMINAL_PROMPT=0`, stdin geschlossen, 120 s Timeout je Aufruf; *(Bau)* dazu `-c core.askPass=` und `GIT_ASKPASS`/`SSH_ASKPASS` entfernt (sonst fiele Git auf ein Askpass-Programm zurück, z. B. das eines Editors), `SSH_ASKPASS_REQUIRE=never`, `GCM_INTERACTIVE=never`, und auf POSIX eine eigene Session (kein steuerndes Terminal für `ssh`); beim Timeout wird die ganze Prozessgruppe beendet, sonst hielte ein `ssh`-Enkel die Pipe offen.
- **`--update <raw-bundle>`** liest URL, Ref-Regel und Globs aus dem Manifest, erfasst nach `incoming/<gleicher Name>/`; `--fidelity`/`--authority` optional (Vorgabe: altes Manifest), angegeben überschreiben sie. Dieselbe URL-Prüfung wie beim Erstaufruf (`read_manifest` prüft URL, Ref-Regel, Globs und `files`-Pfade). URL/`--ref`/`--path`/`--name` neben `--update` sind eine Refusal.
**2. `raw accept` für ein erfasstes Bundle**
- Erstaufnahme: `raw accept incoming/<bundle>`. Mit `_capture.json` kommen `fidelity`/`authority` **nur** aus dem Manifest; Flags auf der Kommandozeile sind eine Refusal. *(Bau)* Der Ordner muss genau die Dateien des Manifests enthalten (sonst Refusal "no longer matches", auch in `raw pending`); die `new source`-Folgezeile lässt `_capture.json` aus `raw_files=` weg und nennt `--file <pfad im bundle>`. *(Bau)* Eine Datei `_capture.json` als lose Datei in `incoming/` oder tiefer als an der Spitze eines Ordners ist eine Refusal (reservierter Name).
- **`raw accept incoming/<bundle> --replaces-bundle <raw-bundle>`** ersetzt das Bundle an seiner bestehenden Adresse. Refusal ohne jede Änderung unter `raw/` oder in `incoming/`, wenn: eine Seite kein `_capture.json` hat oder es nicht lesbar ist; die `repo`-URLs abweichen; der Ordnername abweicht; `--page`, `--replaces` *(Bau)* oder `--fidelity`/`--authority` mitgegeben ist; *(Bau)* der Ordner nicht zu seinem Manifest passt oder eine Seite versteckte Einträge/Symlinks enthält; ein neuer Pfad über dem Budget liegt; eine Datei des Bundles mehr als eine besitzende Quellseite hat.
- Ablauf: `D` löschen → leer gewordene Verzeichnisse mit `rmdir` (nie rekursiv) → `A`/`M` per `os.replace` → unveränderte Dateien in `incoming/` löschen → Manifest ersetzen → `incoming/<bundle>/` per `rmdir`. **Invariante:** danach genau die Dateien des neuen Manifests plus `_capture.json`; `incoming/<bundle>/` entfernt. Nicht atomar; ein Abbruch mittendrin wird gemeldet, nicht fortgesetzt.
- `raw_files:` bleibt unverändert. Abweichende `fidelity`/`authority` werden auf den besitzenden Quellseiten überschrieben (`_overwrite_capture_fields_on_page`).
- Ausgabe: `alt → neu`, Zählung, je Datei `A`/`M`/`D` gruppiert nach besitzender Quellseite mit deren zitierenden Seiten (neue Dateien: "not covered by any source page"). Folgezeilen in der echten Syntax *(Bau: der Entwurf nannte `touch "<Quellseite>"`)*: `D` → `tools/wikitool touch --page "<Quellseite>" --remove raw_files=<pfad>`; `A` → `touch --page "<einzige Quellseite des Bundles | <Source page>>" --add raw_files=<pfad>` oder eine neue Quellseite; bleibt eine Seite ohne `raw_files:`, Verweis auf `page-lifecycle.md` § Delete.
- **`--replaces` und `--page` verweigern ein Ziel innerhalb eines erfassten Bundles** (`repo_capture.captured_bundle_of`: nächstes Verzeichnis unter `raw/` mit `_capture.json`). Die Refusal nennt `raw capture --update` + `--replaces-bundle`. Die ON-FAILURE-Reaktion bei belegtem Ordnernamen nennt für ein erfasstes Bundle ebenfalls `--replaces-bundle` (Nachziehung `8ce202a`).
**3. `raw status` (nur lesend, schreibt nur in den Cache)**
- Liest jedes `_capture.json` unter `raw/` (versteckte Pfade ausgenommen), löst je Repo die Ref-Regel per `ls-remote` auf. Nur bei abweichendem Commit wird der neue geholt und mit `repo_capture.select_files` - derselben Funktion wie `raw capture` - gegen den Bundle-Inhalt unter `raw/` abgeglichen.
- Ausgabe je geändertem Repo `alt → neu`, A/M/D gruppiert wie oben, plus die Zeilen `raw capture --update` / `raw accept … --replaces-bundle`. Unveränderte Repos (auch bewegter Commit ohne Änderung in den Globs) nur als Zählung "N unchanged". Fehlerzeilen für nicht erreichbar / Manifest oder URL abgewiesen / kein passendes Ref; Exit 0.
- `--json`: je Bundle `bundle`, `repo`, `ref`, `old`, `new`, `changed`, `files` (`[{path, status}]`, Pfade relativ zur Instanz, `raw/…`), `error`.
**4. Abdeckung:** `config.RAW_IGNORE_NAMES` = `{".gitkeep", ".DS_Store", "_capture.json"}` (jede Tiefe); neu `config.RAW_IGNORE_TOP_LEVEL` = `{"CONTRACT.md"}`, nur direkt in `raw/`.
## Akzeptanzkriterien
Belegt durch `tools/chemenu/tests/test_raw_capture.py` (54 Tests), grün lokal und in CI-Lauf 545 (und erneut in 547).
- [x] Nach `raw capture` + `raw accept` enthält das Bundle genau die passenden, nicht ausgeschlossenen Dateien zum Manifest-Commit, byte-identisch zum Blob (auch mit `core.autocrlf=true`), mit Repo-Pfaden, plus `_capture.json`; `*.yaml` neben `docs/**/*.md` fehlt. `docs/**/*.md` trifft `docs/a.md` und `docs/x/y/b.md`, `docs/*.md` trifft `docs/x/b.md` nicht.
- [x] Nach `--replaces-bundle` dasselbe für den neuen Commit; gelöschte Dateien und leere Verzeichnisse weg; Bundle-Pfad unverändert; Manifest nennt den neuen Commit; kein `raw_files:` verändert.
- [x] `--replaces-bundle` mit abweichender `repo`-URL, abweichendem Ordnernamen oder einem Bundle ohne `_capture.json` wird abgewiesen, ohne Änderung unter `raw/` oder in `incoming/`.
- [x] `raw accept --replaces` und `--page` mit einem Ziel in einem erfassten Bundle werden abgewiesen; außerhalb unverändert.
- [x] Export-Kopf, `.github/`, Symlink, Submodul, LFS-Zeiger und Repo-Datei `_capture.json` werden nie erfasst und je namentlich gemeldet; `raw status` meldet keine davon als `A`.
- [x] `ext::`/`fd::`/`file://` und `https://` mit Passwort werden von `raw capture`, `--update` und `raw status` abgewiesen; ein `ext::`-Transport führt nachweislich keinen Befehl aus (URL-Ebene und Git-Ebene getestet, mit Kontrolltest, dass derselbe Befehl bei freigegebenem `ext` läuft).
- [x] Kein Kommando liest oder schreibt Zugangsdaten; nach einer abgewiesenen Token-URL steht das Token in keiner Datei. Ein Repo, das nach Zugangsdaten fragt (lokaler HTTP-401-Server), blockiert nicht und erscheint als "not reachable: git ls-remote failed: … could not read Username … terminal prompts disabled".
- [x] `raw status`: korrekte A/M/D-Liste, nach `--replaces-bundle` still; Änderungen außerhalb der Globs still; Tag-Muster folgt nur neuen Tags; nicht erreichbares Repo ist eine Zeile neben den übrigen.
- [x] `_capture.json` nicht in `sources coverage`; `CONTRACT.md` in einem Bundle erscheint, solange unabgedeckt; `raw/CONTRACT.md` weiterhin nicht.
- [x] Ohne `_capture.json` unter `raw/` verhält sich jedes bestehende Kommando unverändert - bestehende Verhaltens-Tests grün ohne Anpassung. *(Bau)* Angepasst wurden nur zwei Register, die jedes neue Kommando nachziehen muss: die feste Menge der Netz-Kommandos in `test_cli.py` und `cli_contract.GROUPS`.
- [x] Tests gegen lokale Repositories über `file://`, freigeschaltet nur durch die Fixture (`monkeypatch` auf `repo_capture.ALLOWED_SCHEMES`). `pytest`, `docs verify`, `instructions verify` grün; CI grün (Lauf 545).
- [x] `raw status` an einem echten Repo ausprobiert, an diesem Repo selbst, in einem Scratch-Root (`CHEMENU_ROOT`, nicht in der Instanz): `raw capture ssh://git@gitea.nehmer.net:10101/torben/chemenu.git --ref 'v*' --path README.md --path 'docs/**' --path raw/CONTRACT.md --name chemenu-docs` löste `v*` zu `v7.0.0` (`4446424e`) auf und erfasste 9 Dateien; `README.md`, `raw/CONTRACT.md`, `docs/version-model.md` per `cmp` byte-identisch zu `git show v7.0.0:<datei>`. Nach `raw accept` meldete `raw status` "1 unchanged" (`old` = `new` = `4446424e`, ~0,7 s). Eine echte Drift am echten Repo war nicht vorführbar, weil kein neueres Release-Tag existiert; der Drift-Pfad ist durch die Tests belegt.
## Betroffene Stellen
- `tools/chemenu/repo_capture.py` (neu): URL-Prüfung, Git-Aufruf, Cache, Ref-Auflösung, Glob, Auswahl/Ausschlüsse, Manifest, Diff.
- `tools/chemenu/commands/raw_cmd.py`: `raw capture`, `raw status`, `--replaces-bundle` (als `Annotated`-Option, damit direkte Aufrufe der bestehenden Tests ein echtes `None` sehen), Manifest-Pfad beim Accept, Refusals für `--replaces`/`--page`, reservierter Name, `raw pending` prüft erfasste Ordner gegen ihr Manifest, Records.
- `tools/chemenu/config.py`: `RAW_IGNORE_NAMES`, `RAW_IGNORE_TOP_LEVEL`, `iter_raw_files`.
- `.gitignore` (`/tools/.wikitool_capture/`), Kanarie in `docs_verify.py`; *(Bau)* `dist_cmd.TOOLS_EXCLUDE_DIRS` um `.wikitool_capture` ergänzt, sonst exportierte `dist export` den Cache.
- `cli_contract.GROUPS`, `tools/CONTRACT.md` (per `docs contract --apply`), `test_cli.py` (Netz-Kommandos).
- `raw/CONTRACT.md`: Abschnitt "Getting a repository in: `raw capture`", Capture fields, Rules § Replaceable as a whole, § Getting a file in (Capture-Felder eines erfassten Ordners).
- `instructions/wiki-ingest/SKILL.md`: Schritt 1 (Large-Tree-Abzweig) und 5 (erfasster Ordner ohne Flags), Schritt 7 (`--file` mit bundle-relativem Pfad), Entscheidungspunkt "neue Fassung eines erfassten Bundles", Kommandoliste.
- `README.md`, `tools/README.md`, `CHANGES.md`, `tools/chemenu/tests/test_raw_capture.py`.
## Versionsteil
**`--minor`**: neue Kommandos, neue optionale Manifestdatei, ein bisher abgewiesener `raw accept`-Aufruf wird erlaubt. Drop-in in beide Richtungen: ohne `_capture.json` kein geändertes Verhalten; der verankerte `CONTRACT.md`-Ausschluss kann einen neuen Abdeckungsbefund über eine bisher übersehene Datei erzeugen; eine ältere Version meldet `_capture.json` als unabgedeckt. Gelandet als Bumps im laufenden Kandidaten: `8.0.0-beta.41` (`311c8ee`, das Feature) und `8.0.0-beta.42` (`8ce202a`, `--patch`, die Nachziehung aus der Abschlussphase), je mit Eintrag in `CHANGES.md`.
Changelog: Body gegen den Baum (8ff22ad) geprüft und präzisiert. Neu: Befund RAW_IGNORE_NAMES blendet jede CONTRACT.md unter raw/ aus - wird hier mitbehoben (Punkt 4). Korrigiert: _capture.json ist nicht "wie der Header eines raw fetch-Bundles" (der steht in der .md und ist abgedeckt), sondern ein reservierter Name in iter_raw_files; raw status vergleicht den neuen Commit gegen den Bundle-Inhalt unter raw/ statt gegen den alten Commit (kein Fetch eines unbeworbenen SHA); page-lifecycle.md gilt nur für eine leer werdende Quellseite, A/D laufen über touch --add/--remove raw_files=. Ergänzt: Blob-Lesen statt Checkout (autocrlf), Glob-Abgleich in Python (ls-tree kann keine :(glob)-Pathspec, full_match erst ab 3.13), Ausschluss versteckter Pfade/LFS-Zeiger/_capture.json, Refusal für URLs mit Passwort, GIT_TERMINAL_PROMPT=0 + Timeout, Cache-Pfad /tools/.wikitool_capture/, Manifest als einzige Quelle für fidelity/authority beim Accept, Refusal für --replaces/--page in erfassten Bundles, Ausgabe gruppiert je Quellseite, --json-Felder, Kriterium "an echtem Repo ausprobiert" aus #171 übernommen. Versionsteil unverändert --minor.
**Changelog:** Body gegen den Baum (`8ff22ad`) geprüft und präzisiert. Neu: Befund `RAW_IGNORE_NAMES` blendet jede `CONTRACT.md` unter `raw/` aus - wird hier mitbehoben (Punkt 4). Korrigiert: `_capture.json` ist nicht "wie der Header eines `raw fetch`-Bundles" (der steht in der `.md` und ist abgedeckt), sondern ein reservierter Name in `iter_raw_files`; `raw status` vergleicht den neuen Commit gegen den Bundle-Inhalt unter `raw/` statt gegen den alten Commit (kein Fetch eines unbeworbenen SHA); `page-lifecycle.md` gilt nur für eine leer werdende Quellseite, `A`/`D` laufen über `touch --add/--remove raw_files=`. Ergänzt: Blob-Lesen statt Checkout (autocrlf), Glob-Abgleich in Python (`ls-tree` kann keine `:(glob)`-Pathspec, `full_match` erst ab 3.13), Ausschluss versteckter Pfade/LFS-Zeiger/`_capture.json`, Refusal für URLs mit Passwort, `GIT_TERMINAL_PROMPT=0` + Timeout, Cache-Pfad `/tools/.wikitool_capture/`, Manifest als einzige Quelle für `fidelity`/`authority` beim Accept, Refusal für `--replaces`/`--page` in erfassten Bundles, Ausgabe gruppiert je Quellseite, `--json`-Felder, Kriterium "an echtem Repo ausprobiert" aus #171 übernommen. Versionsteil unverändert `--minor`.
Changelog: Gebaut und publiziert (8.0.0-beta.41, 311c8ee), verifiziert durch CI-Lauf 545 (Release-Lauf 546 grün); alle Kriterien abgehakt, das Echt-Repo-Kriterium mit Ergebnis (v* → v7.0.0, 9 Dateien byte-identisch, raw status "1 unchanged"). Neu im Body, mit (Bau) markiert: Glob ohne Wildcard matcht wie bei Git als führendes Verzeichnis; Staging-Verzeichnis mit atomarem rename; frühe Refusal bei belegtem Bundlenamen; Ordner muss zum Manifest passen (Accept, --replaces-bundle, raw pending); _capture.json als lose oder verschachtelte Datei verweigert; --replaces-bundle verweigert auch --fidelity/--authority; Passwort-URL auch bei ssh:// abgewiesen; zusätzliche Schutzschichten gegen Prompts (core.askPass=, Askpass-Variablen, eigene Session, Prozessgruppen-Kill); dist_cmd.TOOLS_EXCLUDE_DIRS schließt den Cache aus. Korrigiert: Folgezeilen nutzen die echte touch --page "<Titel>"-Syntax. Angepasst wurden nur die Register cli_contract.GROUPS und die Netz-Kommando-Menge in test_cli.py.
**Changelog:** Gebaut und publiziert (`8.0.0-beta.41`, `311c8ee`), verifiziert durch CI-Lauf 545 (Release-Lauf 546 grün); alle Kriterien abgehakt, das Echt-Repo-Kriterium mit Ergebnis (`v*` → `v7.0.0`, 9 Dateien byte-identisch, `raw status` "1 unchanged"). Neu im Body, mit *(Bau)* markiert: Glob ohne Wildcard matcht wie bei Git als führendes Verzeichnis; Staging-Verzeichnis mit atomarem `rename`; frühe Refusal bei belegtem Bundlenamen; Ordner muss zum Manifest passen (Accept, `--replaces-bundle`, `raw pending`); `_capture.json` als lose oder verschachtelte Datei verweigert; `--replaces-bundle` verweigert auch `--fidelity`/`--authority`; Passwort-URL auch bei `ssh://` abgewiesen; zusätzliche Schutzschichten gegen Prompts (`core.askPass=`, Askpass-Variablen, eigene Session, Prozessgruppen-Kill); `dist_cmd.TOOLS_EXCLUDE_DIRS` schließt den Cache aus. Korrigiert: Folgezeilen nutzen die echte `touch --page "<Titel>"`-Syntax. Angepasst wurden nur die Register `cli_contract.GROUPS` und die Netz-Kommando-Menge in `test_cli.py`.
Changelog: Body in den Endzustand gebracht (Problem in der Vergangenheit, "Abgeschlossen" statt offener Phase). Neu: Nachziehung aus der Abschlussphase, 8.0.0-beta.42 / 8ce202a, CI-Lauf 547 grün - die ON-FAILURE-Reaktion bei belegtem Ordnernamen nennt für ein erfasstes Bundle --replaces-bundle statt "rename", und raw/CONTRACT.md § Getting a file in nennt das Manifest als Quelle der Capture-Felder. docs/ geprüft, keine Seite veraltet. Geschlossen.
**Changelog:** Body in den Endzustand gebracht (Problem in der Vergangenheit, "Abgeschlossen" statt offener Phase). Neu: Nachziehung aus der Abschlussphase, `8.0.0-beta.42` / `8ce202a`, CI-Lauf 547 grün - die ON-FAILURE-Reaktion bei belegtem Ordnernamen nennt für ein erfasstes Bundle `--replaces-bundle` statt "rename", und `raw/CONTRACT.md` § Getting a file in nennt das Manifest als Quelle der Capture-Felder. `docs/` geprüft, keine Seite veraltet. Geschlossen.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Teil A aus dem Design #171 (dort Entscheidungen und verworfene Alternativen). Folge-Issues: #178 (Intake-Lauf, baut hierauf auf - nutzt
raw status --jsonundraw capture --update), #179 (Leitlinien-Export, definiert den Export-Kopf, den diese Erfassung ausschließt). Gegen den Baum geprüft am 2026-10-04 (Stand8ff22ad).Abgeschlossen (2026-10-05). Gebaut und publiziert als
8.0.0-beta.41in Commit311c8ee, verifiziert durch CI-Lauf 545 (Jobsverifyundpwshgrün; Release-Lauf 546 grün). Lokal vorher:pytest2272 passed / 3 skipped, die Raw-Suiten zusätzlich gegen eine leere Maschine (env -i),docs verify,instructions verify. In der Abschlussphase kam eine Nachziehung dazu (8.0.0-beta.42, Commit8ce202a, CI-Lauf 547 und Release-Lauf 548 grün): Die ON-FAILURE-Reaktion vonraw accept incoming/<folder>bei belegtem Namen sagte noch "rename the folder - there is no--replacesfor a folder" und hätte eine neue Fassung eines erfassten Bundles damit zur zweiten, getrennten Quelle gemacht. Sie nennt jetzt--replaces-bundle, wenn ein erfasstes Bundle den Namen hält.raw/CONTRACT.md§ "Getting a file in" sagt jetzt, dass ein erfasster Ordner die Capture-Felder aus seinem Manifest nimmt. Keinedocs/-Seite wurde veraltet:docs/pipeline-rationale.mdbegründet die Vertrauensgrenze an der Promotion nachraw/, undraw capturelandet wie jede andere Aufnahme inincoming/. Kommandonamen wie entworfen (raw capture,raw status,raw accept --replaces-bundle). Was beim Bau über den Entwurf hinaus präzisiert oder ergänzt wurde, steht an seiner Stelle und ist mit (Bau) markiert.Problem
Doku aus einem Git-Repository kam nur von Hand in eine Instanz: Dateien nach
incoming/kopieren, umbenennen, um der globalen Stem-Regel auszuweichen, pro Unterordner ein Bundle. Nichts hielt fest, aus welchem Repo und Commit ein Bundle stammte, und nichts meldete, wenn sich das Repo seitdem geändert hatte. Ein Ordner-Bundle ließ sich außerdem nicht als Ganzes durch eine neue Fassung ersetzen (raw_cmd._folder_collision_message: "A folder has no --replaces";--pagescheiterte an verschachtelten Bundles, weil_plan_file_movesalleraw_files:in einem Verzeichnis verlangt).Befund nebenbei, mitbehoben:
config.RAW_IGNORE_NAMES({".gitkeep", ".DS_Store", "CONTRACT.md"}) wurde inconfig.iter_raw_filesper Dateiname in jeder Tiefe angewandt. Gemeint war nurraw/CONTRACT.md, getroffen wurde jedeCONTRACT.mdunterraw/. Behoben überconfig.RAW_IGNORE_TOP_LEVEL(Punkt 4).Entwurf (umgesetzt)
1.
raw capture <repo-url> --ref <ref-regel> --path <glob>... --name <bundle> --fidelity <v> --authority <v>- Kern intools/chemenu/repo_capture.pymain) oder ein Tag-Muster (v*, erkannt an*?[→ das jüngste passende Tag nach Versionssortierung,-v:refname-Semantik ohne Suffix-Konfiguration). Aufgelöst pergit ls-remote --heads --tagszu genau einem Commit; bei einem annotierten Tag der gepeelte Commit (^{}-Zeile).--depth 1, Refspec+<ref>:refs/wikitool/fetched) in einen gitignorierten Cache:config.ROOT/tools/.wikitool_capture/<sha256 der URL>/, ein Bare-Repo pro URL, Zugriff unterchemenu.filelock(Lockdatei<hash>.lockdaneben). Nie per SHA. Alle Git-Aufrufe laufen mit--git-dir=<cache>, damit weder ein umgebendes Repo noch ein geerbtesGIT_DIRgreift.git ls-tree -r -z --long+git cat-file --batch), nie über einen Arbeitsbaum - byte-identisch zum Blob.repo_capture.glob_matches):*/?/[...]überspannen kein/,**als ganzes Segment beliebig viele Verzeichnisse (auch null), sonst wie*. (Bau) Wie Gits Pathspec matcht ein Glob ohne Wildcard zusätzlich als führendes Verzeichnis (docstrifftdocs/x/b.md), einer mit Wildcard nicht (docs/x*trifft nichts darunter) - gegengit ls-files ':(glob)…'geprüft, und der Test vergleicht jede Musterzeile mit Git selbst.incoming/<bundle>/. Refusals vor jedem Schreiben:incoming/<bundle>existiert; nichts passt; Pfadbudget am späteren Ziel (raw/<YYYY>/<MM>/<bundle>/…, bei--updatedie bestehende Bundle-Adresse) überschritten - mit den betroffenen Pfaden. (Bau) Zusätzlich: ist<bundle>unterraw/schon belegt (Erstaufruf), Refusal sofort statt erst beim Accept; ist der Halter ein erfasstes Bundle, nennt sie--update+--replaces-bundle. (Bau) Geschrieben wird in ein verstecktes Staging-Verzeichnis neben dem Ziel und perrenamean seinen Platz - ganz oder gar nicht;raw pendingüberspringt das versteckte Verzeichnis.incoming/<bundle>/_capture.json:schema(1),repo,ref,commit,paths,captured(ISO-8601 UTC,Z),fidelity,authority,files(sortiert). Einzige Deklaration, dass und wie die Instanz dieses Repo verfolgt.<!-- wikitool:export(ein führendes UTF-8-BOM wird toleriert); Symlinks (120000); Submodule (160000); überweb_capture.MAX_BYTES(ausls-tree --long, ohne den Blob zu lesen); Pfade mit einem.-Segment; jede Datei namens_capture.json; Git-LFS-Zeiger; (Bau) Pfade, die kein UTF-8 sind.wikitool: Git läuft übertoolpaths.git()mit der Konfiguration und den Credential-Helfern des Hosts. Erlaubt nurssh://,https://unduser@host:pfad; (Bau) eine URL mit Passwort im Userinfo wird beihttps://undssh://abgewiesen, ebenso eine URL mit-am Anfang oder Steuerzeichen.GIT_ALLOW_PROTOCOLwird bei jedem Aufruf ausrepo_capture.ALLOWED_SCHEMESgesetzt und überschreibt die Umgebung. Unbeaufsichtigt:GIT_TERMINAL_PROMPT=0, stdin geschlossen, 120 s Timeout je Aufruf; (Bau) dazu-c core.askPass=undGIT_ASKPASS/SSH_ASKPASSentfernt (sonst fiele Git auf ein Askpass-Programm zurück, z. B. das eines Editors),SSH_ASKPASS_REQUIRE=never,GCM_INTERACTIVE=never, und auf POSIX eine eigene Session (kein steuerndes Terminal fürssh); beim Timeout wird die ganze Prozessgruppe beendet, sonst hielte einssh-Enkel die Pipe offen.--update <raw-bundle>liest URL, Ref-Regel und Globs aus dem Manifest, erfasst nachincoming/<gleicher Name>/;--fidelity/--authorityoptional (Vorgabe: altes Manifest), angegeben überschreiben sie. Dieselbe URL-Prüfung wie beim Erstaufruf (read_manifestprüft URL, Ref-Regel, Globs undfiles-Pfade). URL/--ref/--path/--nameneben--updatesind eine Refusal.2.
raw acceptfür ein erfasstes Bundleraw accept incoming/<bundle>. Mit_capture.jsonkommenfidelity/authoritynur aus dem Manifest; Flags auf der Kommandozeile sind eine Refusal. (Bau) Der Ordner muss genau die Dateien des Manifests enthalten (sonst Refusal "no longer matches", auch inraw pending); dienew source-Folgezeile lässt_capture.jsonausraw_files=weg und nennt--file <pfad im bundle>. (Bau) Eine Datei_capture.jsonals lose Datei inincoming/oder tiefer als an der Spitze eines Ordners ist eine Refusal (reservierter Name).raw accept incoming/<bundle> --replaces-bundle <raw-bundle>ersetzt das Bundle an seiner bestehenden Adresse. Refusal ohne jede Änderung unterraw/oder inincoming/, wenn: eine Seite kein_capture.jsonhat oder es nicht lesbar ist; dierepo-URLs abweichen; der Ordnername abweicht;--page,--replaces(Bau) oder--fidelity/--authoritymitgegeben ist; (Bau) der Ordner nicht zu seinem Manifest passt oder eine Seite versteckte Einträge/Symlinks enthält; ein neuer Pfad über dem Budget liegt; eine Datei des Bundles mehr als eine besitzende Quellseite hat.Dlöschen → leer gewordene Verzeichnisse mitrmdir(nie rekursiv) →A/Mperos.replace→ unveränderte Dateien inincoming/löschen → Manifest ersetzen →incoming/<bundle>/perrmdir. Invariante: danach genau die Dateien des neuen Manifests plus_capture.json;incoming/<bundle>/entfernt. Nicht atomar; ein Abbruch mittendrin wird gemeldet, nicht fortgesetzt.raw_files:bleibt unverändert. Abweichendefidelity/authoritywerden auf den besitzenden Quellseiten überschrieben (_overwrite_capture_fields_on_page).alt → neu, Zählung, je DateiA/M/Dgruppiert nach besitzender Quellseite mit deren zitierenden Seiten (neue Dateien: "not covered by any source page"). Folgezeilen in der echten Syntax (Bau: der Entwurf nanntetouch "<Quellseite>"):D→tools/wikitool touch --page "<Quellseite>" --remove raw_files=<pfad>;A→touch --page "<einzige Quellseite des Bundles | <Source page>>" --add raw_files=<pfad>oder eine neue Quellseite; bleibt eine Seite ohneraw_files:, Verweis aufpage-lifecycle.md§ Delete.--replacesund--pageverweigern ein Ziel innerhalb eines erfassten Bundles (repo_capture.captured_bundle_of: nächstes Verzeichnis unterraw/mit_capture.json). Die Refusal nenntraw capture --update+--replaces-bundle. Die ON-FAILURE-Reaktion bei belegtem Ordnernamen nennt für ein erfasstes Bundle ebenfalls--replaces-bundle(Nachziehung8ce202a).3.
raw status(nur lesend, schreibt nur in den Cache)_capture.jsonunterraw/(versteckte Pfade ausgenommen), löst je Repo die Ref-Regel perls-remoteauf. Nur bei abweichendem Commit wird der neue geholt und mitrepo_capture.select_files- derselben Funktion wieraw capture- gegen den Bundle-Inhalt unterraw/abgeglichen.alt → neu, A/M/D gruppiert wie oben, plus die Zeilenraw capture --update/raw accept … --replaces-bundle. Unveränderte Repos (auch bewegter Commit ohne Änderung in den Globs) nur als Zählung "N unchanged". Fehlerzeilen für nicht erreichbar / Manifest oder URL abgewiesen / kein passendes Ref; Exit 0.--json: je Bundlebundle,repo,ref,old,new,changed,files([{path, status}], Pfade relativ zur Instanz,raw/…),error.4. Abdeckung:
config.RAW_IGNORE_NAMES={".gitkeep", ".DS_Store", "_capture.json"}(jede Tiefe); neuconfig.RAW_IGNORE_TOP_LEVEL={"CONTRACT.md"}, nur direkt inraw/.Akzeptanzkriterien
Belegt durch
tools/chemenu/tests/test_raw_capture.py(54 Tests), grün lokal und in CI-Lauf 545 (und erneut in 547).raw capture+raw acceptenthält das Bundle genau die passenden, nicht ausgeschlossenen Dateien zum Manifest-Commit, byte-identisch zum Blob (auch mitcore.autocrlf=true), mit Repo-Pfaden, plus_capture.json;*.yamlnebendocs/**/*.mdfehlt.docs/**/*.mdtrifftdocs/a.mdunddocs/x/y/b.md,docs/*.mdtrifftdocs/x/b.mdnicht.--replaces-bundledasselbe für den neuen Commit; gelöschte Dateien und leere Verzeichnisse weg; Bundle-Pfad unverändert; Manifest nennt den neuen Commit; keinraw_files:verändert.--replaces-bundlemit abweichenderrepo-URL, abweichendem Ordnernamen oder einem Bundle ohne_capture.jsonwird abgewiesen, ohne Änderung unterraw/oder inincoming/.raw accept --replacesund--pagemit einem Ziel in einem erfassten Bundle werden abgewiesen; außerhalb unverändert..github/, Symlink, Submodul, LFS-Zeiger und Repo-Datei_capture.jsonwerden nie erfasst und je namentlich gemeldet;raw statusmeldet keine davon alsA.ext::/fd::/file://undhttps://mit Passwort werden vonraw capture,--updateundraw statusabgewiesen; einext::-Transport führt nachweislich keinen Befehl aus (URL-Ebene und Git-Ebene getestet, mit Kontrolltest, dass derselbe Befehl bei freigegebenemextläuft).raw status: korrekte A/M/D-Liste, nach--replaces-bundlestill; Änderungen außerhalb der Globs still; Tag-Muster folgt nur neuen Tags; nicht erreichbares Repo ist eine Zeile neben den übrigen._capture.jsonnicht insources coverage;CONTRACT.mdin einem Bundle erscheint, solange unabgedeckt;raw/CONTRACT.mdweiterhin nicht._capture.jsonunterraw/verhält sich jedes bestehende Kommando unverändert - bestehende Verhaltens-Tests grün ohne Anpassung. (Bau) Angepasst wurden nur zwei Register, die jedes neue Kommando nachziehen muss: die feste Menge der Netz-Kommandos intest_cli.pyundcli_contract.GROUPS.file://, freigeschaltet nur durch die Fixture (monkeypatchaufrepo_capture.ALLOWED_SCHEMES).pytest,docs verify,instructions verifygrün; CI grün (Lauf 545).raw statusan einem echten Repo ausprobiert, an diesem Repo selbst, in einem Scratch-Root (CHEMENU_ROOT, nicht in der Instanz):raw capture ssh://git@gitea.nehmer.net:10101/torben/chemenu.git --ref 'v*' --path README.md --path 'docs/**' --path raw/CONTRACT.md --name chemenu-docslöstev*zuv7.0.0(4446424e) auf und erfasste 9 Dateien;README.md,raw/CONTRACT.md,docs/version-model.mdpercmpbyte-identisch zugit show v7.0.0:<datei>. Nachraw acceptmeldeteraw status"1 unchanged" (old=new=4446424e, ~0,7 s). Eine echte Drift am echten Repo war nicht vorführbar, weil kein neueres Release-Tag existiert; der Drift-Pfad ist durch die Tests belegt.Betroffene Stellen
tools/chemenu/repo_capture.py(neu): URL-Prüfung, Git-Aufruf, Cache, Ref-Auflösung, Glob, Auswahl/Ausschlüsse, Manifest, Diff.tools/chemenu/commands/raw_cmd.py:raw capture,raw status,--replaces-bundle(alsAnnotated-Option, damit direkte Aufrufe der bestehenden Tests ein echtesNonesehen), Manifest-Pfad beim Accept, Refusals für--replaces/--page, reservierter Name,raw pendingprüft erfasste Ordner gegen ihr Manifest, Records.tools/chemenu/config.py:RAW_IGNORE_NAMES,RAW_IGNORE_TOP_LEVEL,iter_raw_files..gitignore(/tools/.wikitool_capture/), Kanarie indocs_verify.py; (Bau)dist_cmd.TOOLS_EXCLUDE_DIRSum.wikitool_captureergänzt, sonst exportiertedist exportden Cache.cli_contract.GROUPS,tools/CONTRACT.md(perdocs contract --apply),test_cli.py(Netz-Kommandos).raw/CONTRACT.md: Abschnitt "Getting a repository in:raw capture", Capture fields, Rules § Replaceable as a whole, § Getting a file in (Capture-Felder eines erfassten Ordners).instructions/wiki-ingest/SKILL.md: Schritt 1 (Large-Tree-Abzweig) und 5 (erfasster Ordner ohne Flags), Schritt 7 (--filemit bundle-relativem Pfad), Entscheidungspunkt "neue Fassung eines erfassten Bundles", Kommandoliste.README.md,tools/README.md,CHANGES.md,tools/chemenu/tests/test_raw_capture.py.Versionsteil
--minor: neue Kommandos, neue optionale Manifestdatei, ein bisher abgewiesenerraw accept-Aufruf wird erlaubt. Drop-in in beide Richtungen: ohne_capture.jsonkein geändertes Verhalten; der verankerteCONTRACT.md-Ausschluss kann einen neuen Abdeckungsbefund über eine bisher übersehene Datei erzeugen; eine ältere Version meldet_capture.jsonals unabgedeckt. Gelandet als Bumps im laufenden Kandidaten:8.0.0-beta.41(311c8ee, das Feature) und8.0.0-beta.42(8ce202a,--patch, die Nachziehung aus der Abschlussphase), je mit Eintrag inCHANGES.md.Changelog: Body gegen den Baum (
8ff22ad) geprüft und präzisiert. Neu: BefundRAW_IGNORE_NAMESblendet jedeCONTRACT.mdunterraw/aus - wird hier mitbehoben (Punkt 4). Korrigiert:_capture.jsonist nicht "wie der Header einesraw fetch-Bundles" (der steht in der.mdund ist abgedeckt), sondern ein reservierter Name initer_raw_files;raw statusvergleicht den neuen Commit gegen den Bundle-Inhalt unterraw/statt gegen den alten Commit (kein Fetch eines unbeworbenen SHA);page-lifecycle.mdgilt nur für eine leer werdende Quellseite,A/Dlaufen übertouch --add/--remove raw_files=. Ergänzt: Blob-Lesen statt Checkout (autocrlf), Glob-Abgleich in Python (ls-treekann keine:(glob)-Pathspec,full_matcherst ab 3.13), Ausschluss versteckter Pfade/LFS-Zeiger/_capture.json, Refusal für URLs mit Passwort,GIT_TERMINAL_PROMPT=0+ Timeout, Cache-Pfad/tools/.wikitool_capture/, Manifest als einzige Quelle fürfidelity/authoritybeim Accept, Refusal für--replaces/--pagein erfassten Bundles, Ausgabe gruppiert je Quellseite,--json-Felder, Kriterium "an echtem Repo ausprobiert" aus #171 übernommen. Versionsteil unverändert--minor.Changelog: Gebaut und publiziert (
8.0.0-beta.41,311c8ee), verifiziert durch CI-Lauf 545 (Release-Lauf 546 grün); alle Kriterien abgehakt, das Echt-Repo-Kriterium mit Ergebnis (v*→v7.0.0, 9 Dateien byte-identisch,raw status"1 unchanged"). Neu im Body, mit (Bau) markiert: Glob ohne Wildcard matcht wie bei Git als führendes Verzeichnis; Staging-Verzeichnis mit atomaremrename; frühe Refusal bei belegtem Bundlenamen; Ordner muss zum Manifest passen (Accept,--replaces-bundle,raw pending);_capture.jsonals lose oder verschachtelte Datei verweigert;--replaces-bundleverweigert auch--fidelity/--authority; Passwort-URL auch beissh://abgewiesen; zusätzliche Schutzschichten gegen Prompts (core.askPass=, Askpass-Variablen, eigene Session, Prozessgruppen-Kill);dist_cmd.TOOLS_EXCLUDE_DIRSschließt den Cache aus. Korrigiert: Folgezeilen nutzen die echtetouch --page "<Titel>"-Syntax. Angepasst wurden nur die Registercli_contract.GROUPSund die Netz-Kommando-Menge intest_cli.py.Changelog: Body in den Endzustand gebracht (Problem in der Vergangenheit, "Abgeschlossen" statt offener Phase). Neu: Nachziehung aus der Abschlussphase,
8.0.0-beta.42/8ce202a, CI-Lauf 547 grün - die ON-FAILURE-Reaktion bei belegtem Ordnernamen nennt für ein erfasstes Bundle--replaces-bundlestatt "rename", undraw/CONTRACT.md§ Getting a file in nennt das Manifest als Quelle der Capture-Felder.docs/geprüft, keine Seite veraltet. Geschlossen.