feat(2.9.1): Die Begruendung zieht ins Deploymentcenter, die Datei wird erzeugt

Bis hierher galt eine Zweiteilung: Zustand im Deploymentcenter, Begruendung
in docs/ROADMAP.md, verbunden ueber doc_anchor. Die Beschreibung der
uebernommenen Eintraege sagte es woertlich - "Die Begruendung steht weiterhin
in der Datei - hier steht nur der Zustand."

Zwei Folgen, die erst im Betrieb sichtbar wurden:

1. Die Roadmap-Tafel kann die Begruendung nicht zeigen. Sie sieht die
   Datenbank; die Datei liegt auf einem Entwicklungsrechner. Im Seitenpanel
   stand deshalb nur doc_anchor als nicht anklickbarer Text mit dem Hinweis,
   er zeige auf etwas, das das WebUI nie zu sehen bekommt.

2. Schwerer: Die Beschreibung von dc_roadmap wies Agenten an, vor dem
   erneuten Vorschlagen eines zurueckgestellten Vorhabens "diesen Absatz" zu
   lesen. Ein Agent in polytrader oder clawddotnet kann ihn nicht lesen - er
   liegt in einem fremden Repository. Die Anweisung war nicht befolgbar.

Dazu kam, dass cli/roadmap.php --check PHP braucht und auf dem
Windows-Arbeitsplatz keines liegt: Die Haelfte der Pflegemaschinerie, die die
Zweiteilung zusammenhalten sollte, lief dort nie.

== Was sich aendert ==
bugtracker_items und roadmap_epics tragen rationale (Migration 020). Ein
EIGENES Feld neben description, nicht statt dessen: description sagt, was
gemeldet wurde, und kommt oft von einer fremden Maschine; rationale sagt,
warum wir uns so entschieden haben. Zusammengelegt haette jede redaktionelle
Ueberarbeitung den urspruenglichen Wortlaut der Meldung ueberschrieben, also
die Grundlage, auf der entschieden wurde.

Der Verlauf vermerkt nur, DASS die Begruendung sich geaendert hat und um wie
viele Zeichen. Der ganze Absatz ein zweites Mal im Verlauf haette ihn nach
der fuenften Ueberarbeitung unlesbar gemacht.

== Der Weg herauf, einmalig ==
?action=import_rationale liest eine mitgeschickte Roadmap-Datei. Drei Formen
werden erkannt: Bereichsabschnitte, Vorhabensabschnitte und Tabellenzeilen.
Die dritte ist kein Beiwerk - der Abschnitt "Verworfen" fuehrt seine zwoelf
Ablehnungen als Tabelle, und ohne sie bliebe ausgerechnet das nicht am Item
auffindbar, was am haeufigsten gebraucht wird: warum etwas schon einmal
abgelehnt wurde. Ohne overwrite=true bleibt stehen, was im DC schon steht.

== Der Weg herunter, dauerhaft ==
Ein zweites Markerpaar dc:rationale neben dc:roadmap. Bewusst ein zweites und
keine Erweiterung des ersten: Zwischen den beiden steht in jeder gewachsenen
Roadmap-Datei Handgeschriebenes - Einleitung, Leitlinie, Statuszeichen -, und
ein einziger Block haette das beim naechsten Erzeugen verschluckt.
?action=render_file setzt beide Bloecke serverseitig ein, damit die
Markersuche nur einmal umgesetzt ist. Sie hat eine Feinheit, die zweimal
umgesetzt zweimal verschieden waere: Ein im Fliesstext ERWAEHNTER Marker darf
nicht gefunden werden - die Einleitung dieser Datei erwaehnt jetzt beide.

== Werkzeuge ==
scripts/dc.py roadmap-import und roadmap-write, in Python und nicht in
cli/roadmap.php - sonst waeren sie auf dem Arbeitsplatz wieder nicht
aufrufbar. roadmap-write behaelt die Zeilenenden der Datei bei.

== Bestand ==
35 Vorhaben und 6 Bereiche aus docs/ROADMAP.md uebernommen, 476 Zeilen
handgepflegte Prosa durch den erzeugten Block ersetzt. Gegengeprueft auf
Wortebene: Aus dem Fliesstext fehlt nichts, die einzigen Abweichungen sind
die ersetzten Ueberschriften und die alte Einleitung.

Kaertchen ohne hinterlegte Begruendung tragen auf der Tafel eine dezente
Marke - erledigte nicht, dort waere sie ein Vorwurf ohne Adressaten.

MCP-Adapter auf 1.3.0: dc_roadmap_set nimmt rationale, und die Beschreibung
von dc_roadmap verweist nicht mehr auf eine Datei, die der Agent nicht
erreichen kann.

Geprueft: 7 neue Testfaelle, Suite gesamt 179 von 179.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-09-20 18:59:06 +02:00
co-authored by Claude Opus 5
parent 700be4614c
commit 8f514eb17a
12 changed files with 1416 additions and 191 deletions
+188
View File
@@ -334,3 +334,191 @@ def _(ctx):
erwarte_status(r, 404)
erwarte(isinstance(r.feld('error', 'available'), list),
"Die Absage nennt die moeglichen Aktionen nicht")
# ---------------------------------------------------------------------------
# Begruendungen
# ---------------------------------------------------------------------------
@test('tafel', 'Begruendung laesst sich setzen und kommt zurueck')
def _(ctx):
r = _melden(ctx, 'Tafel-Testfall: mit Begruendung', 'e2e-tafel-grund-%d' % time.time())
erwarte_status(r, 200, 201)
item = _item_id(r)
text = 'Warum so und nicht anders:\n\nWeil die Alternative teurer waere.'
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
body={'id': item, 'epic': 'T', 'roadmap_rank': 700, 'rationale': text})
erwarte_erfolg(r)
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % item)
erwarte_erfolg(r)
erwarte(r.feld('item', 'rationale') == text,
"Begruendung kam anders zurueck: %r" % r.feld('item', 'rationale'))
erwarte(r.feld('item', 'rationale_updated_at') is not None,
"rationale_updated_at wurde nicht gesetzt")
ctx['tafel_grund_item'] = item
@test('tafel', 'Der Verlauf traegt nicht den ganzen Begruendungstext')
def _(ctx):
"""
Eine Begruendung kann einige tausend Zeichen lang sein. Stuende sie bei
jeder Ueberarbeitung noch einmal im Verlauf, waere der Verlauf nach der
fuenften unlesbar - und damit als Werkzeug wertlos.
"""
item = ctx.get('tafel_grund_item')
if not item:
ueberspringen('tafel_grund_item')
langer_text = 'Sehr ausfuehrliche Begruendung. ' * 40
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
body={'id': item, 'rationale': langer_text})
erwarte_erfolg(r)
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % item)
verlauf = ' '.join(k.get('comment', '') for k in r.feld('item', 'comments', default=[]))
erwarte('Sehr ausfuehrliche Begruendung. Sehr ausfuehrliche' not in verlauf,
"Der ganze Text steht im Verlauf")
erwarte('Begruendung ueberarbeitet' in verlauf,
"Die Aenderung wurde gar nicht vermerkt")
@test('tafel', 'Die Tafel meldet, ob eine Begruendung hinterlegt ist')
def _(ctx):
item = ctx.get('tafel_grund_item')
if not item:
ueberspringen('tafel_grund_item')
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
erwarte_erfolg(r)
karten = {k['id']: k for k in r.feld('items', default=[])}
erwarte(item in karten, "Item #%d fehlt auf der Tafel" % item)
erwarte(karten[item].get('has_rationale') is True,
"has_rationale ist %r" % karten[item].get('has_rationale'))
ohne = [k for k in karten.values() if k.get('has_rationale') is False]
erwarte(ohne, "Kein einziges Kaertchen ohne Begruendung - der Fall wird nicht geprueft")
@test('tafel', 'Einlesen erkennt Absaetze UND Tabellenzeilen')
def _(ctx):
"""
Der Abschnitt "Verworfen" fuehrt seine Ablehnungen als Tabelle, nicht als
Absaetze. Bliebe die Tabelle beim Bereich stecken, waere ausgerechnet das
nicht am Item auffindbar, was am haeufigsten gebraucht wird: warum etwas
schon einmal abgelehnt wurde.
"""
stempel = int(time.time())
ids = []
for i in range(2):
r = _melden(ctx, 'Tafel-Testfall: Einlesen %d' % i, 'e2e-tafel-imp-%d-%d' % (stempel, i))
erwarte_status(r, 200, 201)
item = _item_id(r)
ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': item, 'epic': 'T'})
ids.append(item)
datei = "\n".join([
"# Fahrplan", "",
"## T - Tafel-Testbereich", "",
"Dieser Bereich traegt seine eigene Begruendung.", "",
"### [T] #%d - Erster" % ids[0], "",
"Der Absatz zum ersten Vorhaben.", "",
"## Verworfen", "",
"| | Vorschlag | Warum nicht |",
"|---|---|---|",
"| #%d | **Zweiter** | Der Aufwand stand in keinem Verhaeltnis. |" % ids[1],
"",
])
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=import_rationale',
body={'project_slug': PROJEKT, 'content': datei})
erwarte_erfolg(r)
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[0])
erwarte(r.feld('item', 'rationale') == 'Der Absatz zum ersten Vorhaben.',
"Absatz nicht uebernommen: %r" % r.feld('item', 'rationale'))
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[1])
erwarte(r.feld('item', 'rationale') == 'Der Aufwand stand in keinem Verhaeltnis.',
"Tabellenzeile nicht uebernommen: %r" % r.feld('item', 'rationale'))
ctx['tafel_imp_ids'] = ids
@test('tafel', 'Ein zweiter Lauf ueberschreibt nichts ohne Ansage')
def _(ctx):
"""
Wer eine Begruendung im Deploymentcenter ueberarbeitet hat, soll sie nicht
beim naechsten Einlesen stillschweigend auf den Stand der Datei
zurueckgesetzt bekommen.
"""
ids = ctx.get('tafel_imp_ids')
if not ids:
ueberspringen('tafel_imp_ids')
ueberarbeitet = 'Im Deploymentcenter ueberarbeitet.'
ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
body={'id': ids[0], 'rationale': ueberarbeitet})
datei = "\n".join([
"## T - Tafel-Testbereich", "",
"### [T] #%d - Erster" % ids[0], "",
"Der alte Absatz aus der Datei.", "",
])
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=import_rationale',
body={'project_slug': PROJEKT, 'content': datei})
erwarte_erfolg(r)
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[0])
erwarte(r.feld('item', 'rationale') == ueberarbeitet,
"Der Stand der Datei hat gewonnen: %r" % r.feld('item', 'rationale'))
# Mit Ansage darf er gewinnen.
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=import_rationale',
body={'project_slug': PROJEKT, 'content': datei, 'overwrite': True})
erwarte_erfolg(r)
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[0])
erwarte(r.feld('item', 'rationale') == 'Der alte Absatz aus der Datei.',
"overwrite=true hat nicht gewirkt: %r" % r.feld('item', 'rationale'))
@test('tafel', 'Die Datei wird erzeugt, Handgeschriebenes bleibt stehen')
def _(ctx):
"""
Zwei erzeugte Bloecke, und alles ausserhalb bleibt unberuehrt. Genau das
ist die Zusage, auf die sich jeder verlaesst, der in dieser Datei noch
etwas von Hand schreibt.
"""
datei = "\n".join([
"# Fahrplan", "",
"Diese Einleitung ist von Hand und soll das ueberleben.", "",
"<!-- dc:rationale stand=alt items=0 projekt=x -->",
"Alter erzeugter Inhalt, der ersetzt gehoert.",
"<!-- /dc:rationale -->", "",
"## Pflege dieser Datei", "",
"Auch dieser Abschnitt ist von Hand.", "",
])
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=render_file',
body={'project_slug': PROJEKT, 'content': datei})
erwarte_erfolg(r)
neu = r.feld('markdown', default='')
erwarte('Diese Einleitung ist von Hand' in neu, "Die Einleitung wurde ueberschrieben")
erwarte('Auch dieser Abschnitt ist von Hand' in neu, "Der Fussabschnitt wurde ueberschrieben")
erwarte('Alter erzeugter Inhalt' not in neu, "Der alte Block steht noch da")
erwarte(neu.count('<!-- dc:rationale') == 1, "Der Begruendungsblock steht mehrfach")
erwarte(r.feld('changed') is True, "changed meldet keine Aenderung")
@test('tafel', 'render verlangt einen bekannten Teil')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=render&project=%s&part=quatsch' % PROJEKT)
erwarte_fehler(r, http=400)