Abgetrennt aus #178 (dort am 2026-10-05 gegen den Baum 8ce202a gefunden und auf MVP reduziert; der Befund galt unabhängig vom Intake). Am 2026-10-06 gegen den Baum 40839d9 nachgeprüft, am selben Tag gebaut (8.0.0-beta.46) und geschlossen.
Problem (bis 8.0.0-beta.45)
reconcile() in tools/chemenu/commands/git_publish.py kannte generierte Dateien nicht, obwohl dieselbe Datei sie mit is_generated() schon für den Mass-Update-Gate ausnahm. Jedes Ingest berührt mindestens kb/index.md und kb/log.md. Deshalb scheiterte reconcile (hinter sync und beiden Reconciles von publish) an drei Formen derselben Lage:
Ungespeichertes Ingest, <remote>/main hatte sich bewegt (ff-possible): git merge --ff-only verweigerte wegen der lokal geänderten kb/index.md/kb/log.md, publish endete mit Exit 1.
Beide Seiten mit Commits (diverged, z. B. ein gestrandeter Commit nach fehlgeschlagenem Push, oder der Retry-Pfad nach abgelehntem Push): die generierten Dateien zählten als Überlappung → Rebase-Review-Gate (Exit 42); nach Freigabe scheiterte git rebase an den zwei Anhängen am Ende von kb/log.md → Exit 1.
diverged mit ungespeicherten Änderungen:git rebase verweigerte bei jeder geänderten getrackten Datei, auch einer nicht überlappenden. Typisch: gestrandeter Commit + neues ungespeichertes Ingest → publish Exit 1.
Außerdem fing is_generated() jede Datei, die auf /INDEX.md endet - auch eine unter raw/, die der neue Pfad sonst verworfen hätte.
Entscheidungen
Fall 3 wird mit abgedeckt (Betreiber, 2026-10-06). Ohne ihn hätte ein ungespeichertes Ingest über einem gestrandeten Commit - der im Betrieb wahrscheinlichste Fall - weiter scheitern müssen.
Versionsteil --patch, Drop-in: kein Befehl, Flag, Dateiformat oder Zustandsfile ändert sich; was vorher mit Exit 1 oder 42 endete, geht durch, und kein Lauf, der vorher ohne Gate und Fehler durchging, ändert sein Ergebnis. Ein vor dem Update ausgestellter --confirm-rebase-Token kann danach veraltet sein, weil overlap_files keine generierten Dateien mehr enthält - der Gate fragt dann neu. Die Vorversion ignoriert einen liegengebliebenen Sicherungs-Ref.
Umsetzung
Grundsatz: Der mechanische Pfad greift nur, wo git sonst gescheitert wäre. ff-possible versucht zuerst git merge --ff-only wie zuvor; diverged nimmt den bisherigen git rebase unverändert, wenn nichts Ungespeichertes im Weg liegt (getrackt geändert, oder eine ungetrackte generierte Datei) und keine generierte Datei auf beiden Seiten committet ist. Dort läuft auch kein Rebuild - index rebuild stempelt das heutige Datum in kb/index.md, und jeder sync endete sonst mit einer geänderten Datei.
Generiert heißt kb/:is_generated() gilt nur noch für kb/index.md, kb/log.md, kb/provenance.md und kb/**/INDEX.md (wie AGENTS.md Invariante 1). Eine INDEX.md unter raw/ zählt im Mass-Update-Gate und geht in den Rebase-Review-Gate.
Überlappung: generierte Pfade zählen nicht; nur überlappende nicht generierte Dateien führen in den Rebase-Review-Gate, dessen overlap_files, Token und Diff keine generierten Dateien mehr enthalten.
Beiseitelegen: Im mechanischen Pfad werden alle ungespeicherten generierten Dateien beiseitegelegt (danach ohnehin alle neu erzeugt): von kb/log.md die Bytes über die HEAD-Fassung hinaus, der Rest auf HEAD zurückgesetzt bzw. gelöscht. Im ff-possible-Fall nur, wenn mindestens eine davon mit einer eingehenden Datei kollidiert.
Fall 3:git rebase --autostash, nach dem Beiseitelegen, damit der Autostash nur nicht generierte Änderungen enthält; rebase --abort spielt ihn ebenfalls zurück.
Vorbedingungen, alle vor dem ersten Schreibschritt geprüft; ist eine verletzt, endet reconcile mit conflict und ändert nichts: die Arbeitsfassung von kb/log.md beginnt byte-genau mit der HEAD-Fassung; keine ungespeicherte nicht generierte Datei liegt in touched_files(base, remote_tip); keine ungetrackte Datei liegt auf einem eingehend angelegten Pfad.
Konflikte beim Rebase, Schritt für Schritt (git diff --name-only --diff-filter=U, GIT_EDITOR=true): nur generierte Dateien im Konflikt → Katalog/Provenance nehmen Stage 2 (die onto-Seite), kb/log.md Stage 2 plus den Rest von Stage 3 über Stage 1 hinaus (Vorbedingung: Stage 3 beginnt mit Stage 1); eine nicht generierte Datei im Konflikt → rebase --abort, Zurückspielen, conflict. Ein dadurch leer gewordener lokaler Commit wird von git rebase --continue selbst verworfen (mit git 2.56 geprüft).
Danach: Neu erzeugt wird, wenn etwas beiseitegelegt oder aufgelöst wurde oder beide Seiten eine generierte Datei committet haben (ein textuell sauberer Merge zweier Kataloge kann trotzdem falsch sein). Erst kommen die Log-Bytes ans Ende von kb/log.md, dann index_build.write_index und provenance_cmd.write_provenance_index (aus index rebuild und sources rebuild-index herausgezogen); Einstieg git_publish.regenerate_generated. Die neu erzeugten Dateien bleiben als ungespeicherte Änderung liegen: sync überlässt sie dem nächsten publish, der proaktive Reconcile von publish läuft vor dem Staging, und der Retry-Pfad nach abgelehntem Push committet sie über git_publish.commit_regenerated vor dem zweiten Push.
Sicherung: vor dem ersten Schreibschritt ein Commit des ganzen Arbeitsbaums (getrackt und ungetrackt, über eine Scratch-Kopie des Index, eigene Identität wikitool) unter refs/wikitool/reconcile-backup; bei Erfolg gelöscht, bei Fehlerausgang behalten und gemeldet. Ein Ausnahmefehler mitten im Rebase bricht ihn ab und spielt die generierten Dateien zurück.
Bewusst nicht behandelt: Ein publish --path <X>, dessen Umfang die generierten Dateien nicht einschließt, committet den neu erzeugten Katalog nicht; der gepushte Stand trägt dann bis zum nächsten publish ohne --path den Katalog der eingehenden Seite, die neu erzeugten Dateien liegen lokal als ungespeicherte Änderung. --path ist eine bewusste Teilmenge, und den Umfang still zu erweitern hätte genau das unterlaufen.
Akzeptanzkriterien
Die Tests stehen in tools/chemenu/tests/test_git_publish.py, Abschnitt "Reconciling around generated files"; die Fixture wiki_repo baut den Katalog mit den ausgelieferten Funktionen.
Fall 1: Ein ungespeichertes Ingest, während <remote>/main ein anderes Ingest bekommen hat, wird von publish ohne Gate und ohne Fehler publiziert. (test_case_1_an_uncommitted_ingest_publishes_over_an_ingest_that_landed_meanwhile, test_case_1_on_sync_leaves_the_regenerated_catalog_uncommitted)
Fall 2: Zwei Ingests auf divergierten Ständen mit Commits, die außer generierten Dateien nichts gemeinsam haben, führen bei publish/sync nicht ins Rebase-Review-Gate und enden mit Exit 0; überlappende nicht generierte Dateien führen weiterhin hinein. (test_case_2_*)
Fall 2 im Retry-Pfad: Wird der Push abgelehnt, weil <remote>/main zwischen proaktivem Reconcile und Push ein Ingest bekommen hat, endet publish mit Exit 0, und der gepushte Tip erfüllt das Katalog-Kriterium unten. (test_case_2_in_the_retry_path_commits_the_regenerated_catalog_before_the_push)
Fall 3: Gestrandeter lokaler Commit + ungespeichertes Ingest, dessen nicht generierte Dateien keine eingehende Datei berühren → publish endet mit Exit 0; die ungespeicherten Seiten sind im gepushten Stand. Dasselbe vor sync: Exit 0, die ungespeicherten Seiten liegen danach unverändert im Arbeitsbaum. (test_case_3_*, auch ohne generierte Dateien: kein Rebuild)
Invariante: Jede nicht generierte Datei im Arbeitsbaum ist nach reconcile byte-identisch zu vorher - ungespeicherte Arbeit geht nie verloren, auch nicht bei einem Fehlerausgang (getestet mit einem erzwungenen Abbruch mitten im Rebase). (test_uncommitted_work_survives_an_abort_in_the_middle_of_the_rebase, als Ausnahme und als gemeldeter Fehlschlag; dazu: Branch unbewegt, kein Rebase offen, Stash-Liste leer, Sicherungs-Ref hält die ungespeicherte Seite)
Jeder Log-Eintrag beider Seiten steht danach genau einmal in kb/log.md, eingehende zuerst; kb/index.md, jedes INDEX.md und kb/provenance.md gleichen Byte für Byte dem, was index rebuild/sources rebuild-index am selben Tag auf dem Ergebnis erzeugen. (in jedem Fall-Test geprüft, beim publish an einem frischen Klon des gepushten Tips)
Ist eine Vorbedingung verletzt (Log nicht nur angehängt, eine nicht generierte Datei kollidiert - getrackt geändert oder ungetrackt auf einem eingehend angelegten Pfad), verhält sich reconcile wie zuvor und lässt Arbeitsbaum und Branch unverändert. (vier Tests: Log von Hand geändert, Log in einem lokalen Commit umgeschrieben, ungespeicherte Änderung an eingehend geänderter Seite, ungetrackte Datei auf eingehendem Pfad)
Tritt keiner der Fälle 1-3 ein, ist das Ergebnis von reconcile identisch zu zuvor; insbesondere läuft kein Rebuild (bestehende reconcile-/sync-Tests unverändert grün). (alle bestehenden Tests unverändert grün; test_a_plain_fast_forward_regenerates_nothing)
Eine Datei raw/<…>/INDEX.md gilt nicht als generiert: weder im Mass-Update-Zähler noch im Reconcile. (test_an_index_md_outside_kb_is_not_generated, test_an_index_md_under_raw_still_goes_to_review_when_both_sides_change_it)
pytest, docs verify, instructions verify grün; CI grün. (siehe Verifikation)
Geänderte Stellen
tools/chemenu/commands/git_publish.py: is_generated, reconcile und die neuen Helfer (Abschnitt "Reconciling around generated files"), _run mit env, Retry-Pfad in publish_command, _reconcile_summary, Records von sync/publish
tools/CONTRACT.md (per docs contract --apply), instructions/session-setup.md (sync-Absatz), instructions/gates.md (Rebase-Review: was ihn nicht auslöst), CHANGES.md
tools/chemenu/tests/test_git_publish.py: 16 neue Tests, Fixture wiki_repo
Geprüft und unverändert gelassen, weil sie nichts über den Reconcile behaupten: kb/CONTRACT.md § Generated files, README.md, tools/README.md, docs/why-gates-are-code.md (der Rebase-Review ist dort kein benannter Gate).
Verifikation
8.0.0-beta.46, Commit d4638ba: lokal pytest 2322 bestanden / 3 übersprungen, auf leerer Maschine identisch; docs verify, instructions verify grün; CI-Runs 559 und 560 grün.
8.0.0-beta.47, Commit fa106ee (Abschlussphase: eine Note im sync-Record beschrieb die Änderung - "no longer" - statt des Verhaltens): docs verify, instructions verify, die betroffenen Tests grün; CI-Runs 561 und 562 grün.
Priorität
War prio/planned (Betreiber, 2026-10-06), vorher prio/waiting mit dem Auslöser "eine Instanz wird von mehr als einem Checkout oder einer parallelen Sitzung aus beschrieben".
Abgetrennt aus #178 (dort am 2026-10-05 gegen den Baum `8ce202a` gefunden und auf MVP reduziert; der Befund galt unabhängig vom Intake). Am 2026-10-06 gegen den Baum `40839d9` nachgeprüft, am selben Tag gebaut (`8.0.0-beta.46`) und geschlossen.
## Problem (bis `8.0.0-beta.45`)
`reconcile()` in `tools/chemenu/commands/git_publish.py` kannte generierte Dateien nicht, obwohl dieselbe Datei sie mit `is_generated()` schon für den Mass-Update-Gate ausnahm. Jedes Ingest berührt mindestens `kb/index.md` und `kb/log.md`. Deshalb scheiterte `reconcile` (hinter `sync` und beiden Reconciles von `publish`) an drei Formen derselben Lage:
1. **Ungespeichertes Ingest, `<remote>/main` hatte sich bewegt** (`ff-possible`): `git merge --ff-only` verweigerte wegen der lokal geänderten `kb/index.md`/`kb/log.md`, `publish` endete mit **Exit 1**.
2. **Beide Seiten mit Commits** (`diverged`, z. B. ein gestrandeter Commit nach fehlgeschlagenem Push, oder der Retry-Pfad nach abgelehntem Push): die generierten Dateien zählten als Überlappung → **Rebase-Review-Gate (Exit 42)**; nach Freigabe scheiterte `git rebase` an den zwei Anhängen am Ende von `kb/log.md` → Exit 1.
3. **`diverged` mit ungespeicherten Änderungen:** `git rebase` verweigerte bei *jeder* geänderten getrackten Datei, auch einer nicht überlappenden. Typisch: gestrandeter Commit + neues ungespeichertes Ingest → `publish` Exit 1.
Außerdem fing `is_generated()` jede Datei, die auf `/INDEX.md` endet - auch eine unter `raw/`, die der neue Pfad sonst verworfen hätte.
## Entscheidungen
1. **Fall 3 wird mit abgedeckt** (Betreiber, 2026-10-06). Ohne ihn hätte ein ungespeichertes Ingest über einem gestrandeten Commit - der im Betrieb wahrscheinlichste Fall - weiter scheitern müssen.
2. **Versionsteil `--patch`, Drop-in:** kein Befehl, Flag, Dateiformat oder Zustandsfile ändert sich; was vorher mit Exit 1 oder 42 endete, geht durch, und kein Lauf, der vorher ohne Gate und Fehler durchging, ändert sein Ergebnis. Ein vor dem Update ausgestellter `--confirm-rebase`-Token kann danach veraltet sein, weil `overlap_files` keine generierten Dateien mehr enthält - der Gate fragt dann neu. Die Vorversion ignoriert einen liegengebliebenen Sicherungs-Ref.
## Umsetzung
**Grundsatz:** Der mechanische Pfad greift nur, wo git sonst gescheitert wäre. `ff-possible` versucht zuerst `git merge --ff-only` wie zuvor; `diverged` nimmt den bisherigen `git rebase` unverändert, wenn nichts Ungespeichertes im Weg liegt (getrackt geändert, oder eine ungetrackte generierte Datei) und keine generierte Datei auf beiden Seiten committet ist. Dort läuft auch kein Rebuild - `index rebuild` stempelt das heutige Datum in `kb/index.md`, und jeder `sync` endete sonst mit einer geänderten Datei.
- **Generiert heißt `kb/`:** `is_generated()` gilt nur noch für `kb/index.md`, `kb/log.md`, `kb/provenance.md` und `kb/**/INDEX.md` (wie AGENTS.md Invariante 1). Eine `INDEX.md` unter `raw/` zählt im Mass-Update-Gate und geht in den Rebase-Review-Gate.
- **Überlappung:** generierte Pfade zählen nicht; nur überlappende *nicht* generierte Dateien führen in den Rebase-Review-Gate, dessen `overlap_files`, Token und Diff keine generierten Dateien mehr enthalten.
- **Beiseitelegen:** Im mechanischen Pfad werden alle ungespeicherten generierten Dateien beiseitegelegt (danach ohnehin alle neu erzeugt): von `kb/log.md` die Bytes über die `HEAD`-Fassung hinaus, der Rest auf `HEAD` zurückgesetzt bzw. gelöscht. Im `ff-possible`-Fall nur, wenn mindestens eine davon mit einer eingehenden Datei kollidiert.
- **Fall 3:** `git rebase --autostash`, nach dem Beiseitelegen, damit der Autostash nur nicht generierte Änderungen enthält; `rebase --abort` spielt ihn ebenfalls zurück.
- **Vorbedingungen**, alle vor dem ersten Schreibschritt geprüft; ist eine verletzt, endet `reconcile` mit `conflict` und ändert nichts: die Arbeitsfassung von `kb/log.md` beginnt byte-genau mit der `HEAD`-Fassung; keine ungespeicherte nicht generierte Datei liegt in `touched_files(base, remote_tip)`; keine ungetrackte Datei liegt auf einem eingehend angelegten Pfad.
- **Konflikte beim Rebase**, Schritt für Schritt (`git diff --name-only --diff-filter=U`, `GIT_EDITOR=true`): nur generierte Dateien im Konflikt → Katalog/Provenance nehmen Stage 2 (die `onto`-Seite), `kb/log.md` Stage 2 plus den Rest von Stage 3 über Stage 1 hinaus (Vorbedingung: Stage 3 beginnt mit Stage 1); eine nicht generierte Datei im Konflikt → `rebase --abort`, Zurückspielen, `conflict`. Ein dadurch leer gewordener lokaler Commit wird von `git rebase --continue` selbst verworfen (mit git 2.56 geprüft).
- **Danach:** Neu erzeugt wird, wenn etwas beiseitegelegt oder aufgelöst wurde oder beide Seiten eine generierte Datei committet haben (ein textuell sauberer Merge zweier Kataloge kann trotzdem falsch sein). Erst kommen die Log-Bytes ans Ende von `kb/log.md`, dann `index_build.write_index` und `provenance_cmd.write_provenance_index` (aus `index rebuild` und `sources rebuild-index` herausgezogen); Einstieg `git_publish.regenerate_generated`. Die neu erzeugten Dateien bleiben als ungespeicherte Änderung liegen: `sync` überlässt sie dem nächsten `publish`, der proaktive Reconcile von `publish` läuft vor dem Staging, und der Retry-Pfad nach abgelehntem Push committet sie über `git_publish.commit_regenerated` vor dem zweiten Push.
- **Sicherung:** vor dem ersten Schreibschritt ein Commit des ganzen Arbeitsbaums (getrackt und ungetrackt, über eine Scratch-Kopie des Index, eigene Identität `wikitool`) unter `refs/wikitool/reconcile-backup`; bei Erfolg gelöscht, bei Fehlerausgang behalten und gemeldet. Ein Ausnahmefehler mitten im Rebase bricht ihn ab und spielt die generierten Dateien zurück.
**Bewusst nicht behandelt:** Ein `publish --path <X>`, dessen Umfang die generierten Dateien nicht einschließt, committet den neu erzeugten Katalog nicht; der gepushte Stand trägt dann bis zum nächsten `publish` ohne `--path` den Katalog der eingehenden Seite, die neu erzeugten Dateien liegen lokal als ungespeicherte Änderung. `--path` ist eine bewusste Teilmenge, und den Umfang still zu erweitern hätte genau das unterlaufen.
## Akzeptanzkriterien
Die Tests stehen in `tools/chemenu/tests/test_git_publish.py`, Abschnitt "Reconciling around generated files"; die Fixture `wiki_repo` baut den Katalog mit den ausgelieferten Funktionen.
- [x] Fall 1: Ein ungespeichertes Ingest, während `<remote>/main` ein anderes Ingest bekommen hat, wird von `publish` ohne Gate und ohne Fehler publiziert. (`test_case_1_an_uncommitted_ingest_publishes_over_an_ingest_that_landed_meanwhile`, `test_case_1_on_sync_leaves_the_regenerated_catalog_uncommitted`)
- [x] Fall 2: Zwei Ingests auf divergierten Ständen mit Commits, die außer generierten Dateien nichts gemeinsam haben, führen bei `publish`/`sync` nicht ins Rebase-Review-Gate und enden mit Exit 0; überlappende *nicht* generierte Dateien führen weiterhin hinein. (`test_case_2_*`)
- [x] Fall 2 im Retry-Pfad: Wird der Push abgelehnt, weil `<remote>/main` zwischen proaktivem Reconcile und Push ein Ingest bekommen hat, endet `publish` mit Exit 0, und der gepushte Tip erfüllt das Katalog-Kriterium unten. (`test_case_2_in_the_retry_path_commits_the_regenerated_catalog_before_the_push`)
- [x] Fall 3: Gestrandeter lokaler Commit + ungespeichertes Ingest, dessen nicht generierte Dateien keine eingehende Datei berühren → `publish` endet mit Exit 0; die ungespeicherten Seiten sind im gepushten Stand. Dasselbe vor `sync`: Exit 0, die ungespeicherten Seiten liegen danach unverändert im Arbeitsbaum. (`test_case_3_*`, auch ohne generierte Dateien: kein Rebuild)
- [x] **Invariante:** Jede nicht generierte Datei im Arbeitsbaum ist nach `reconcile` byte-identisch zu vorher - ungespeicherte Arbeit geht nie verloren, auch nicht bei einem Fehlerausgang (getestet mit einem erzwungenen Abbruch mitten im Rebase). (`test_uncommitted_work_survives_an_abort_in_the_middle_of_the_rebase`, als Ausnahme und als gemeldeter Fehlschlag; dazu: Branch unbewegt, kein Rebase offen, Stash-Liste leer, Sicherungs-Ref hält die ungespeicherte Seite)
- [x] Jeder Log-Eintrag beider Seiten steht danach genau einmal in `kb/log.md`, eingehende zuerst; `kb/index.md`, jedes `INDEX.md` und `kb/provenance.md` gleichen Byte für Byte dem, was `index rebuild`/`sources rebuild-index` am selben Tag auf dem Ergebnis erzeugen. (in jedem Fall-Test geprüft, beim `publish` an einem frischen Klon des gepushten Tips)
- [x] Ist eine Vorbedingung verletzt (Log nicht nur angehängt, eine nicht generierte Datei kollidiert - getrackt geändert oder ungetrackt auf einem eingehend angelegten Pfad), verhält sich `reconcile` wie zuvor und lässt Arbeitsbaum und Branch unverändert. (vier Tests: Log von Hand geändert, Log in einem lokalen Commit umgeschrieben, ungespeicherte Änderung an eingehend geänderter Seite, ungetrackte Datei auf eingehendem Pfad)
- [x] Tritt keiner der Fälle 1-3 ein, ist das Ergebnis von `reconcile` identisch zu zuvor; insbesondere läuft kein Rebuild (bestehende `reconcile`-/`sync`-Tests unverändert grün). (alle bestehenden Tests unverändert grün; `test_a_plain_fast_forward_regenerates_nothing`)
- [x] Eine Datei `raw/<…>/INDEX.md` gilt nicht als generiert: weder im Mass-Update-Zähler noch im Reconcile. (`test_an_index_md_outside_kb_is_not_generated`, `test_an_index_md_under_raw_still_goes_to_review_when_both_sides_change_it`)
- [x] `pytest`, `docs verify`, `instructions verify` grün; CI grün. (siehe Verifikation)
## Geänderte Stellen
- `tools/chemenu/commands/git_publish.py`: `is_generated`, `reconcile` und die neuen Helfer (Abschnitt "Reconciling around generated files"), `_run` mit `env`, Retry-Pfad in `publish_command`, `_reconcile_summary`, Records von `sync`/`publish`
- `tools/chemenu/commands/index_build.py` (`write_index`), `tools/chemenu/commands/provenance_cmd.py` (`write_provenance_index`)
- `tools/CONTRACT.md` (per `docs contract --apply`), `instructions/session-setup.md` (`sync`-Absatz), `instructions/gates.md` (Rebase-Review: was ihn nicht auslöst), `CHANGES.md`
- `tools/chemenu/tests/test_git_publish.py`: 16 neue Tests, Fixture `wiki_repo`
Geprüft und unverändert gelassen, weil sie nichts über den Reconcile behaupten: `kb/CONTRACT.md` § Generated files, `README.md`, `tools/README.md`, `docs/why-gates-are-code.md` (der Rebase-Review ist dort kein benannter Gate).
## Verifikation
- `8.0.0-beta.46`, Commit `d4638ba`: lokal `pytest` 2322 bestanden / 3 übersprungen, auf leerer Maschine identisch; `docs verify`, `instructions verify` grün; CI-Runs [559](https://gitea.nehmer.net/torben/chemenu/actions/runs/559) und [560](https://gitea.nehmer.net/torben/chemenu/actions/runs/560) grün.
- `8.0.0-beta.47`, Commit `fa106ee` (Abschlussphase: eine Note im `sync`-Record beschrieb die Änderung - "no longer" - statt des Verhaltens): `docs verify`, `instructions verify`, die betroffenen Tests grün; CI-Runs [561](https://gitea.nehmer.net/torben/chemenu/actions/runs/561) und [562](https://gitea.nehmer.net/torben/chemenu/actions/runs/562) grün.
## Priorität
War `prio/planned` (Betreiber, 2026-10-06), vorher `prio/waiting` mit dem Auslöser "eine Instanz wird von mehr als einem Checkout oder einer parallelen Sitzung aus beschrieben".
Changelog: Am Baum 40839d9 nachgeprüft. Neu: Fall 3 (diverged + ungespeicherte, nicht überlappende Änderung → git rebase verweigert; im Scratch-Repo nachgestellt) als offene Frage; is_generated() wird auf kb/ eingeengt (sonst würde eine raw/**/INDEX.md verworfen); kein Rebuild ohne Kollision (Last Updated in kb/index.md würde jeden sync verschmutzen); der Retry-Pfad in publish_command muss neu erzeugte Dateien vor dem zweiten Push committen; Sicherung vor dem ersten Schreibschritt; Konfliktauflösung über Stages 1/2/3 präzisiert. Akzeptanzkriterien: Retry-Pfad, unveränderter Lauf ohne Kollision und raw/-INDEX.md neu. Korrigiert: sources rebuild-index liegt in provenance_cmd.py. Neu gelabelt kind/defect → kind/decision wegen der offenen Frage.
**Changelog:** Am Baum `40839d9` nachgeprüft. Neu: Fall 3 (`diverged` + ungespeicherte, nicht überlappende Änderung → `git rebase` verweigert; im Scratch-Repo nachgestellt) als offene Frage; `is_generated()` wird auf `kb/` eingeengt (sonst würde eine `raw/**/INDEX.md` verworfen); kein Rebuild ohne Kollision (`Last Updated` in `kb/index.md` würde jeden `sync` verschmutzen); der Retry-Pfad in `publish_command` muss neu erzeugte Dateien vor dem zweiten Push committen; Sicherung vor dem ersten Schreibschritt; Konfliktauflösung über Stages 1/2/3 präzisiert. Akzeptanzkriterien: Retry-Pfad, unveränderter Lauf ohne Kollision und `raw/`-`INDEX.md` neu. Korrigiert: `sources rebuild-index` liegt in `provenance_cmd.py`. Neu gelabelt `kind/defect` → `kind/decision` wegen der offenen Frage.
torben
changed title from publish/sync scheitern, wenn sich lokale und eingehende Änderungen nur in generierten Dateien überschneiden to publish/sync scheitern an Überschneidungen nur in generierten Dateien und an nicht überlappender ungespeicherter Arbeit2026-10-06 06:23:16 +00:00
Changelog: Offene Frage 1 entschieden (Betreiber): Fall 3 wird mit abgedeckt - jetzt Entscheidung 1, eigener Entwurfspunkt mit Vorbedingung (keine Überlappung mit touched_files(base, remote_tip), keine ungetrackte Datei auf einem eingehend angelegten Pfad), Akzeptanzkriterium ohne Vorbehalt und um sync ergänzt. Titel erweitert. Neu gelabelt: kind/decision → kind/build (keine offene Frage mehr), prio/waiting → prio/planned (Betreiber: wird jetzt gebaut).
**Changelog:** Offene Frage 1 entschieden (Betreiber): Fall 3 wird mit abgedeckt - jetzt Entscheidung 1, eigener Entwurfspunkt mit Vorbedingung (keine Überlappung mit `touched_files(base, remote_tip)`, keine ungetrackte Datei auf einem eingehend angelegten Pfad), Akzeptanzkriterium ohne Vorbehalt und um `sync` ergänzt. Titel erweitert. Neu gelabelt: `kind/decision` → `kind/build` (keine offene Frage mehr), `prio/waiting` → `prio/planned` (Betreiber: wird jetzt gebaut).
Changelog: Gebaut und publiziert als 8.0.0-beta.46 (d4638ba), CI-Runs 559/560 grün; alle Akzeptanzkriterien abgehakt, jedes mit den Tests, die es belegen. Entwurf um die Umsetzung präzisiert: im mechanischen Pfad werden alle ungespeicherten generierten Dateien beiseitegelegt (gleiches Ergebnis, weil alle neu erzeugt werden); Rebuild auch, wenn beide Seiten eine generierte Datei committet haben und git sie textuell sauber gemergt hat; Fall 3 über git rebase --autostash; Sicherung als Commit des ganzen Arbeitsbaums unter refs/wikitool/reconcile-backup; eine verletzte Vorbedingung im diverged-Fall meldet conflict mit der Datei statt git's Verweigerung; ein durch die Auflösung leer gewordener Commit wird von git selbst verworfen (geprüft). Neuer Abschnitt "Stand".
**Changelog:** Gebaut und publiziert als `8.0.0-beta.46` (`d4638ba`), CI-Runs 559/560 grün; alle Akzeptanzkriterien abgehakt, jedes mit den Tests, die es belegen. Entwurf um die Umsetzung präzisiert: im mechanischen Pfad werden *alle* ungespeicherten generierten Dateien beiseitegelegt (gleiches Ergebnis, weil alle neu erzeugt werden); Rebuild auch, wenn beide Seiten eine generierte Datei committet haben und git sie textuell sauber gemergt hat; Fall 3 über `git rebase --autostash`; Sicherung als Commit des ganzen Arbeitsbaums unter `refs/wikitool/reconcile-backup`; eine verletzte Vorbedingung im `diverged`-Fall meldet `conflict` mit der Datei statt git's Verweigerung; ein durch die Auflösung leer gewordener Commit wird von git selbst verworfen (geprüft). Neuer Abschnitt "Stand".
Changelog: Endstand. Problem in die Vergangenheit gesetzt ("bis 8.0.0-beta.45"), "Entwurf" ist jetzt "Umsetzung", der Versionsteil steht als Entscheidung 2. Neu: "Bewusst nicht behandelt" (publish --path ohne die generierten Dateien im Umfang), "Geänderte Stellen" mit den geprüften und unverändert gelassenen Dokumenten, "Verifikation" mit beiden Ständen. In der Abschlussphase nachgezogen: 8.0.0-beta.47 (fa106ee), eine Note im sync-Record beschreibt jetzt das Verhalten statt der Änderung; CI 561/562 grün. Geschlossen.
**Changelog:** Endstand. Problem in die Vergangenheit gesetzt ("bis `8.0.0-beta.45`"), "Entwurf" ist jetzt "Umsetzung", der Versionsteil steht als Entscheidung 2. Neu: "Bewusst nicht behandelt" (`publish --path` ohne die generierten Dateien im Umfang), "Geänderte Stellen" mit den geprüften und unverändert gelassenen Dokumenten, "Verifikation" mit beiden Ständen. In der Abschlussphase nachgezogen: `8.0.0-beta.47` (`fa106ee`), eine Note im `sync`-Record beschreibt jetzt das Verhalten statt der Änderung; CI 561/562 grün. 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.
Abgetrennt aus #178 (dort am 2026-10-05 gegen den Baum
8ce202agefunden und auf MVP reduziert; der Befund galt unabhängig vom Intake). Am 2026-10-06 gegen den Baum40839d9nachgeprüft, am selben Tag gebaut (8.0.0-beta.46) und geschlossen.Problem (bis
8.0.0-beta.45)reconcile()intools/chemenu/commands/git_publish.pykannte generierte Dateien nicht, obwohl dieselbe Datei sie mitis_generated()schon für den Mass-Update-Gate ausnahm. Jedes Ingest berührt mindestenskb/index.mdundkb/log.md. Deshalb scheitertereconcile(hintersyncund beiden Reconciles vonpublish) an drei Formen derselben Lage:<remote>/mainhatte sich bewegt (ff-possible):git merge --ff-onlyverweigerte wegen der lokal geändertenkb/index.md/kb/log.md,publishendete mit Exit 1.diverged, z. B. ein gestrandeter Commit nach fehlgeschlagenem Push, oder der Retry-Pfad nach abgelehntem Push): die generierten Dateien zählten als Überlappung → Rebase-Review-Gate (Exit 42); nach Freigabe scheitertegit rebasean den zwei Anhängen am Ende vonkb/log.md→ Exit 1.divergedmit ungespeicherten Änderungen:git rebaseverweigerte bei jeder geänderten getrackten Datei, auch einer nicht überlappenden. Typisch: gestrandeter Commit + neues ungespeichertes Ingest →publishExit 1.Außerdem fing
is_generated()jede Datei, die auf/INDEX.mdendet - auch eine unterraw/, die der neue Pfad sonst verworfen hätte.Entscheidungen
--patch, Drop-in: kein Befehl, Flag, Dateiformat oder Zustandsfile ändert sich; was vorher mit Exit 1 oder 42 endete, geht durch, und kein Lauf, der vorher ohne Gate und Fehler durchging, ändert sein Ergebnis. Ein vor dem Update ausgestellter--confirm-rebase-Token kann danach veraltet sein, weiloverlap_fileskeine generierten Dateien mehr enthält - der Gate fragt dann neu. Die Vorversion ignoriert einen liegengebliebenen Sicherungs-Ref.Umsetzung
Grundsatz: Der mechanische Pfad greift nur, wo git sonst gescheitert wäre.
ff-possibleversucht zuerstgit merge --ff-onlywie zuvor;divergednimmt den bisherigengit rebaseunverändert, wenn nichts Ungespeichertes im Weg liegt (getrackt geändert, oder eine ungetrackte generierte Datei) und keine generierte Datei auf beiden Seiten committet ist. Dort läuft auch kein Rebuild -index rebuildstempelt das heutige Datum inkb/index.md, und jedersyncendete sonst mit einer geänderten Datei.kb/:is_generated()gilt nur noch fürkb/index.md,kb/log.md,kb/provenance.mdundkb/**/INDEX.md(wie AGENTS.md Invariante 1). EineINDEX.mdunterraw/zählt im Mass-Update-Gate und geht in den Rebase-Review-Gate.overlap_files, Token und Diff keine generierten Dateien mehr enthalten.kb/log.mddie Bytes über dieHEAD-Fassung hinaus, der Rest aufHEADzurückgesetzt bzw. gelöscht. Imff-possible-Fall nur, wenn mindestens eine davon mit einer eingehenden Datei kollidiert.git rebase --autostash, nach dem Beiseitelegen, damit der Autostash nur nicht generierte Änderungen enthält;rebase --abortspielt ihn ebenfalls zurück.reconcilemitconflictund ändert nichts: die Arbeitsfassung vonkb/log.mdbeginnt byte-genau mit derHEAD-Fassung; keine ungespeicherte nicht generierte Datei liegt intouched_files(base, remote_tip); keine ungetrackte Datei liegt auf einem eingehend angelegten Pfad.git diff --name-only --diff-filter=U,GIT_EDITOR=true): nur generierte Dateien im Konflikt → Katalog/Provenance nehmen Stage 2 (dieonto-Seite),kb/log.mdStage 2 plus den Rest von Stage 3 über Stage 1 hinaus (Vorbedingung: Stage 3 beginnt mit Stage 1); eine nicht generierte Datei im Konflikt →rebase --abort, Zurückspielen,conflict. Ein dadurch leer gewordener lokaler Commit wird vongit rebase --continueselbst verworfen (mit git 2.56 geprüft).kb/log.md, dannindex_build.write_indexundprovenance_cmd.write_provenance_index(ausindex rebuildundsources rebuild-indexherausgezogen); Einstieggit_publish.regenerate_generated. Die neu erzeugten Dateien bleiben als ungespeicherte Änderung liegen:syncüberlässt sie dem nächstenpublish, der proaktive Reconcile vonpublishläuft vor dem Staging, und der Retry-Pfad nach abgelehntem Push committet sie übergit_publish.commit_regeneratedvor dem zweiten Push.wikitool) unterrefs/wikitool/reconcile-backup; bei Erfolg gelöscht, bei Fehlerausgang behalten und gemeldet. Ein Ausnahmefehler mitten im Rebase bricht ihn ab und spielt die generierten Dateien zurück.Bewusst nicht behandelt: Ein
publish --path <X>, dessen Umfang die generierten Dateien nicht einschließt, committet den neu erzeugten Katalog nicht; der gepushte Stand trägt dann bis zum nächstenpublishohne--pathden Katalog der eingehenden Seite, die neu erzeugten Dateien liegen lokal als ungespeicherte Änderung.--pathist eine bewusste Teilmenge, und den Umfang still zu erweitern hätte genau das unterlaufen.Akzeptanzkriterien
Die Tests stehen in
tools/chemenu/tests/test_git_publish.py, Abschnitt "Reconciling around generated files"; die Fixturewiki_repobaut den Katalog mit den ausgelieferten Funktionen.<remote>/mainein anderes Ingest bekommen hat, wird vonpublishohne Gate und ohne Fehler publiziert. (test_case_1_an_uncommitted_ingest_publishes_over_an_ingest_that_landed_meanwhile,test_case_1_on_sync_leaves_the_regenerated_catalog_uncommitted)publish/syncnicht ins Rebase-Review-Gate und enden mit Exit 0; überlappende nicht generierte Dateien führen weiterhin hinein. (test_case_2_*)<remote>/mainzwischen proaktivem Reconcile und Push ein Ingest bekommen hat, endetpublishmit Exit 0, und der gepushte Tip erfüllt das Katalog-Kriterium unten. (test_case_2_in_the_retry_path_commits_the_regenerated_catalog_before_the_push)publishendet mit Exit 0; die ungespeicherten Seiten sind im gepushten Stand. Dasselbe vorsync: Exit 0, die ungespeicherten Seiten liegen danach unverändert im Arbeitsbaum. (test_case_3_*, auch ohne generierte Dateien: kein Rebuild)reconcilebyte-identisch zu vorher - ungespeicherte Arbeit geht nie verloren, auch nicht bei einem Fehlerausgang (getestet mit einem erzwungenen Abbruch mitten im Rebase). (test_uncommitted_work_survives_an_abort_in_the_middle_of_the_rebase, als Ausnahme und als gemeldeter Fehlschlag; dazu: Branch unbewegt, kein Rebase offen, Stash-Liste leer, Sicherungs-Ref hält die ungespeicherte Seite)kb/log.md, eingehende zuerst;kb/index.md, jedesINDEX.mdundkb/provenance.mdgleichen Byte für Byte dem, wasindex rebuild/sources rebuild-indexam selben Tag auf dem Ergebnis erzeugen. (in jedem Fall-Test geprüft, beimpublishan einem frischen Klon des gepushten Tips)reconcilewie zuvor und lässt Arbeitsbaum und Branch unverändert. (vier Tests: Log von Hand geändert, Log in einem lokalen Commit umgeschrieben, ungespeicherte Änderung an eingehend geänderter Seite, ungetrackte Datei auf eingehendem Pfad)reconcileidentisch zu zuvor; insbesondere läuft kein Rebuild (bestehendereconcile-/sync-Tests unverändert grün). (alle bestehenden Tests unverändert grün;test_a_plain_fast_forward_regenerates_nothing)raw/<…>/INDEX.mdgilt nicht als generiert: weder im Mass-Update-Zähler noch im Reconcile. (test_an_index_md_outside_kb_is_not_generated,test_an_index_md_under_raw_still_goes_to_review_when_both_sides_change_it)pytest,docs verify,instructions verifygrün; CI grün. (siehe Verifikation)Geänderte Stellen
tools/chemenu/commands/git_publish.py:is_generated,reconcileund die neuen Helfer (Abschnitt "Reconciling around generated files"),_runmitenv, Retry-Pfad inpublish_command,_reconcile_summary, Records vonsync/publishtools/chemenu/commands/index_build.py(write_index),tools/chemenu/commands/provenance_cmd.py(write_provenance_index)tools/CONTRACT.md(perdocs contract --apply),instructions/session-setup.md(sync-Absatz),instructions/gates.md(Rebase-Review: was ihn nicht auslöst),CHANGES.mdtools/chemenu/tests/test_git_publish.py: 16 neue Tests, Fixturewiki_repoGeprüft und unverändert gelassen, weil sie nichts über den Reconcile behaupten:
kb/CONTRACT.md§ Generated files,README.md,tools/README.md,docs/why-gates-are-code.md(der Rebase-Review ist dort kein benannter Gate).Verifikation
8.0.0-beta.46, Commitd4638ba: lokalpytest2322 bestanden / 3 übersprungen, auf leerer Maschine identisch;docs verify,instructions verifygrün; CI-Runs 559 und 560 grün.8.0.0-beta.47, Commitfa106ee(Abschlussphase: eine Note imsync-Record beschrieb die Änderung - "no longer" - statt des Verhaltens):docs verify,instructions verify, die betroffenen Tests grün; CI-Runs 561 und 562 grün.Priorität
War
prio/planned(Betreiber, 2026-10-06), vorherprio/waitingmit dem Auslöser "eine Instanz wird von mehr als einem Checkout oder einer parallelen Sitzung aus beschrieben".Changelog: Am Baum
40839d9nachgeprüft. Neu: Fall 3 (diverged+ ungespeicherte, nicht überlappende Änderung →git rebaseverweigert; im Scratch-Repo nachgestellt) als offene Frage;is_generated()wird aufkb/eingeengt (sonst würde eineraw/**/INDEX.mdverworfen); kein Rebuild ohne Kollision (Last Updatedinkb/index.mdwürde jedensyncverschmutzen); der Retry-Pfad inpublish_commandmuss neu erzeugte Dateien vor dem zweiten Push committen; Sicherung vor dem ersten Schreibschritt; Konfliktauflösung über Stages 1/2/3 präzisiert. Akzeptanzkriterien: Retry-Pfad, unveränderter Lauf ohne Kollision undraw/-INDEX.mdneu. Korrigiert:sources rebuild-indexliegt inprovenance_cmd.py. Neu gelabeltkind/defect→kind/decisionwegen der offenen Frage.publish/sync scheitern, wenn sich lokale und eingehende Änderungen nur in generierten Dateien überschneidento publish/sync scheitern an Überschneidungen nur in generierten Dateien und an nicht überlappender ungespeicherter ArbeitChangelog: Offene Frage 1 entschieden (Betreiber): Fall 3 wird mit abgedeckt - jetzt Entscheidung 1, eigener Entwurfspunkt mit Vorbedingung (keine Überlappung mit
touched_files(base, remote_tip), keine ungetrackte Datei auf einem eingehend angelegten Pfad), Akzeptanzkriterium ohne Vorbehalt und umsyncergänzt. Titel erweitert. Neu gelabelt:kind/decision→kind/build(keine offene Frage mehr),prio/waiting→prio/planned(Betreiber: wird jetzt gebaut).Changelog: Gebaut und publiziert als
8.0.0-beta.46(d4638ba), CI-Runs 559/560 grün; alle Akzeptanzkriterien abgehakt, jedes mit den Tests, die es belegen. Entwurf um die Umsetzung präzisiert: im mechanischen Pfad werden alle ungespeicherten generierten Dateien beiseitegelegt (gleiches Ergebnis, weil alle neu erzeugt werden); Rebuild auch, wenn beide Seiten eine generierte Datei committet haben und git sie textuell sauber gemergt hat; Fall 3 übergit rebase --autostash; Sicherung als Commit des ganzen Arbeitsbaums unterrefs/wikitool/reconcile-backup; eine verletzte Vorbedingung imdiverged-Fall meldetconflictmit der Datei statt git's Verweigerung; ein durch die Auflösung leer gewordener Commit wird von git selbst verworfen (geprüft). Neuer Abschnitt "Stand".Changelog: Endstand. Problem in die Vergangenheit gesetzt ("bis
8.0.0-beta.45"), "Entwurf" ist jetzt "Umsetzung", der Versionsteil steht als Entscheidung 2. Neu: "Bewusst nicht behandelt" (publish --pathohne die generierten Dateien im Umfang), "Geänderte Stellen" mit den geprüften und unverändert gelassenen Dokumenten, "Verifikation" mit beiden Ständen. In der Abschlussphase nachgezogen:8.0.0-beta.47(fa106ee), eine Note imsync-Record beschreibt jetzt das Verhalten statt der Änderung; CI 561/562 grün. Geschlossen.Der unter "Bewusst nicht behandelt" festgehaltene
publish --path-Fall ist nachgestellt und als #182 abgelegt.