Sitzung, die write-once-Frontmatterfelder reparierbar macht: touch bekommt --set/--add/--remove ueber eine Denylist statt einer Allowlist, ein idempotentes --remove und einen bewusst engen Scope (Stack 1.4.0, Gitea-Issue #14)
Autor: Claude Code (claude-opus-5) Datum: 2026-08-31 Raw-Dateien: raw/notes/Conversation Transcript - Write-Once Frontmatter Fields and touch --set Session 2026-08-31.md Typ: Notes
Zusammenfassung
Das Transkript ist eine zusammenfassende Rekonstruktion der Sitzung, kein wörtliches Protokoll.
Die zitierten Befehlsausgaben sind echt, Torbens drei Entwurfsentscheidungen sind samt der ihm
vorgelegten Optionen festgehalten, die Begründungen des Assistenten sind verdichtet. Es ist
eines von zwei Transkripten dieses Sitzungsabschnitts; das zweite behandelt die Zählregel des
Mass-Update Gate und die gemessene Kalibrierung des Iteration Budget und wird getrennt
eingelesen.
Gegenstand ist Gitea-Issue #14, ausgeliefert als touch --set/--add/--remove in Stack-Version
1.4.0 (Commit dbe2f73, 9 Dateien, 674 Tests grün). Der Defekt, den es schließt: ein Feld,
das new einmal geschrieben hat, war danach nicht mehr erreichbar. touch kannte nur die
Felder modified, summary, provenance und confidence_base; Handeditierung ist genau das,
was der Stack verhindern soll; Löschen und Neuanlegen zerreißt jede bestehende Referenz auf die
Seite; und new ist nicht idempotent, das Zeitfenster für den richtigen Wert war also genau
einen Befehl breit.
Den bleibenden Wert der Sitzung tragen drei Entscheidungen, die Torben mit ausgewiesenen
Trade-offs vorgelegt wurden: Denylist statt Allowlist für die schreibbaren Felder, Ersetzen
plus --add/--remove für Listenfelder, und ein bewusst enger Auslieferungsschnitt, der
raw rename als Issue #16 abspaltet.
Kernaussagen
Die Vorarbeit lag schon im Code.validate_fields() in touch.py validiert pro Feld
statt pro Dokument - genau die Form, die ein --set braucht. Der Grund steht im Docstring:
eine Validierung über das ganze Dokument würde sich weigern, modified: auf einer Seite zu
bumpen, die aus einem unbeteiligten Grund ungültig ist, also auf der Seite, die Wartung am
dringendsten braucht.
Entscheidung 1 - Denylist statt Allowlist. Schreibbar ist alles, was das Schema für den
Seitentyp deklariert, abzüglich einer kurzen begründeten Sperrliste. Das Argument, das den
Ausschlag gab: eine gepflegte Allowlist ist eine zweite Kopie des Schemas, und die Kopie ist
die Seite, die driftet - Invariante 8 aus AGENTS.md, angewandt auf eine Konstante. Der
Preis der Allowlist wäre gewesen, dass jedes neue Schema-Feld eine Codeänderung braucht.
Gesperrt sind vier Gruppen, jede mit dem Befehl benannt, dem das Feld gehört:type:
ändert Schema und Verzeichnis der Seite und gehört nach page-lifecycle.md; confidence:
ist aus confidence_base abgeleitet und nicht autorisiert; related:, sources:,
entities: und concepts: gehören xref, weil ein blanker Frontmatter-Schreibvorgang die
Gegenrichtung und die Body-Bullets stehen ließe.
Entscheidung 2 - Ersetzen plus --add/--remove. Reines Ersetzen wäre eine Regel
gewesen, hätte aber verlangt, für ein einzelnes Tag die ganze Liste zu nennen. Der Preis der
gewählten Variante sind drei Optionen statt einer.
Die Teilfrage, was --remove mit einem nicht vorhandenen Element tut, entschied der
Assistent selbst und wies das aus: es gelingt und wird gemeldet. Idempotent wie
xref remove, weil ein Reparaturbefehl, der sich beim zweiten Lauf verweigert, nicht
skriptbar ist - aber nie stillschweigend, weil ein stiller No-op genauso aussieht wie eine
gelungene Entfernung, und genau so verbirgt sich ein vertippter Elementname.
Entscheidung 3 - enger Schnitt. Nur touch --set; die Dateiverschiebung bleibt zweistufig
(git mv, dann touch --set raw_files=…). Die Alternative, raw rename mitzuliefern, hätte
Issue #14 vollständig geschlossen und den Zwischenzustand vermieden, zum Preis von size/M:
Rückwärtssuche über alle raw_files:-Referenzen, Verhalten bei mehreren Besitzern,
Contract-Zeilen für zwei Befehle. raw rename wurde Issue #16 (prio/2, size/S).
Zwei Kommandos, eine Implementierung._coerce_set_value, _parse_set_fields und
_check_raw_files_exist wanderten aus new_page.py nach commands/_util.py und verloren
ihren führenden Unterstrich. Ohne das hätte touch --set den Komma-Defekt aus Issue #12 am
ersten Tag geerbt; ein Test deckt genau diesen Fall ab, mit einer Rohdatei, deren Name ein
Komma enthält, referenziert über \, und als ein Pfad zurückgelesen.
Der Existenzcheck für raw_files: gilt auch für touch, identisch zu dem, den new
ausführt. Er ist I/O und keine Datenform, also kann kein Schema ihn ausdrücken.
Die beiden Ablehnungen sind bewusst unterschiedlich formuliert. Ein gesperrtes Feld ist
ein Routing-Problem, die Meldung nennt deshalb den zuständigen Befehl. Ein unbekanntes Feld
ist ein Tippfehler oder der falsche Seitentyp, die Meldung listet deshalb auf, welche Felder
die Seite tatsächlich hat - der Nutzwert liegt darin, zu erfahren, dass tags gemeint war.
Eine Falle im Testaufbau, einmal beseitigt.test_touch.py rief den Typer-Callback direkt
mit vollständiger Argumentliste auf; drei neue Optionen brachen sieben Aufrufstellen mit
TypeError: 'OptionInfo' object is not iterable, weil ein direkt aufgerufener Callback für
jedes ausgelassene Argument ein OptionInfo-Objekt bekommt. Die Tests laufen jetzt über einen
_touch(**overrides)-Helper, der jede Option belegt; die nächste Option kostet eine Zeile
statt sieben.
Der Beleg am realen Korpus:Diff-Reviewable Agent Edits war Stunden zuvor von einem
Ingest angelegt worden, dessen --set tags=-Wert ein Komma am Ende trug, worauf alles hinter
dem Trennzeichen verlorenging. Der Subagent hatte alle drei Auswege korrekt geprüft und
verworfen - touch konnte tags: nicht setzen, Handeditierung kommt Invariante 1 zu nahe,
rm plus new hätte die concepts:-Referenz der Source-Seite zerrissen. Die Seite behielt
[agent-workflow] dauerhaft, wegen eines Kommas. Mit touch --add trägt sie jetzt
[agent-workflow, context-engineering, tooling].
Der zweite Aufruf desselben --add meldete "already up to date; nothing to change", der
--remove eines nicht vorhandenen Elements meldete "not present, nothing removed" und endete
ebenfalls erfolgreich - die zugesagte Idempotenz, an der Kommandozeile gezeigt.
Dass dies eine Rate und kein Einzelfall war: drei Fehlschläge in drei aufeinanderfolgenden
Ingests desselben Tages, an zwei verschiedenen Feldern, von drei verschiedenen Agenten. Einer
davon war ein nachgestelltes Komma.
Ergebnis:1.4.0 als MINOR (neue Fähigkeit, rückwärtskompatibel), Commit dbe2f73 über
9 Dateien, 674 Tests grün in der normalen und in der gehärteten Umgebung, Issue #14
geschlossen mit den drei Entscheidungen im Protokoll, Issue #16 eröffnet.
Aufgaben
Gitea-Issue #16 - raw rename, das git mv und jede referenzierende Source-Seite in
einem Schritt erledigt (prio/2, size/S)
Nicht übernommen
Die Modul- und Funktionsnamen der Testumbauten über den _touch-Helper hinaus. Der
Umbau selbst ist eine dauerhafte Aussage über die Testschnittstelle, die einzelnen sieben
Aufrufstellen sind es nicht.
Der vollständige Wortlaut der beiden Fehlermeldungen. Übernommen ist die Entwurfsregel
dahinter - Routing-Problem nennt den Befehl, Tippfehler nennt die vorhandenen Felder -, weil
der Wortlaut sich ändern kann und die Regel nicht.
Das Schwestertranskript desselben Sitzungsabschnitts (Zählregel des Mass-Update Gate,
gemessene Kalibrierung des Iteration Budget). Es wird getrennt eingelesen und bekommt eine
eigene Source-Seite; hier stünde es unbelegt.
Die Einzelheiten der 674 Tests. Übernommen sind die Gesamtzahl und die Testfalle, die
sich daran zeigte.