-
v4.5.0 Stable
released this
2026-09-04 10:51:12 +00:00 | 65 commits to main since this release4.5.0 - 2026-09-04 - Beide Update-Wege in Code: upstream merge fuer Clones, dist upgrade fuer Tarball-Instanzen
Author: Torben Nehmer
- Faktenkorrekturen in der 4.4.0-Prosa; DEVELOPMENT.md ohne zweite Kommandobeschreibung
- wikitool upstream merge/verify: code procedure for taking a stack update, ownership.py as the shared stack/instance boundary
- upstream merge: combined-commit regression test (edit+add+delete+contract+template+contract-delete in one commit)
- upstream merge: keep gitignored local data under a content stage, refuse a merge git never opened, report what actually changed
- dist upgrade: apply a stack update, not just detect one (#7)
- dist upgrade: Stamp-Semantik nach --keep-local benannt, docs/ownership-and-templates.md auf drei Eigentumsklassen nachgezogen
Die Prosa zu 4.4.0 - Changelog-Eintrag,
docs/version-model.md,instructions/dev/version-parts.md,
DEVELOPMENT.md- entstand ungeprüft: kein maschineller Check liest sie, und die Sitzung, die sie
schrieb, hat den dafür vorgesehenen Modellwechsel ausstack-devSchritt 6 übersprungen. Ein
nachgeholter Durchgang hat drei Fehler gefunden, von denen der erste der teuerste war.Der Befund war an einer Stelle schlicht falsch. Changelog und
docs/version-model.md
behaupteten, unter dem alten Modell seien Nummern vergeben worden, die „nie ausgeliefert" wurden -
im Changelog sogar mit einer erfundenen Zahl („fünf Minor-Bumps ... vier nie ausgeliefert"). Das
Gegenteil stimmt: weilrelease.ymlauf jedeVERSION-Bewegung feuerte, wurde jeder Bump zu
einem echten, getaggten Release. Der 2026-09-03 hat vier davon in sechs Stunden erzeugt (4.3.0
bis4.3.3), zwei für reine Prosa-Änderungen. Der Schaden war nie ein Phantom-Release, sondern
dass „Release" aufhörte, etwas zu bedeuten - vier Upgrades an einem Nachmittag sind für einen
Konsumenten kein kleineres Versprechen, sondern ein unleserliches. Beide Stellen tragen jetzt den
tatsächlichen Vorgang samt Beleg.Dazu zwei kleinere Korrekturen:
version-parts.mdnannte den Stack weiterhin2.x(er steht bei
4.x), undDEVELOPMENT.mdtrug eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
beschrieb, was er prüft - eine Kopie dessen, wastools/CONTRACT.mdhält unddocs verifydort
gegen die CLI prüft. Die Tabelle ist raus; dass die Datei selbst außerhalb der von diesem Check
abgedeckten Dokumente liegt, steht jetzt an ihrer Stelle. Der Code-seitige Teil davon - ob
DEVELOPMENT.mdindocs_verify.STAGE_READMESgehört - hängt an #47, zusammen mit der Lücke im
Skill, die den übersprungenen Modellwechsel überhaupt erst unauffällig gemacht hat.Kein Verhalten geändert, nur Prosa - und bewusst als laufender Kandidat gelassen statt fixiert:
CIs Version-Gate verlangt dieVERSION-Bewegung, ein Release verlangt sie nicht. Genau dafür gibt
es das Modell aus 4.4.0.Zweiter Bump auf demselben Kandidaten (#30):
git merge upstream/mainbehandelt einen
bewegten Korpus asymmetrisch - eine gelöschte, upstream-geänderte Seite meldet sich als Konflikt,
eine neu angelegte Seite wird still gestaged, nur eine beidseitig gelöschte Seite ist harmlos. Die
Prosa-Prozedur inprivate-instance.md§ "Taking a stack update" hat das geschlossen, aber mit
vier eigenen Fehlern: der Pfadsatz stand dreifach (im Skript, im Kontroll-Grep, implizit in
dist_cmd.py); eine vom Upstream gelöschte Maschinerie-Datei wurde vongit checkout MERGE_HEAD -- <pfad>still ignoriert, weil das Skript keinset -ehatte; ein echter Konflikt in
tools//types//instructions/endete in einem von der Prosa nirgends erwähnten offenen Merge;
und ein neuer Maschinerie-Pfad unter einer Content-Stage hätte die Literal-Liste nie erreicht.Die Eigentumsgrenze ist jetzt ein Prädikat statt einer Liste:
chemenu/ownership.py,
is_stack_owned(relative), wahr für<stage>/CONTRACT.mddirekt unter einer Content-Stage
(kb,raw,work,reports) und für jeden Pfad, der dort auf.templateendet - nicht
rekursiv,kb/<collection>/COLLECTION.mdbleibt seit #39 instanzeigen.dist_cmd.pyliest das
Modul jetzt statt einer eigenen_CONTENT_ALLOWED_NAMES-Liste zu pflegen, und
CONTRACT_ONLY_STAGESleitet sich ausownership.CONTENT_STAGESab statt die drei Stage-Pfade
ein zweites Mal aufzuschreiben - ein Test hält fest, dass beide Sichten nicht auseinanderlaufen
können.Neu:
wikitool upstream merge [--remote upstream] [--branch main] [--no-fetch]undwikitool upstream verify --since <rev> [--until HEAD].mergeprüft Vorbedingungen (sauberer Baum, kein
laufendes Merge, Remote löst auf), warnt statt zu blockieren, wenn.wikitool-remotes.jsonfehlt,
hält den Merge offen (--no-commit --no-ff), zwingt jede Content-Stage auf die lokale Seite
zurück, holt dann über die Vereinigungsmenge der Bäume vonMERGE_HEADundHEADgenau die
stack-eigenen Pfade zurück - inklusive einer Löschung, falls der Upstream einen Maschinerie-Pfad
entfernt hat -, verweigert bei verbliebenen unaufgelösten Pfaden ohne zu committen, committet
sonst und verifiziert den entstandenen Commit mit derselben Logik wieverify- ein Fund dort
wird laut gemeldet und nicht automatisch zurückgerollt. Nicht idempotent (AGENTS.md § Tool
error contract), nicht budget-exempt;verifyliest nur und ist wiemigrate verifyvon der
Budget-Gate ausgenommen. Die Mass-Update-Gate greift bei einem Merge-Commit strukturell nicht -
das steht jetzt als eigener Absatz ininstructions/gates.md, mitupstream merges eigener
Nachkontrolle als der Sicherung, die hier tatsächlich trägt.private-instance.md§ "Taking a stack update" verweist jetzt auf den Befehl statt das Skript
auszuschreiben; die Pfadtabelle bleibt als Erklärung stehen, ist aber nicht mehr die operative
Liste. Vorschlag B (eigenes Demo-Repo) bleibt zurückgestellt, siehe #30.23 neue Tests unter
test_upstream_cmd.py(35 Fälle mit der parametrisierten
is_stack_owned-Tabelle) decken die Fälle aus der Spezifikation ab:
gelöschte vs. geänderte vs. neu angelegte Seiten,kb/CONTRACT.md- und Template-Änderungen,
kb/entities/COLLECTION.mdbleibt lokal, eine gelöschteraw/CONTRACT.mdlandet, ein neuer
Template-Pfad landet, ein offenerwork/-Lauf landet nicht, ein echtertools/-Konflikt lässt
den Merge offen, ein schmutziger Baum wird unberührt abgewiesen, "bereits aktuell" ist ein No-op,
die Publish-Remote-Gate-Warnung,verifygegen einen von Hand verpfuschten Merge, und die
dist_cmd/ownership-Konsistenz.Dritter Bump auf demselben Kandidaten: die im Akzeptanzkriterium geforderte Kombinationsprobe
fehlte noch - ein einzelner Upstream-Commit, der Editieren, Anlegen, Löschen einer Seite, eine
Contract-Änderung, eine Template-Änderung und eine Contract-Löschung gleichzeitig bewegt. Jetzt
alstest_one_upstream_commit_mixing_every_case_at_oncenachgetragen; alle sechs Erwartungen in
einemupstream merge-Aufruf verifiziert.Vierter Bump: zwei Fehler, die ein Review-Durchgang nach dem Publish gefunden hat. Beide
waren in der ersten Fassung enthalten, beide hätten Daten vernichtet, und keiner der bestehenden
Tests hat sie berührt.Erstens: die Content-Stage wurde als Ganzes gelöscht.
_restore_stage_to_localhieß in der
ersten Fassungshutil.rmtree(stage_dir)— die wörtliche Übersetzung desrm -rf kb rawaus der
Prosa-Prozedur. Fürkb/undraw/ist das harmlos, weil dort nichts Ignoriertes liegt. Für die
beiden Stages, die dieses Issue neu in den Satz aufgenommen hat, ist es das nicht:reports/
ist bis auf seinen Contract komplett gitignored und trägt genau die Daten, die nirgends sonst
existieren — die Telemetrie-Traces, aus deneneval scoreliest, gespeicherte Eval-Berichte,
alte Lint-Reports. In dieser Instanz standen zum Zeitpunkt des Fundes 497 Trace-Verzeichnisse
unterreports/telemetry/; ein einzigerupstream mergehätte sie alle gelöscht, und zwar
stillschweigend, weil git von ignorierten Dateien nichts meldet. Die Stage wird jetzt über die
getrackten Pfade beider Bäume zurückgesetzt statt über das Verzeichnis; ignorierte lokale
Daten bleiben unberührt. Leergewordene Verzeichnisse werden aufgeräumt, aber nur wirklich leere.Zweitens: ein Merge, den git nie eröffnet hat, hätte die Maschinerie gelöscht. Der Exit-Code
vongit merge --no-commit --no-ffwird bewusst ignoriert (Konflikte unter den Content-Stages
sind erwartet). Nur: wenn git das Merge gar nicht erst eröffnet — unverwandte Historien, eine
ignorierte Datei im Weg —, gibt es keinMERGE_HEAD,_tree_paths("MERGE_HEAD")liefert die
leere Menge, und jeder stack-eigene Pfad inHEADfällt damit in den Zweig „der Upstream hat
ihn gelöscht":kb/CONTRACT.md,raw/CONTRACT.mdund sämtliche Templates werden entfernt. Der
Kommando-Ablauf prüft jetzt nach dem Merge-Aufruf, dass tatsächlich ein Merge offen ist, und
bricht sonst ab, ohne den Baum angefasst zu haben. Beide Fehler haben je einen Regressionstest,
und beide Tests wurden gegen die alte Fassung laufen gelassen, um zu zeigen, dass sie sie
tatsächlich fangen.Dazu eine Ehrlichkeitskorrektur an der Erfolgsmeldung: sie zählte die wiederhergestellten
Pfade, nicht die geänderten — ein Merge, der eine Datei bewegt, meldete vier oder fünf. Sie fragt
jetztgit diffzwischen Vor- und Nach-Commit, kennzeichnet Löschungen, und stimmt damit mit dem
überein, was ein Leser nachprüfen würde.docs/ownership-and-templates.mdhat einen Abschnitt
bekommen, warum die Grenze ein Prädikat und keine Liste ist — die Begründung, die dieses Issue
erarbeitet hat, gehörte in die Hintergrunddoku und nicht nur in einen Changelog-Eintrag.Fünfter Bump:
wikitool dist upgrade(#7), der zweite der beiden Update-Wege.upstream mergeoben bedient eine Instanz mit gemeinsamer Git-History;dist upgradebedient eine
Instanz aus einem Tarball, ohne History, die bislang eine rein manuelle Prozedur in
INSTALL.mddurchlaufen musste - Schritt 4 verlangte einen sha256-Vergleich von Hand gegen den
files-Block der alten.wikitool-release.json.Die tragende Regel: die Schreibmenge ist genau der
files-Block der neuen
.wikitool-release.json, minus was ein Export aus einer leeren Vorlage neu sät
(chemenu.ownership.is_export_stub, wie bisher schon fürkb/log.md/.gitkeep) oder einmalig
sät und danach der Instanz gehört (chemenu.ownership.is_upgrade_preserved, neu für
.wikitool-kb.jsonundCHANGES.md), plus der Stamp selbst. Jeder Kandidatpfad wird gegen die
alte Instanz-Summe klassifiziert: unverändert wird geräuschlos überschrieben, neu im Release
wird angelegt, lokal verändert oder gelöscht wird nie still überschrieben - der Lauf bricht
mit der vollständigen Liste ab, außer--keep-localsagt ausdrücklich, dass die Dateien liegen
bleiben sollen.--pruneentfernt zusätzlich aus dem Release entfallene Dateien, aber nur
solche, die seit der Installation unverändert sind.Die Migrationskette nach dem Tausch wird aus den
instructions/migrations/des neuen Baums
ermittelt (kb_state.load_migrationsbekam dafür einendirectory-Parameter) und nur
gemeldet, nie ausgeführt - es gibt bewusst keinmigrate run. Eine bereits gegen die
installierte Maschinerie offene Kette lässt den Befehl abbrechen, bevor er die Quelle
überhaupt öffnet.kb_state.divergent_files()(bisher nur vonmigrate statusgelesen) ist
jetzt eine dünne Hülle um das neue, zwei-Baum-fähigecompare_against_stamp()- gleiches
Verhalten für den bestehenden Aufrufer, wiederverwendbar fürdist upgrades eigenen Vergleich.Quelle ist immer ein bereits vorhandenes Verzeichnis oder
.tar.gz- kein Download, das bleibt
alleinversion checks Sache. Ein Tarball muss genau ein Top-Level-Verzeichnis enthalten (die
Form, in derrelease.ymles baut) und wird gegen eine.sha256-Beidatei geprüft, falls eine
danebenliegt (fehlt sie: WARN, kein Abbruch). Weitere Abbruchgründe vor jedem Schreiben: fehlende
lokaleVERSION/.wikitool-kb.json/Stamp mitfiles-Block, ein schmutziger Arbeitsbaum (kein
Git-Repo ist ein WARN, keine Sperre), eine Vorab-Version (-beta.N) ohne--pre, sowie ein
Downgrade; Gleichstand ist ein No-op. Ein Grenzübertritt der Kompatibilität wird laut gemeldet,
blockiert aber nicht. Committet und pusht nichts (Invariante 5).Gegenüber dem ersten Entwurf des Issues zwei Korrekturen, die dort auch nachgetragen sind: der
files-Block wurde entgegen der ursprünglichen Annahme bereits vor diesem Bump gelesen
(divergent_files/migrate status), und die Migrationskette war ursprünglich falsch begründet- sie kann nur aus dem neuen Baum kommen, nicht durch eine andere Abfragereihenfolge aus der
alten Instanz. 24 neue Tests untertest_dist_upgrade.pydecken die Klassifikation, alle
Abbruchgründe,--keep-local,--pruneund beide Quellformen (Verzeichnis und Tarball,
inklusive der sha256- und Top-Level-Prüfung) ab.
Bewusst nicht angetastet:
instructions/private-instance.md(der Clone-Weg ändert sich nicht,
INSTALL.mdbenennt jetzt beide Wege nebeneinander) und die Frage, wiedist upgrademit
Collection-Templates umgeht, deren Namen eine fremde Instanz gar nicht hat - es verhält sich wie
upstream mergeund schreibt sie, was ein eigenes Issue gegen den Export wäre, keins gegen das
Upgrade.Sechster Bump: die ungeprüfte Phase nachgeholt. Der Abschluss des vorigen Bumps lief auf
Sonnet, undstack-devSchritt 6 verlangt dort genau zwei Dinge, die kein Check erzwingt:
Issue-Body unddocs/-Veralterung. Der Body war gemacht, diedocs/-Prüfung nicht - sie wurde
benannt statt durchgeführt. Nachgeholt auf Opus, mit einem Fund.docs/ownership-and-templates.md§ „The consequence in practice" beschrieb ein Upgrade als
Zweiteilung: verbatim überschreiben,.template-gestützte Dateien liegen lassen - und begründete
den ersten Teil damit, dass verbatim ausgelieferte Dateien „safe to replace wholesale" seien,
weil sie „never instance-specific to begin with" waren. Genau diese Annahme trifftdist upgrade
nicht: eine Instanz kann eine verbatim ausgelieferte Datei angefasst haben, und die sha256 je
Datei existiert, um das zu erkennen, statt es vorauszusetzen. Dazu fehlte die dritte Klasse ganz -
die einmalig gesäten, danach instanzeigenen Pfade (.wikitool-kb.json,CHANGES.md,
kb/log.md,raw/*/.gitkeep), die im Stamp stehen wie jede andere Datei und deshalb aktiv
ausgeschlossen werden müssen. Die Seite nennt jetzt drei Klassen und die engere praktische Regel:
überschreibe die verbatim ausgelieferten Dateien, die diese Instanz nicht angefasst hat.Dazu eine Präzisierung in
tools/CONTRACT.md, die vorher nirgends stand: nach--keep-local
wird der neue Stamp trotzdem vollständig geschrieben, trägt also die Release-Summe auch für
Dateien, die bewusst nicht geschrieben wurden. Der Stamp ist die Vergleichsbasis für den
nächsten Lauf, kein wörtliches Inventar der Platte - und genau das hält eine übersprungene Datei
bei jedem weiteren Lauf als abweichend gemeldet, statt sie nach einmaligem Überspringen still als
aktuell zu führen.Downloads