raw accept: Stem-Eindeutigkeit im Typverzeichnis erzwingen, --replaces als einziger Weg daran vorbei
#64
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
0b3c496aufmain, Version4.8.0-beta.4. CI grün.Befund: ein fremdes Bundle wird still aufgefüllt
raw acceptprüfte Kollisionen nur auf einzelnen Dateipfaden (dst.exists()intools/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:Exit 0, keine Warnung.
lintmeldete 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-PfadDer 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 --replacesschließ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 bleibenhandbuch.pdfundhandbuch.mdselbstverständlich nebeneinander; die Regel greift eine Ebene darüber.Umgesetzt als
_occupied_stems()inraw_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 coveragenach dem Publish weiterhin0/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.--replacesnimmt 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):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--replacesbleibt 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.mdSchritt 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.pyWie spezifiziert umgesetzt:
_occupied_stems(), die Stem-Prüfung nach der bestehendendst.exists()-Schleife,--replacesals eigener Codepfad (_replace()), Ausgabe der Sprengweite über die vorhandenensource_pages_by_raw_file/citing_pages-Bausteine ausprovenance.py.Eine Ergänzung über die Spezifikation hinaus:
--replacesund--pageschließen sich gegenseitig aus (_replace()lehnt die Kombination ab) — die Spezifikation hatte diesen Fall offen gelassen, aber eine Ersetzung ändert per Entscheidung 2 nieraw_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ürraw 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 im4.8.0-beta.4-Eintrag.Akzeptanzkriterien
incoming/documents/handbuch.txt+anhang.md(ohne--page) wandert nicht mehr in ein bestehendesraw/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)raw/<typ>/-Ebene belegt ist — durch eine Datei oder durch ein Bundle-Verzeichnis — wird mit Exit 1 abgelehnt; keine Datei ist bewegt, dieincoming/-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)--replacesund Umbenennen inincoming/) und empfiehlt keinen von beiden. (test_stem_collision_message_names_both_routes_without_recommending_one)--replacesüberschreibt oder löscht kein Codepfad inraw_cmd.pyeine existierende Datei unterraw/. (byte-identisch geprüft intest_flat_promote_rejected_when_stem_matches_an_existing_flat_file)--replacesersetzt die Datei, lässtraw_files:jeder Seite unverändert und schreibt keinekb/-Seite. (test_replaces_swaps_bytes_and_leaves_raw_files_untouched+ CLI-Durchlauf)--replacesmit mehr als einerincoming/-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)--replacesauf eine Datei ohne Source-Seite gelingt und meldet, dass keine Seite sie deckt. (test_replaces_succeeds_with_no_owner_and_reports_it)--replacesgibt 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-runfunktioniert mit--replacesund schreibt nichts. (test_replaces_dry_run_writes_nothing+ CLI-Durchlauf)--pagehebt eine Einzeldatei-Quelle auf ein Bundle gleichen Stems, ohne an der neuen Prüfung zu scheitern (die zwei bestehenden Teststest_page_flag_extends_raw_files_for_a_single_new_fileundtest_growth_case_no_broken_or_uncovered_refs_afterwardsbleiben grün; zusätzlich gegen die echte CLI nachgestellt, inklusive Fortsetzung eines bereits bestehenden Bundles).raw/CONTRACT.mdträgt beide Regeln aus Entscheidung 4, einschließlich des Satzes, dass die Wahl dem Menschen gehört.tools/CONTRACT.mdträgt Kommando- und Fehlerkontrakt-Zeile;docs verifyerzwingt beide Richtungen.instructions/wiki-ingest/SKILL.mdbenennt 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 Commit0b3c496beide mitsuccessabgeschlossen.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:--page, gegen bestehendes Bundle)incoming/-Dateien liegen unberührtincoming/umbenannt, dann normal akzeptiertraw/documents/handbuch-netzplan/, altes Bundle unangetastet--replaces --dry-runincoming/-Bytes unverändert--replacesechtincoming/-Datei verbraucht,raw_files:unverändert, Nachbardatei im Bundle unangetastet[[Source - Handbuch]]undcited by: [[Handbuch-Konzept]]--page(Einzeldatei → Bundle)raw_files:auf beide Pfade nachgezogen--pageraw_files:auf 3 Pfade--replaces-Ablehnungen (zwei Dateien, Namensabweichung, Typabweichung, fehlendes Ziel, mit--pagekombiniert)incoming/-Bytes unverändert--replacesauf Datei mit zwei OwnernEin Zwischenbefund beim Nachstellen war ein Artefakt des Wegwerf-Baums, kein Defekt: ohne
types/unter der Wurzel löstPage.kindnicht auf, jede Source-Seite fällt aussource_pages_by_raw_fileheraus, und--replacesmeldete „No source page covers this file". Mit vorhandenemtypes/— 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.4und dieCHANGES.md-Überschrift stimmen überein,sources coverageweiterhin0/0/0.Nicht-Ziele
--replacesbleibt eine Datei für eine Datei.Abgrenzung
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.incoming/, nicht die Beförderung daraus. Unberührt.Version
4.8.0-beta.3→4.8.0-beta.4(--minor). Kein--major:raw acceptwar nie released (letztes Releasev4.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.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/--pageschließ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: 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
0b3c496sind seither beide mitsuccessdurchgelaufen — die vorherige Fassung sagte „noch in Bearbeitung". Kollisionsmeldung im Body durch die tatsächliche CLI-Ausgabe ersetzt und um den Bundle-Fall ergänzt.