raw capture / raw status / --replaces-bundle: Doku aus Git-Repositories als Bundle erfassen und Drift melden #177

Closed
opened 2026-10-04 20:39:17 +00:00 by torben · 3 comments
Owner

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:// 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).

  • 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.

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.

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`.
torben added the prio/plannedsize/Larea/kbkind/build labels 2026-10-04 20:39:17 +00:00
Author
Owner

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`.
Author
Owner

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`.
Author
Owner

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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#177