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:
co-authored by
Claude Opus 5
parent
700be4614c
commit
8f514eb17a
@@ -35,8 +35,8 @@ use Deploymentcenter\Modules\Bugtracker\RoadmapBoard;
|
||||
|
||||
Http::beginJson(['GET', 'POST', 'OPTIONS'], true);
|
||||
|
||||
const READ_ACTIONS = ['list', 'epics', 'next', 'active', 'render', 'check', 'board'];
|
||||
const WRITE_ACTIONS = ['set', 'move', 'reorder', 'epic', 'rank_between'];
|
||||
const READ_ACTIONS = ['list', 'epics', 'next', 'active', 'render', 'render_file', 'check', 'board'];
|
||||
const WRITE_ACTIONS = ['set', 'move', 'reorder', 'epic', 'rank_between', 'import_rationale'];
|
||||
|
||||
$action = strtolower(Http::str('action') ?? 'list');
|
||||
|
||||
@@ -115,9 +115,47 @@ switch ($action) {
|
||||
|
||||
case 'render': {
|
||||
$project = requireProject($scope, $context);
|
||||
|
||||
// Zwei erzeugte Bloecke: der Zustand (Vorgabe) und die
|
||||
// Begruendungen. Getrennt abrufbar, weil sie an verschiedenen
|
||||
// Stellen der Datei stehen und je ein eigenes Markerpaar haben.
|
||||
$part = strtolower(Http::str('part') ?? 'state');
|
||||
|
||||
if (!in_array($part, ['state', 'rationale'], true)) {
|
||||
Http::fail(400, 'unknown_part', 'Erlaubt sind "state" und "rationale".');
|
||||
}
|
||||
|
||||
Http::ok([
|
||||
'project_slug' => $project,
|
||||
'markdown' => $roadmap->renderBlock($project),
|
||||
'part' => $part,
|
||||
'markdown' => $part === 'rationale'
|
||||
? $roadmap->renderRationaleBlock($project)
|
||||
: $roadmap->renderBlock($project),
|
||||
]);
|
||||
}
|
||||
|
||||
case 'render_file': {
|
||||
// Die ganze Datei, beide erzeugten Bloecke eingesetzt.
|
||||
//
|
||||
// Das Einsetzen laeuft hier und nicht im Aufrufer: Die Markersuche
|
||||
// hat Feinheiten - ein im Fliesstext ERWAEHNTER Marker darf nicht
|
||||
// gefunden werden -, und die zweimal umzusetzen hiesse, sie beim
|
||||
// zweiten Mal anders umzusetzen. Der Aufrufer schickt den
|
||||
// Dateiinhalt und schreibt zurueck, was er bekommt.
|
||||
$project = requireProject($scope, $context);
|
||||
$content = Http::str('content');
|
||||
|
||||
if ($content === null) {
|
||||
Http::fail(400, 'missing_content', 'Das Feld "content" mit dem Dateiinhalt fehlt.');
|
||||
}
|
||||
|
||||
$neu = Roadmap::spliceBlock($content, $roadmap->renderBlock($project));
|
||||
$neu = Roadmap::spliceRationaleBlock($neu, $roadmap->renderRationaleBlock($project));
|
||||
|
||||
Http::ok([
|
||||
'project_slug' => $project,
|
||||
'markdown' => $neu,
|
||||
'changed' => $neu !== $content,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -196,7 +234,7 @@ switch ($action) {
|
||||
// weiterhin ueber /manage - ein zweiter Schreibweg auf dieselben
|
||||
// Spalten waere eine zweite Stelle, an der Regeln gelten muessten.
|
||||
$updates = [];
|
||||
foreach (['epic', 'target_build', 'defer_trigger', 'doc_anchor'] as $field) {
|
||||
foreach (['epic', 'target_build', 'defer_trigger', 'doc_anchor', 'rationale'] as $field) {
|
||||
if (Http::input($field) !== null) {
|
||||
$updates[$field] = Http::str($field);
|
||||
}
|
||||
@@ -314,6 +352,40 @@ switch ($action) {
|
||||
]);
|
||||
}
|
||||
|
||||
case 'import_rationale': {
|
||||
// Einmaliger Weg herauf: Die Begruendungen, die bisher nur in der
|
||||
// Projektdatei standen, werden eingelesen. Der Dateiinhalt kommt
|
||||
// mitgeschickt - genau wie bei action=check und aus demselben Grund:
|
||||
// Die Datei liegt im Repository des Projekts, oft auf einer anderen
|
||||
// Maschine, und der Server soll nicht anfangen, in Verzeichnissen zu
|
||||
// suchen.
|
||||
$project = requireProject($writeProject ?? $angefragt, $context);
|
||||
$content = Http::str('content');
|
||||
|
||||
if ($content === null) {
|
||||
Http::fail(400, 'missing_content', 'Das Feld "content" mit dem Dateiinhalt fehlt.');
|
||||
}
|
||||
|
||||
// Ohne overwrite bleibt stehen, was schon hinterlegt ist. Ein zweiter
|
||||
// Lauf soll eine im Deploymentcenter ueberarbeitete Begruendung nicht
|
||||
// stillschweigend auf den Stand der Datei zuruecksetzen.
|
||||
$overwrite = (bool)Http::input('overwrite', false);
|
||||
|
||||
$bericht = $roadmap->importRationale($project, $content, $overwrite);
|
||||
|
||||
Http::ok([
|
||||
'project_slug' => $project,
|
||||
'overwrite' => $overwrite,
|
||||
'report' => $bericht,
|
||||
'message' => sprintf(
|
||||
'%d Abschnitt(e) gelesen, %d Vorhaben und %d Bereich(e) uebernommen.',
|
||||
$bericht['gefunden'],
|
||||
$bericht['items']['gespeichert'],
|
||||
$bericht['epics']['gespeichert']
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
case 'reorder': {
|
||||
$project = requireProject($writeProject ?? $angefragt, $context);
|
||||
$epic = Http::str('epic');
|
||||
|
||||
@@ -1,8 +1,65 @@
|
||||
{
|
||||
"_comment": "Einzige Quelle der Wahrheit fuer den Changelog. Wird von /docs/changelog.php gerendert und von GET /api/updateservice/v1/changelog?since=X ausgeliefert. Neue Eintraege oben einfuegen.",
|
||||
"schema": 1,
|
||||
"current": "2.9.0",
|
||||
"current": "2.9.1",
|
||||
"versions": [
|
||||
{
|
||||
"version": "2.9.1",
|
||||
"date": "2026-09-20",
|
||||
"summary": "Die Begruendung eines Roadmap-Vorhabens steht jetzt im Deploymentcenter statt nur in einer Markdown-Datei auf einem Entwicklungsrechner. Die Roadmap-Tafel zeigt sie, Agenten koennen sie lesen, und docs/ROADMAP.md wird daraus erzeugt.",
|
||||
"actionRequired": true,
|
||||
"changes": [
|
||||
{
|
||||
"area": "roadmap",
|
||||
"breaking": false,
|
||||
"title": "Begruendungen ziehen in die Datenbank",
|
||||
"text": "bugtracker_items und roadmap_epics tragen jetzt ein Feld rationale (Migration 020). Bisher galt eine Zweiteilung: Zustand im Deploymentcenter, Begruendung in einer Datei im Repository des Projekts, verbunden ueber doc_anchor. Die Beschreibung der uebernommenen Eintraege sagte es woertlich: \"Die Begruendung steht weiterhin in der Datei - hier steht nur der Zustand.\" Das hatte zwei Folgen, die erst im Betrieb auffielen: Die Roadmap-Tafel konnte die Begruendung nicht zeigen, und - schwerer - die Beschreibung von dc_roadmap wies Agenten an, vor dem erneuten Vorschlagen eines zurueckgestellten Vorhabens \"diesen Absatz\" zu lesen, den ein Agent ausserhalb dieses einen Repositorys gar nicht erreichen konnte. Die Anweisung war nicht befolgbar.",
|
||||
"action": "Migration 020 ausfuehren, dann einmalig `python scripts/dc.py roadmap-import` je Projekt mit Roadmap-Datei."
|
||||
},
|
||||
{
|
||||
"area": "roadmap",
|
||||
"breaking": false,
|
||||
"title": "Ein eigenes Feld, nicht description",
|
||||
"text": "description sagt, was gemeldet wurde - es kommt vom Melder, oft von einer fremden Maschine, und wird nicht umgeschrieben. rationale sagt, warum wir uns so entschieden haben. Zusammengelegt haette jede redaktionelle Ueberarbeitung der Begruendung 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 - nicht den ganzen Absatz ein zweites Mal.",
|
||||
"action": null
|
||||
},
|
||||
{
|
||||
"area": "roadmap",
|
||||
"breaking": false,
|
||||
"title": "Einlesen erkennt Absaetze und Tabellenzeilen",
|
||||
"text": "POST /api/bugtracker/v1/roadmap?action=import_rationale liest eine mitgeschickte Roadmap-Datei und verteilt ihre Prosa auf Vorhaben und Bereiche. Drei Formen: \"## A - Titel\" fuer einen Bereich, \"### [A] #65 - Titel\" fuer ein Vorhaben, und Tabellenzeilen der Form \"| #111 | ... | Warum nicht |\". Die dritte Form ist kein Beiwerk: Der Abschnitt \"Verworfen\" fuehrt seine 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 Deploymentcenter bereits hinterlegt ist; ein zweiter Lauf setzt eine dort ueberarbeitete Begruendung nicht stillschweigend zurueck.",
|
||||
"action": null
|
||||
},
|
||||
{
|
||||
"area": "roadmap",
|
||||
"breaking": false,
|
||||
"title": "Die Datei wird erzeugt - jetzt in zwei Bloecken",
|
||||
"text": "Neben dem bestehenden Zustandsblock (dc:roadmap) gibt es ein zweites Markerpaar dc:rationale fuer die Begruendungen. Bewusst ein zweites Paar und keine Erweiterung des ersten: Zwischen den beiden steht in jeder gewachsenen Roadmap-Datei etwas von Hand Geschriebenes - eine Einleitung, eine Leitlinie, die Erklaerung der Statuszeichen -, und ein einziger Block haette das beim naechsten Erzeugen verschluckt. GET ?action=render&part=rationale liefert den Block, ?action=render_file die ganze Datei mit beiden eingesetzten Bloecken. Das Einsetzen laeuft serverseitig, damit die Markersuche nur einmal umgesetzt ist.",
|
||||
"action": null
|
||||
},
|
||||
{
|
||||
"area": "cli",
|
||||
"breaking": false,
|
||||
"title": "scripts/dc.py roadmap-import und roadmap-write",
|
||||
"text": "Die beiden neuen Wege liegen in dc.py und nicht in cli/roadmap.php. Grund: cli/roadmap.php braucht PHP, und auf dem Windows-Arbeitsplatz gibt es keines - die Haelfte der Pflegemaschinerie, die die alte Zweiteilung zusammenhalten sollte, war dort nie ausfuehrbar. roadmap-write behaelt die Zeilenenden der vorhandenen Datei bei, damit nicht nach dem ersten Lauf die ganze Datei als Aenderung im Diff steht.",
|
||||
"action": null
|
||||
},
|
||||
{
|
||||
"area": "webui",
|
||||
"breaking": false,
|
||||
"title": "Begruendung im Seitenpanel der Tafel",
|
||||
"text": "Das Panel zeigt die Begruendung als erstes Feld und laesst sie dort auch schreiben. Zuvor stand an dieser Stelle nur der Verweis doc_anchor - als nicht anklickbarer Text mit dem Hinweis, er zeige auf eine Datei, die das WebUI nie zu sehen bekommt. Kaertchen ohne hinterlegte Begruendung tragen eine dezente Marke; erledigte Vorhaben nicht, dort waere sie ein Vorwurf ohne Adressaten.",
|
||||
"action": null
|
||||
},
|
||||
{
|
||||
"area": "mcp",
|
||||
"breaking": false,
|
||||
"title": "MCP-Adapter 1.3.0",
|
||||
"text": "dc_roadmap_set nimmt rationale entgegen. Die Beschreibung von dc_roadmap verweist nicht mehr auf eine Projektdatei, sondern nennt das Feld - die alte Fassung wies Agenten auf einen Absatz hin, den sie nicht erreichen konnten. doc_anchor bleibt fuer alte Verweise erhalten und ist als historisch gekennzeichnet.",
|
||||
"action": "In Agenten-Umgebungen den MCP-Adapter neu starten, damit die geaenderten Werkzeugbeschreibungen greifen."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.9.0",
|
||||
"date": "2026-09-20",
|
||||
|
||||
+64
-9
@@ -2887,6 +2887,8 @@ $csrfField = Csrf::field();
|
||||
.tafel-marke.pruef { background: rgba(192, 132, 252, 0.18); color: #d8b4fe; }
|
||||
.tafel-marke.hoch { background: rgba(246, 70, 93, 0.18); color: var(--danger); }
|
||||
.tafel-marke.neu { background: rgba(91, 157, 255, 0.2); color: var(--primary); }
|
||||
.tafel-marke.leer { background: transparent; color: var(--text-muted);
|
||||
border: 1px dashed var(--border-glass); opacity: 0.75; }
|
||||
|
||||
.tafel-luecke { height: 0.35rem; border-radius: 3px; transition: background 0.1s ease; }
|
||||
.tafel-luecke.aktiv { background: var(--primary); }
|
||||
@@ -8492,6 +8494,11 @@ function tafelKarte(karte, spur) {
|
||||
}
|
||||
if (karte.lease_expired) { marken.push(['Lease abgelaufen', 'warn']); }
|
||||
if (karte.is_new) { marken.push(['neu', 'neu']); }
|
||||
// Erledigtes braucht keine Begruendung mehr - dort waere die Marke nur
|
||||
// ein Vorwurf ohne Adressaten.
|
||||
if (!karte.has_rationale && karte.state !== 'done') {
|
||||
marken.push(['ohne Begründung', 'leer']);
|
||||
}
|
||||
if (karte.severity === 'critical' || karte.severity === 'high') {
|
||||
marken.push([karte.severity === 'critical' ? 'kritisch' : 'hoch', 'hoch']);
|
||||
}
|
||||
@@ -8707,16 +8714,64 @@ function tafelPanelFuellen(item) {
|
||||
}
|
||||
inhalt.appendChild(kopf);
|
||||
|
||||
if (item.doc_anchor) {
|
||||
const block = panelBlock('Begründung');
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.textContent = item.doc_anchor;
|
||||
link.title = 'Steht im Repository des Projekts, nicht hier.';
|
||||
link.addEventListener('click', ev => ev.preventDefault());
|
||||
block.appendChild(link);
|
||||
inhalt.appendChild(block);
|
||||
/* --- Begründung: warum wir uns so entschieden haben --------------
|
||||
Bis 2.9.0 stand hier nur der Verweis doc_anchor auf einen Absatz in
|
||||
einer Datei, die das WebUI nie zu sehen bekommt. Jetzt steht der
|
||||
Absatz selbst hier - und lässt sich an derselben Stelle schreiben. */
|
||||
const grundBlock = panelBlock('Begründung');
|
||||
|
||||
const grundFeld = document.createElement('textarea');
|
||||
grundFeld.className = 'form-input';
|
||||
grundFeld.rows = 8;
|
||||
grundFeld.style.cssText = 'width:100%; font-family:inherit; line-height:1.5; resize:vertical;';
|
||||
grundFeld.value = item.rationale || '';
|
||||
grundFeld.placeholder = 'Warum ist das so entschieden? Was spricht dafür, was dagegen, '
|
||||
+ 'und woran erkennt man später, dass es richtig war? — Markdown ist erlaubt.';
|
||||
grundBlock.appendChild(grundFeld);
|
||||
|
||||
const grundZeile = document.createElement('div');
|
||||
grundZeile.style.cssText = 'display:flex; gap:0.5rem; margin-top:0.5rem; align-items:center; flex-wrap:wrap;';
|
||||
|
||||
const grundSpeichern = document.createElement('button');
|
||||
grundSpeichern.className = 'btn btn-sm';
|
||||
grundSpeichern.textContent = 'Begründung speichern';
|
||||
grundSpeichern.addEventListener('click', async () => {
|
||||
grundSpeichern.disabled = true;
|
||||
try {
|
||||
await tafelApi('/api/bugtracker/v1/roadmap?action=set', {
|
||||
id: item.id,
|
||||
rationale: grundFeld.value.trim()
|
||||
});
|
||||
await tafelLaden();
|
||||
await tafelPanelOeffnen(item.id);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
grundSpeichern.disabled = false;
|
||||
}
|
||||
});
|
||||
grundZeile.appendChild(grundSpeichern);
|
||||
|
||||
const grundStand = document.createElement('span');
|
||||
grundStand.style.cssText = 'font-size:0.72rem; color:var(--text-muted);';
|
||||
if (item.rationale_updated_at) {
|
||||
grundStand.textContent = 'zuletzt geändert ' + item.rationale_updated_at + ' UTC';
|
||||
} else if (!item.rationale) {
|
||||
grundStand.textContent = 'Noch keine hinterlegt.';
|
||||
}
|
||||
grundZeile.appendChild(grundStand);
|
||||
|
||||
grundBlock.appendChild(grundZeile);
|
||||
|
||||
if (item.doc_anchor) {
|
||||
const alt = document.createElement('div');
|
||||
alt.style.cssText = 'font-size:0.72rem; color:var(--text-muted); margin-top:0.5rem;';
|
||||
alt.textContent = 'Früherer Verweis: ' + item.doc_anchor
|
||||
+ ' — die Datei wird inzwischen aus diesem Feld erzeugt.';
|
||||
grundBlock.appendChild(alt);
|
||||
}
|
||||
|
||||
inhalt.appendChild(grundBlock);
|
||||
|
||||
if (item.defer_trigger) {
|
||||
const block = panelBlock('Wieder dran, wenn');
|
||||
|
||||
Reference in New Issue
Block a user