raw accept: Stem-Eindeutigkeit im Typverzeichnis erzwingen, --replaces als einziger Weg daran vorbei #64

Closed
opened 2026-09-05 06:29:16 +00:00 by torben · 2 comments
Owner

Nachzug zu #58. Zwei Dinge, die dieselbe Entscheidung von zwei Seiten sind: eine Regel, die Kollisionen ablehnt, und der einzige sanktionierte Weg an ihr vorbei. Getrennt gebaut wäre die erste eine Sackgasse für jede Quelle, die sich bewegt.

Status: umgesetzt, geschlossen mit Commit 0b3c496 auf main, Version 4.8.0-beta.4. CI grün.

Befund: ein fremdes Bundle wird still aufgefüllt

raw accept prüfte Kollisionen nur auf einzelnen Dateipfaden (dst.exists() in tools/chemenu/commands/raw_cmd.py:167-169), nie auf dem Bundle-Verzeichnis selbst. Weil der Bundle-Name aus dem Stem der Primärdatei entsteht (raw_cmd.py:155-157), wanderte eine zweite, unabhängige Quelle wortlos in das Bundle einer ersten, sobald die Dateinamen zufällig nicht kollidierten.

Verifiziert am 2026-09-05 mit einem Wegwerf-Test gegen den Stand 36d2128:

raw/documents/handbuch/         ← Bundle von "Source - Handbuch": handbuch.pdf, handbuch.md
incoming/documents/handbuch.txt + anhang.md   ← eine ANDERE Quelle, ohne --page
→ raw/documents/handbuch/ enthält danach: anhang.md, handbuch.md, handbuch.pdf, handbuch.txt

Exit 0, keine Warnung. lint meldete nichts: beide Quellen deckten ihre Dateien korrekt ab. Der Normalfall fing sich meist selbst, weil die Primärdatei üblicherweise gleich heißt und dann auf Dateiebene kollidiert — aber „meist" war Zufall, keine Eigenschaft. Dieser Befund ist behoben; derselbe Aufbau lehnt jetzt mit Exit 1 ab, nachgestellt gegen die echte CLI (siehe § Verifikation).

Die Lücke dahinter: raw/ hatte keinen legalen Update-Pfad

Der Bundle-Defekt war die sichtbare Hälfte. Die unsichtbare: raw/CONTRACT.md sagte „Immutable. Never edit, reformat, summarize, or 'clean up' a file after it lands here." Eine Quelle, die sich bewegt — eine Cluster-Dokumentation, ein Handbuch in neuer Ausgabe — hatte damit gar keinen vorgesehenen Weg. raw accept --replaces schließt diese Lücke jetzt.

Entscheidung 1: Stem-Eindeutigkeit innerhalb des Typverzeichnisses

Die Menge der Einträge auf raw/<typ>/-Ebene — Dateistämme plus Bundle-Verzeichnisnamen — ist eindeutig. Innerhalb eines Bundles bleiben handbuch.pdf und handbuch.md selbstverständlich nebeneinander; die Regel greift eine Ebene darüber.

Umgesetzt als _occupied_stems() in raw_cmd.py, ausgewertet gegen den Namen, den ein Aufruf neu beansprucht (Bundle-Name, oder Stem der Einzeldatei ohne Bundle), unter Ausschluss dessen, was der Aufruf selbst schon besitzt (ein wachsendes eigenes Bundle, ein sich fortsetzendes bestehendes Bundle). Der Korpus war dafür sauber (null Kollisionen, kein Bundle vorhanden) und blieb es — sources coverage nach dem Publish weiterhin 0/0/0.

Entscheidung 2: Ob ersetzt oder danebengelegt wird, entscheidet immer der Mensch

Ohne explizites --replaces <pfad> existiert kein Codepfad, der eine vorhandene Rohdatei überschreibt oder ersetzt — kein Heuristik-Fallback, kein interaktiver Prompt, den eine Session selbst beantwortet. --replaces nimmt den Zielpfad als Argument, nicht als Boolean. Ohne Flag lehnt das Kommando ab und nennt beide Wege, ohne einen zu empfehlen (Ausgabe der echten CLI):

ERROR raw/documents/handbuch already claims the stem "handbuch" in raw/documents/.
  These are two different intents and only you can tell them apart:
    Same source, new edition   -> tools/wikitool raw accept --replaces raw/documents/handbuch/handbuch.md <incoming file>
    A second, separate source  -> rename it in incoming/ (add a distinguishing suffix) and accept it normally
  raw accept does not guess which one this is.

Ist der belegende Eintrag ein Bundle-Verzeichnis statt einer Datei, nennt die Meldung als --replaces-Ziel eine konkrete Datei aus diesem Bundle — ein Verzeichnis wäre als Ziel ungültig, und --replaces bleibt eine Datei für eine Datei.

Eine Session, die diese Meldung bekommt, führt keine der beiden Varianten aus eigenem Antrieb aus. instructions/wiki-ingest/SKILL.md Schritt 1 benennt das jetzt explizit als Haltepunkt (AGENTS.md-Invariante-6-Klasse, ohne dass hier ein Exit-42-Gate greift).

Entscheidung 3: Git ist das Archiv — keine Altfassung wird als Datei aufgehoben

Umgesetzt genau wie entschieden: target.unlink() + incoming_path.rename(target), kein Archivverzeichnis, kein Hash im Namen, kein neues Frontmatter-Feld. raw/CONTRACT.md § Rules trägt den Nachsatz zur Ein-Commit-Regel.

Entscheidung 4: die Contract-Regel wurde präzisiert, nicht aufgeweicht

raw/CONTRACT.md § Rules trägt jetzt beide Sätze: „Immutable" unverändert, plus „Replaceable as a whole, never in part" mit dem Verweis, dass die Wahl zwischen neuer Ausgabe und zweiter Quelle dem Menschen gehört.

Umsetzung

tools/chemenu/commands/raw_cmd.py

Wie spezifiziert umgesetzt: _occupied_stems(), die Stem-Prüfung nach der bestehenden dst.exists()-Schleife, --replaces als eigener Codepfad (_replace()), Ausgabe der Sprengweite über die vorhandenen source_pages_by_raw_file/citing_pages-Bausteine aus provenance.py.

Eine Ergänzung über die Spezifikation hinaus: --replaces und --page schließen sich gegenseitig aus (_replace() lehnt die Kombination ab) — die Spezifikation hatte diesen Fall offen gelassen, aber eine Ersetzung ändert per Entscheidung 2 nie raw_files:, also ist die Kombination sinnlos und wurde als Guard abgefangen statt stillschweigend eine der beiden Semantiken zu bevorzugen.

Weitere Dateien

  • tools/CONTRACT.mdraw accept-Zeile um Stem-Regel ergänzt, neue Zeile für raw accept --replaces, Fehlerkontrakt für beide Formen.
  • raw/CONTRACT.md — Entscheidung 4 als zwei Regeln, Kollisionsmeldung mit beiden Wegen im Abschnitt „Getting a file in".
  • instructions/wiki-ingest/SKILL.md — Schritt 1 benennt den Kollisionsfall als Haltepunkt.
  • tools/chemenu/tests/test_raw_cmd.py — 16 neue Tests (4 Stem-Kollision, 12 --replaces), alle 19 bestehenden bleiben grün (33 gesamt in dieser Datei).
  • CHANGES.md — Prosa im 4.8.0-beta.4-Eintrag.

Akzeptanzkriterien

  • Der Befund oben ist behoben: incoming/documents/handbuch.txt + anhang.md (ohne --page) wandert nicht mehr in ein bestehendes raw/documents/handbuch/, sondern wird mit belegtem Stem abgelehnt; das Bundle enthält danach unverändert genau seine zwei ursprünglichen Dateien. (test_second_source_does_not_silently_join_an_existing_bundle + Wegwerf-Repro gegen die echte CLI)
  • Ein Promote, dessen Zielname auf raw/<typ>/-Ebene belegt ist — durch eine Datei oder durch ein Bundle-Verzeichnis — wird mit Exit 1 abgelehnt; keine Datei ist bewegt, die incoming/-Datei liegt unverändert an ihrem Platz. (test_flat_promote_rejected_when_stem_matches_an_existing_bundle, test_flat_promote_rejected_when_stem_matches_an_existing_flat_file)
  • Die Ablehnungsmeldung nennt beide Wege (--replaces und Umbenennen in incoming/) und empfiehlt keinen von beiden. (test_stem_collision_message_names_both_routes_without_recommending_one)
  • Ohne --replaces überschreibt oder löscht kein Codepfad in raw_cmd.py eine existierende Datei unter raw/. (byte-identisch geprüft in test_flat_promote_rejected_when_stem_matches_an_existing_flat_file)
  • --replaces ersetzt die Datei, lässt raw_files: jeder Seite unverändert und schreibt keine kb/-Seite. (test_replaces_swaps_bytes_and_leaves_raw_files_untouched + CLI-Durchlauf)
  • --replaces mit mehr als einer incoming/-Datei, mit abweichendem Dateinamen, mit abweichendem Typverzeichnis, auf einen nicht existierenden Zielpfad oder auf eine Datei mit zwei Ownern wird jeweils abgelehnt, ohne etwas zu bewegen. (je ein Test pro Fall, zusätzlich alle fünf gegen die echte CLI nachgestellt)
  • --replaces auf eine Datei ohne Source-Seite gelingt und meldet, dass keine Seite sie deckt. (test_replaces_succeeds_with_no_owner_and_reports_it)
  • --replaces gibt die Liste der betroffenen Source- und zitierenden Seiten aus; ein Test mit einer Source-Seite und zwei zitierenden Seiten prüft, dass alle drei genannt sind. (test_replaces_reports_source_and_both_citing_pages)
  • --dry-run funktioniert mit --replaces und schreibt nichts. (test_replaces_dry_run_writes_nothing + CLI-Durchlauf)
  • Der Wachstumsfall bleibt möglich: --page hebt eine Einzeldatei-Quelle auf ein Bundle gleichen Stems, ohne an der neuen Prüfung zu scheitern (die zwei bestehenden Tests test_page_flag_extends_raw_files_for_a_single_new_file und test_growth_case_no_broken_or_uncovered_refs_afterwards bleiben grün; zusätzlich gegen die echte CLI nachgestellt, inklusive Fortsetzung eines bereits bestehenden Bundles).
  • raw/CONTRACT.md trägt beide Regeln aus Entscheidung 4, einschließlich des Satzes, dass die Wahl dem Menschen gehört.
  • tools/CONTRACT.md trägt Kommando- und Fehlerkontrakt-Zeile; docs verify erzwingt beide Richtungen.
  • instructions/wiki-ingest/SKILL.md benennt den Kollisionsfall als Haltepunkt.
  • pytest (1046 grün), docs verify (52 Kommandos, grün), instructions verify (20 Instructions/7 Skills, grün); CI-Läufe #139 und #140 auf Commit 0b3c496 beide mit success abgeschlossen.
  • Changelog-Eintrag, MINOR (4.8.0-beta.34.8.0-beta.4).

Verifikation

Nach dem Publish gegen die echte CLI in einem Wegwerf-Baum (CHEMENU_ROOT) nachgestellt, nicht nur über die Unit-Tests:

Fall Ergebnis
Repro aus diesem Issue (zwei Dateien, ohne --page, gegen bestehendes Bundle) Exit 1, Bundle unverändert bei seinen zwei Dateien, beide incoming/-Dateien liegen unberührt
Weg 2: in incoming/ umbenannt, dann normal akzeptiert Exit 0, neues Bundle raw/documents/handbuch-netzplan/, altes Bundle unangetastet
--replaces --dry-run Exit 0, Ziel- und incoming/-Bytes unverändert
--replaces echt Ziel trägt die neuen Bytes, incoming/-Datei verbraucht, raw_files: unverändert, Nachbardatei im Bundle unangetastet
Sprengweiten-Ausgabe nennt [[Source - Handbuch]] und cited by: [[Handbuch-Konzept]]
Wachstumsfall --page (Einzeldatei → Bundle) Exit 0, korrekt gefaltet, raw_files: auf beide Pfade nachgezogen
Fortsetzung eines bestehenden Bundles via --page Exit 0, dritte Datei ins bestehende Bundle, raw_files: auf 3 Pfade
Fünf --replaces-Ablehnungen (zwei Dateien, Namensabweichung, Typabweichung, fehlendes Ziel, mit --page kombiniert) jeweils Exit 1, Ziel- und incoming/-Bytes unverändert
--replaces auf Datei mit zwei Ownern Exit 1, beide Owner namentlich genannt, nichts bewegt

Ein Zwischenbefund beim Nachstellen war ein Artefakt des Wegwerf-Baums, kein Defekt: ohne types/ unter der Wurzel löst Page.kind nicht auf, jede Source-Seite fällt aus source_pages_by_raw_file heraus, und --replaces meldete „No source page covers this file". Mit vorhandenem types/ — dem Zustand jeder echten Instanz — nennt es Source- und zitierende Seite korrekt.

Repository-Zustand nachgeprüft: Arbeitsbaum sauber, HEAD == origin/main == 0b3c496, VERSION = 4.8.0-beta.4 und die CHANGES.md-Überschrift stimmen überein, sources coverage weiterhin 0/0/0.

Nicht-Ziele

  • Kein maschinischer Wächter für die Ein-Commit-Regel. Weiterhin nicht gebaut — ein Folge-Issue wert, sobald der Fall real vorgekommen ist.
  • Kein Content-Hash-Abgleich beim Promoten. Nicht gebaut, eigenes Ticket wert.
  • Kein Frontmatter-Feld für die Fassung einer Quelle. Nicht gebaut, Git beantwortet die Frage.
  • Keine Bundle-Ersetzung. --replaces bleibt eine Datei für eine Datei.

Abgrenzung

  • #16 (raw rename) ist das Gegenstück, nicht die Überschneidung. Nachzug weiterhin fällig bei #16: dessen Abschnitt „Offene Fragen" sagt noch „Löschen und Anlegen sind ausdrücklich nicht Teil davon. raw/ ist unveränderlich" — die zweite Hälfte stimmt seit diesem Issue nicht mehr und ist beim Bearbeiten von #16 zu korrigieren.
  • #32 (Ingest-Queue) betrifft Fremdinhalt auf dem Weg nach incoming/, nicht die Beförderung daraus. Unberührt.

Version

4.8.0-beta.34.8.0-beta.4 (--minor). Kein --major: raw accept war nie released (letztes Release v4.7.4).

Sitzung / Modell

Dieses Issue kam bereits vollständig ausgearbeitet herein (kind/build, keine offene Entscheidung) — Design, Version-Teil und Grenzfragen waren vom Menschen bereits getroffen, keine dieser Phasen lief separat in dieser Sitzung. Umsetzung (Code, Tests, Doku-Sync, Version-Bump, Publish), Abschluss und die Verifikation oben liefen in einer durchgehenden Sitzung auf Claude Sonnet 5.

Nachzug zu #58. Zwei Dinge, die dieselbe Entscheidung von zwei Seiten sind: eine Regel, die Kollisionen ablehnt, und der einzige sanktionierte Weg an ihr vorbei. Getrennt gebaut wäre die erste eine Sackgasse für jede Quelle, die sich bewegt. **Status: umgesetzt, geschlossen mit Commit `0b3c496` auf `main`, Version `4.8.0-beta.4`. CI grün.** ## Befund: ein fremdes Bundle wird still aufgefüllt `raw accept` prüfte Kollisionen nur auf einzelnen **Dateipfaden** (`dst.exists()` in `tools/chemenu/commands/raw_cmd.py:167-169`), nie auf dem Bundle-Verzeichnis selbst. Weil der Bundle-Name aus dem Stem der Primärdatei entsteht (`raw_cmd.py:155-157`), wanderte eine zweite, unabhängige Quelle wortlos in das Bundle einer ersten, sobald die Dateinamen zufällig nicht kollidierten. Verifiziert am 2026-09-05 mit einem Wegwerf-Test gegen den Stand `36d2128`: ``` raw/documents/handbuch/ ← Bundle von "Source - Handbuch": handbuch.pdf, handbuch.md incoming/documents/handbuch.txt + anhang.md ← eine ANDERE Quelle, ohne --page → raw/documents/handbuch/ enthält danach: anhang.md, handbuch.md, handbuch.pdf, handbuch.txt ``` Exit 0, keine Warnung. `lint` meldete nichts: beide Quellen deckten ihre Dateien korrekt ab. Der Normalfall fing sich meist selbst, weil die Primärdatei üblicherweise gleich heißt und dann auf Dateiebene kollidiert — aber „meist" war Zufall, keine Eigenschaft. Dieser Befund ist behoben; derselbe Aufbau lehnt jetzt mit Exit 1 ab, nachgestellt gegen die echte CLI (siehe § Verifikation). ## Die Lücke dahinter: `raw/` hatte keinen legalen Update-Pfad Der Bundle-Defekt war die sichtbare Hälfte. Die unsichtbare: [raw/CONTRACT.md](../src/branch/main/raw/CONTRACT.md) sagte „Immutable. Never edit, reformat, summarize, or 'clean up' a file after it lands here." Eine Quelle, die sich bewegt — eine Cluster-Dokumentation, ein Handbuch in neuer Ausgabe — hatte damit **gar keinen** vorgesehenen Weg. `raw accept --replaces` schließt diese Lücke jetzt. ## Entscheidung 1: Stem-Eindeutigkeit innerhalb des Typverzeichnisses Die Menge der Einträge auf `raw/<typ>/`-Ebene — **Dateistämme plus Bundle-Verzeichnisnamen** — ist eindeutig. Innerhalb eines Bundles bleiben `handbuch.pdf` und `handbuch.md` selbstverständlich nebeneinander; die Regel greift eine Ebene darüber. Umgesetzt als `_occupied_stems()` in `raw_cmd.py`, ausgewertet gegen den Namen, den ein Aufruf neu beansprucht (Bundle-Name, oder Stem der Einzeldatei ohne Bundle), unter Ausschluss dessen, was der Aufruf selbst schon besitzt (ein wachsendes eigenes Bundle, ein sich fortsetzendes bestehendes Bundle). Der Korpus war dafür sauber (null Kollisionen, kein Bundle vorhanden) und blieb es — `sources coverage` nach dem Publish weiterhin `0/0/0`. ## Entscheidung 2: Ob ersetzt oder danebengelegt wird, entscheidet **immer** der Mensch Ohne explizites `--replaces <pfad>` existiert kein Codepfad, der eine vorhandene Rohdatei überschreibt oder ersetzt — kein Heuristik-Fallback, kein interaktiver Prompt, den eine Session selbst beantwortet. `--replaces` nimmt den Zielpfad als Argument, nicht als Boolean. Ohne Flag lehnt das Kommando ab und nennt beide Wege, ohne einen zu empfehlen (Ausgabe der echten CLI): ``` ERROR raw/documents/handbuch already claims the stem "handbuch" in raw/documents/. These are two different intents and only you can tell them apart: Same source, new edition -> tools/wikitool raw accept --replaces raw/documents/handbuch/handbuch.md <incoming file> A second, separate source -> rename it in incoming/ (add a distinguishing suffix) and accept it normally raw accept does not guess which one this is. ``` Ist der belegende Eintrag ein Bundle-Verzeichnis statt einer Datei, nennt die Meldung als `--replaces`-Ziel eine konkrete Datei *aus* diesem Bundle — ein Verzeichnis wäre als Ziel ungültig, und `--replaces` bleibt eine Datei für eine Datei. Eine Session, die diese Meldung bekommt, führt keine der beiden Varianten aus eigenem Antrieb aus. `instructions/wiki-ingest/SKILL.md` Schritt 1 benennt das jetzt explizit als Haltepunkt (AGENTS.md-Invariante-6-Klasse, ohne dass hier ein Exit-42-Gate greift). ## Entscheidung 3: Git ist das Archiv — keine Altfassung wird als Datei aufgehoben Umgesetzt genau wie entschieden: `target.unlink()` + `incoming_path.rename(target)`, kein Archivverzeichnis, kein Hash im Namen, kein neues Frontmatter-Feld. `raw/CONTRACT.md` § Rules trägt den Nachsatz zur Ein-Commit-Regel. ## Entscheidung 4: die Contract-Regel wurde präzisiert, nicht aufgeweicht `raw/CONTRACT.md` § Rules trägt jetzt beide Sätze: „Immutable" unverändert, plus „Replaceable as a whole, never in part" mit dem Verweis, dass die Wahl zwischen neuer Ausgabe und zweiter Quelle dem Menschen gehört. ## Umsetzung ### `tools/chemenu/commands/raw_cmd.py` Wie spezifiziert umgesetzt: `_occupied_stems()`, die Stem-Prüfung nach der bestehenden `dst.exists()`-Schleife, `--replaces` als eigener Codepfad (`_replace()`), Ausgabe der Sprengweite über die vorhandenen `source_pages_by_raw_file`/`citing_pages`-Bausteine aus `provenance.py`. Eine Ergänzung über die Spezifikation hinaus: `--replaces` und `--page` schließen sich gegenseitig aus (`_replace()` lehnt die Kombination ab) — die Spezifikation hatte diesen Fall offen gelassen, aber eine Ersetzung ändert per Entscheidung 2 nie `raw_files:`, also ist die Kombination sinnlos und wurde als Guard abgefangen statt stillschweigend eine der beiden Semantiken zu bevorzugen. ### Weitere Dateien - `tools/CONTRACT.md` — `raw accept`-Zeile um Stem-Regel ergänzt, neue Zeile für `raw accept --replaces`, Fehlerkontrakt für beide Formen. - `raw/CONTRACT.md` — Entscheidung 4 als zwei Regeln, Kollisionsmeldung mit beiden Wegen im Abschnitt „Getting a file in". - `instructions/wiki-ingest/SKILL.md` — Schritt 1 benennt den Kollisionsfall als Haltepunkt. - `tools/chemenu/tests/test_raw_cmd.py` — 16 neue Tests (4 Stem-Kollision, 12 `--replaces`), alle 19 bestehenden bleiben grün (33 gesamt in dieser Datei). - `CHANGES.md` — Prosa im `4.8.0-beta.4`-Eintrag. ## Akzeptanzkriterien - [x] Der Befund oben ist behoben: `incoming/documents/handbuch.txt` + `anhang.md` (ohne `--page`) wandert **nicht** mehr in ein bestehendes `raw/documents/handbuch/`, sondern wird mit belegtem Stem abgelehnt; das Bundle enthält danach unverändert genau seine zwei ursprünglichen Dateien. (`test_second_source_does_not_silently_join_an_existing_bundle` + Wegwerf-Repro gegen die echte CLI) - [x] Ein Promote, dessen Zielname auf `raw/<typ>/`-Ebene belegt ist — durch eine Datei **oder** durch ein Bundle-Verzeichnis — wird mit Exit 1 abgelehnt; keine Datei ist bewegt, die `incoming/`-Datei liegt unverändert an ihrem Platz. (`test_flat_promote_rejected_when_stem_matches_an_existing_bundle`, `test_flat_promote_rejected_when_stem_matches_an_existing_flat_file`) - [x] Die Ablehnungsmeldung nennt beide Wege (`--replaces` und Umbenennen in `incoming/`) und empfiehlt keinen von beiden. (`test_stem_collision_message_names_both_routes_without_recommending_one`) - [x] Ohne `--replaces` überschreibt oder löscht kein Codepfad in `raw_cmd.py` eine existierende Datei unter `raw/`. (byte-identisch geprüft in `test_flat_promote_rejected_when_stem_matches_an_existing_flat_file`) - [x] `--replaces` ersetzt die Datei, lässt `raw_files:` jeder Seite unverändert und schreibt keine `kb/`-Seite. (`test_replaces_swaps_bytes_and_leaves_raw_files_untouched` + CLI-Durchlauf) - [x] `--replaces` mit mehr als einer `incoming/`-Datei, mit abweichendem Dateinamen, mit abweichendem Typverzeichnis, auf einen nicht existierenden Zielpfad oder auf eine Datei mit zwei Ownern wird jeweils abgelehnt, ohne etwas zu bewegen. (je ein Test pro Fall, zusätzlich alle fünf gegen die echte CLI nachgestellt) - [x] `--replaces` auf eine Datei ohne Source-Seite gelingt und meldet, dass keine Seite sie deckt. (`test_replaces_succeeds_with_no_owner_and_reports_it`) - [x] `--replaces` gibt die Liste der betroffenen Source- und zitierenden Seiten aus; ein Test mit einer Source-Seite und zwei zitierenden Seiten prüft, dass alle drei genannt sind. (`test_replaces_reports_source_and_both_citing_pages`) - [x] `--dry-run` funktioniert mit `--replaces` und schreibt nichts. (`test_replaces_dry_run_writes_nothing` + CLI-Durchlauf) - [x] Der Wachstumsfall bleibt möglich: `--page` hebt eine Einzeldatei-Quelle auf ein Bundle gleichen Stems, ohne an der neuen Prüfung zu scheitern (die zwei bestehenden Tests `test_page_flag_extends_raw_files_for_a_single_new_file` und `test_growth_case_no_broken_or_uncovered_refs_afterwards` bleiben grün; zusätzlich gegen die echte CLI nachgestellt, inklusive Fortsetzung eines bereits bestehenden Bundles). - [x] `raw/CONTRACT.md` trägt beide Regeln aus Entscheidung 4, einschließlich des Satzes, dass die Wahl dem Menschen gehört. - [x] `tools/CONTRACT.md` trägt Kommando- und Fehlerkontrakt-Zeile; `docs verify` erzwingt beide Richtungen. - [x] `instructions/wiki-ingest/SKILL.md` benennt den Kollisionsfall als Haltepunkt. - [x] `pytest` (1046 grün), `docs verify` (52 Kommandos, grün), `instructions verify` (20 Instructions/7 Skills, grün); CI-Läufe [#139](https://gitea.nehmer.net/torben/chemenu/actions/runs/184) und [#140](https://gitea.nehmer.net/torben/chemenu/actions/runs/185) auf Commit `0b3c496` beide mit `success` abgeschlossen. - [x] Changelog-Eintrag, **MINOR** (`4.8.0-beta.3` → `4.8.0-beta.4`). ## Verifikation Nach dem Publish gegen die **echte CLI** in einem Wegwerf-Baum (`CHEMENU_ROOT`) nachgestellt, nicht nur über die Unit-Tests: | Fall | Ergebnis | |---|---| | Repro aus diesem Issue (zwei Dateien, ohne `--page`, gegen bestehendes Bundle) | Exit 1, Bundle unverändert bei seinen zwei Dateien, beide `incoming/`-Dateien liegen unberührt | | Weg 2: in `incoming/` umbenannt, dann normal akzeptiert | Exit 0, neues Bundle `raw/documents/handbuch-netzplan/`, altes Bundle unangetastet | | `--replaces --dry-run` | Exit 0, Ziel- und `incoming/`-Bytes unverändert | | `--replaces` echt | Ziel trägt die neuen Bytes, `incoming/`-Datei verbraucht, `raw_files:` unverändert, Nachbardatei im Bundle unangetastet | | Sprengweiten-Ausgabe | nennt `[[Source - Handbuch]]` und `cited by: [[Handbuch-Konzept]]` | | Wachstumsfall `--page` (Einzeldatei → Bundle) | Exit 0, korrekt gefaltet, `raw_files:` auf beide Pfade nachgezogen | | Fortsetzung eines bestehenden Bundles via `--page` | Exit 0, dritte Datei ins bestehende Bundle, `raw_files:` auf 3 Pfade | | Fünf `--replaces`-Ablehnungen (zwei Dateien, Namensabweichung, Typabweichung, fehlendes Ziel, mit `--page` kombiniert) | jeweils Exit 1, Ziel- und `incoming/`-Bytes unverändert | | `--replaces` auf Datei mit zwei Ownern | Exit 1, beide Owner namentlich genannt, nichts bewegt | Ein Zwischenbefund beim Nachstellen war ein Artefakt des Wegwerf-Baums, kein Defekt: ohne `types/` unter der Wurzel löst `Page.kind` nicht auf, jede Source-Seite fällt aus `source_pages_by_raw_file` heraus, und `--replaces` meldete „No source page covers this file". Mit vorhandenem `types/` — dem Zustand jeder echten Instanz — nennt es Source- und zitierende Seite korrekt. Repository-Zustand nachgeprüft: Arbeitsbaum sauber, `HEAD` == `origin/main` == `0b3c496`, `VERSION` = `4.8.0-beta.4` und die `CHANGES.md`-Überschrift stimmen überein, `sources coverage` weiterhin `0/0/0`. ## Nicht-Ziele - **Kein maschinischer Wächter für die Ein-Commit-Regel.** Weiterhin nicht gebaut — ein Folge-Issue wert, sobald der Fall real vorgekommen ist. - **Kein Content-Hash-Abgleich beim Promoten.** Nicht gebaut, eigenes Ticket wert. - **Kein Frontmatter-Feld für die Fassung einer Quelle.** Nicht gebaut, Git beantwortet die Frage. - **Keine Bundle-Ersetzung.** `--replaces` bleibt eine Datei für eine Datei. ## Abgrenzung - **#16 (`raw rename`)** ist das Gegenstück, nicht die Überschneidung. **Nachzug weiterhin fällig bei #16:** dessen Abschnitt „Offene Fragen" sagt noch „Löschen und Anlegen sind ausdrücklich *nicht* Teil davon. `raw/` ist unveränderlich" — die zweite Hälfte stimmt seit diesem Issue nicht mehr und ist beim Bearbeiten von #16 zu korrigieren. - **#32 (Ingest-Queue)** betrifft Fremdinhalt auf dem Weg nach `incoming/`, nicht die Beförderung daraus. Unberührt. ## Version `4.8.0-beta.3` → `4.8.0-beta.4` (`--minor`). Kein `--major`: `raw accept` war nie released (letztes Release `v4.7.4`). ## Sitzung / Modell Dieses Issue kam bereits vollständig ausgearbeitet herein (`kind/build`, keine offene Entscheidung) — Design, Version-Teil und Grenzfragen waren vom Menschen bereits getroffen, keine dieser Phasen lief separat in dieser Sitzung. Umsetzung (Code, Tests, Doku-Sync, Version-Bump, Publish), Abschluss und die Verifikation oben liefen in einer durchgehenden Sitzung auf Claude Sonnet 5.
torben added the area/kbkind/buildprio/blockingsize/M labels 2026-09-05 06:29:33 +00:00
Author
Owner

Changelog: Umgesetzt und geschlossen. Body auf Endzustand geschrieben: alle Akzeptanzkriterien abgehakt mit Testnamen, Status-Zeile mit Commit/Version ergänzt, Umsetzungsabschnitt um die eine Ergänzung über die Spezifikation hinaus erweitert (--replaces/--page schließen sich aus). CI-Lauf #139 lief beim Schließen noch, Publish und lokale Checks (pytest/docs verify/instructions verify) waren bereits grün.

**Changelog:** Umgesetzt und geschlossen. Body auf Endzustand geschrieben: alle Akzeptanzkriterien abgehakt mit Testnamen, Status-Zeile mit Commit/Version ergänzt, Umsetzungsabschnitt um die eine Ergänzung über die Spezifikation hinaus erweitert (`--replaces`/`--page` schließen sich aus). CI-Lauf #139 lief beim Schließen noch, Publish und lokale Checks (pytest/docs verify/instructions verify) waren bereits grün.
Author
Owner

Changelog: Nachverifikation gegen die echte CLI ergänzt (neuer Abschnitt § Verifikation, neun nachgestellte Fälle inkl. Repro, beide Wege, Wachstums- und Bundle-Fortsetzungsfall, alle Ablehnungen). CI-Zeile korrigiert: die Läufe #139 und #140 auf 0b3c496 sind seither beide mit success durchgelaufen — die vorherige Fassung sagte „noch in Bearbeitung". Kollisionsmeldung im Body durch die tatsächliche CLI-Ausgabe ersetzt und um den Bundle-Fall ergänzt.

**Changelog:** Nachverifikation gegen die echte CLI ergänzt (neuer Abschnitt § Verifikation, neun nachgestellte Fälle inkl. Repro, beide Wege, Wachstums- und Bundle-Fortsetzungsfall, alle Ablehnungen). CI-Zeile korrigiert: die Läufe #139 und #140 auf `0b3c496` sind seither beide mit `success` durchgelaufen — die vorherige Fassung sagte „noch in Bearbeitung". Kollisionsmeldung im Body durch die tatsächliche CLI-Ausgabe ersetzt und um den Bundle-Fall ergänzt.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#64