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
+17
-2
@@ -4,13 +4,28 @@
|
||||
/**
|
||||
* Roadmap — Zustandsblock erzeugen und Prosa abgleichen.
|
||||
*
|
||||
* DAS PROBLEM, DAS DIESES SKRIPT LOEST
|
||||
* SEIT 2.9.1 IST DER HAUPTWEG EIN ANDERER
|
||||
*
|
||||
* Die Begruendungen stehen jetzt ebenfalls im Deploymentcenter (Feld
|
||||
* `rationale`), und die Datei wird vollstaendig daraus erzeugt - beide
|
||||
* Bloecke, Zustand und Begruendungen:
|
||||
*
|
||||
* python scripts/dc.py roadmap-write --project deploymentcenter
|
||||
*
|
||||
* Dieses Skript schreibt weiterhin nur den Zustandsblock und kennt den
|
||||
* Begruendungsblock nicht. Es bleibt fuer den Fall, dass jemand auf dem
|
||||
* Server direkt arbeitet; auf dem Windows-Arbeitsplatz ist es ohnehin nicht
|
||||
* ausfuehrbar, weil dort kein PHP liegt. Genau das war einer der Gruende
|
||||
* fuer die Umstellung: Die Haelfte der Pflegemaschinerie, die die alte
|
||||
* Zweiteilung zusammenhalten sollte, lief dort nie.
|
||||
*
|
||||
* DAS PROBLEM, DAS DIESES SKRIPT URSPRUENGLICH LOESTE
|
||||
*
|
||||
* Roadmap-Zustand steht im Deploymentcenter, die Begruendung in einer
|
||||
* Markdown-Datei im Projektrepository. Zwei Orte fuer einen Vorgang - und
|
||||
* damit die Frage, wie sie zusammenbleiben.
|
||||
*
|
||||
* Die Antwort ist nicht "besser aufpassen", sondern zweigeteilt:
|
||||
* Die Antwort war nicht "besser aufpassen", sondern zweigeteilt:
|
||||
*
|
||||
* --write Der Zustandsteil der Datei wird ERZEUGT, nicht gepflegt.
|
||||
* Zwischen den Markern kann nichts driften, weil dort niemand
|
||||
|
||||
+255
-159
@@ -2,16 +2,27 @@
|
||||
|
||||
Die Begründungen zu allem Geplanten, Zurückgestellten und bewusst Verworfenen.
|
||||
|
||||
**Der Zustand steht nicht hier, sondern im Deploymentcenter** — WebUI → Roadmap,
|
||||
oder `dc_roadmap` über den MCP-Adapter. Was unten zwischen den
|
||||
`<!-- dc:roadmap -->`-Markern steht, ist eine **erzeugte Kopie**: `php
|
||||
cli/roadmap.php --write` schreibt sie, und beim nächsten Lauf wird sie
|
||||
überschrieben. Von Hand dort nichts ändern.
|
||||
**Diese Datei wird erzeugt.** Seit 2.9.1 gilt das nicht mehr nur für den
|
||||
Zustand, sondern auch für die Begründungen. Beides steht im Deploymentcenter —
|
||||
WebUI → Roadmap, oder `dc_roadmap` über den MCP-Adapter — und wird von dort
|
||||
hierher geschrieben:
|
||||
|
||||
Alles außerhalb der Marker ist von Hand geschrieben und bleibt unberührt. Das
|
||||
ist der Teil, der eine Tabellenzelle nicht überlebt: warum etwas so entschieden
|
||||
wurde, woran man erkennt, dass ein Zurückgestelltes wieder ansteht, und warum
|
||||
ein Verworfenes verworfen bleibt.
|
||||
```bash
|
||||
python scripts/dc.py roadmap-write --project deploymentcenter
|
||||
```
|
||||
|
||||
Zwischen den `<!-- dc:roadmap -->`- und den `<!-- dc:rationale -->`-Markern
|
||||
also **nichts von Hand ändern**; der nächste Lauf überschreibt es. Alles
|
||||
außerhalb der Marker bleibt unberührt — diese Einleitung etwa, und der
|
||||
Abschnitt „Pflege dieser Datei" am Ende.
|
||||
|
||||
Bis 2.9.0 stand die Begründung ausschließlich hier. Das hatte zwei Folgen, die
|
||||
erst im Betrieb auffielen: Die Roadmap-Tafel im WebUI konnte sie nicht zeigen —
|
||||
sie sieht nur die Datenbank, und diese Datei liegt auf einem
|
||||
Entwicklungsrechner. Und die Beschreibung von `dc_roadmap` wies Agenten an, vor
|
||||
dem erneuten Vorschlagen eines Zurückgestellten „diesen Absatz" zu lesen; ein
|
||||
Agent in einem anderen Projekt konnte ihn gar nicht erreichen. Die Anweisung
|
||||
war nicht befolgbar.
|
||||
|
||||
Bis 2.6.0 lagen diese Angaben über mehrere Entwurfsdokumente verstreut, bis
|
||||
2.7.0 standen Zustand und Begründung nebeneinander in dieser Datei — beides mit
|
||||
@@ -36,12 +47,12 @@ ausführliche Begründung, aber keinen gültigen Stand mehr.
|
||||
|
||||
---
|
||||
|
||||
<!-- dc:roadmap stand=2026-08-30T11:25Z items=61 projekt=deploymentcenter -->
|
||||
<!-- dc:roadmap stand=2026-09-20T16:57Z items=65 projekt=deploymentcenter -->
|
||||
|
||||
<!-- ERZEUGT - nicht von Hand aendern. Dieser Block wird bei jedem Lauf von
|
||||
`php cli/roadmap.php --write` ueberschrieben. Der Zustand wird im
|
||||
Deploymentcenter gepflegt, nicht hier. Prosa gehoert AUSSERHALB der
|
||||
Marker - dort bleibt sie unberuehrt. -->
|
||||
`python scripts/dc.py roadmap-write` ueberschrieben. Der Zustand wird
|
||||
im Deploymentcenter gepflegt, nicht hier - und seit 2.9.1 auch die
|
||||
Begruendungen, die im Block dc:rationale weiter unten stehen. -->
|
||||
|
||||
### Als Naechstes
|
||||
|
||||
@@ -98,6 +109,7 @@ ausführliche Begründung, aber keinen gültigen Stand mehr.
|
||||
| #83 | Monitoren ein Projekt zuordnen (watchdog_monitors.project_slug) | 📋 Offen | — |
|
||||
| #84 | Je Projekt ein Werkbank-Token (dev_agent) anlegen | 📋 Offen | — |
|
||||
| #85 | Supervisor-Vorbereitungen in den Projekten zurückbauen | 📋 Offen | — |
|
||||
| #666 | WebUI-Anmeldung: Hardwarekey (WebAuthn) als zweite Stufe, Notfalltoken als Ersatzweg, Konten umbenennbar machen | 🔨 In Arbeit | `2.9.0` |
|
||||
|
||||
### F — Quelltext-Zugriff
|
||||
|
||||
@@ -113,7 +125,7 @@ ausführliche Begründung, aber keinen gültigen Stand mehr.
|
||||
|---|---|---|---|
|
||||
| #93 | dc-testkit supervisor — prüfen, ob eine Anbindung wirklich meldet | 📋 Offen | — |
|
||||
| #96 | Testartefakt oder echter Befund? — die offene Frage | 📋 Offen | — |
|
||||
| #89 | Kopfloser Installationsmodus fuer den update-agent | 📋 Offen | — |
|
||||
| #89 | Kopfloser Installationsmodus fuer den update-agent | ✅ Erledigt | `2.8.2` |
|
||||
| #90 | DEV-Lizenzschluessel zur Selbstbedienung (POST /api/license/v1/dev-key, Scope license:dev) | ✅ Erledigt | `2.6.1` |
|
||||
| #91 | dc-testkit — ein Weg fuer Lizenz, Auslieferung und Meldung | ✅ Erledigt | `2.6.1` |
|
||||
| #92 | Werkbank-Token je Projekt auf der Test-VM hinterlegen | 📋 Offen | — |
|
||||
@@ -138,6 +150,9 @@ ausführliche Begründung, aber keinen gültigen Stand mehr.
|
||||
| #108 | Die anderen Projekte übernehmen (myapp, polytrader, predictalytics) | 📋 Offen | — |
|
||||
| #109 | --check in scripts/deploy.py einhängen | 📋 Offen | — |
|
||||
| #110 | Auswertung: wie lange steht ein Vorhaben in welcher Lage? | 🧊 Zurueckgestellt — Ausloeser: wenn die Frage „warum dauert das so lange?" tatsächlich gestellt wird. status_changed_at und das strukturierte meta_j... | — |
|
||||
| #177 | roadmap_rank ist bereichslokal und projektweit zugleich: nextUp sortiert global, reorder zaehlt je Bereich neu ab 10 | 🔨 In Arbeit | `2.9.0` |
|
||||
| #664 | Roadmap-Tafel: Zeitstrahl nach Prioritaet aus den Items je Projekt erzeugen, mit klickbaren Eintraegen und Kommentar ... | 🔨 In Arbeit | `2.9.0` |
|
||||
| #667 | Erzeugter Markdown-Block und Roadmap-Tafel benutzen verschiedene Phaseneinteilungen | 📋 Offen | — |
|
||||
|
||||
### H — Verworfen
|
||||
|
||||
@@ -162,8 +177,16 @@ ausführliche Begründung, aber keinen gültigen Stand mehr.
|
||||
|
||||
## Begründungen
|
||||
|
||||
Ab hier von Hand. Jede Überschrift trägt die Item-Nummer, unter der das
|
||||
Vorhaben im Deploymentcenter steht.
|
||||
<!-- dc:rationale stand=2026-09-20T16:57Z items=39 projekt=deploymentcenter -->
|
||||
|
||||
> **Erzeugt — hier nicht von Hand aendern.** Die Begruendungen stehen im
|
||||
> Deploymentcenter und werden von dort geschrieben: in der Roadmap-Tafel am
|
||||
> Kaertchen oder ueber `dc_roadmap_set`. Der naechste
|
||||
> `python scripts/dc.py roadmap write` ueberschreibt diesen Abschnitt.
|
||||
>
|
||||
> Der Grund fuer die Umstellung: Solange die Begruendung nur hier stand, war
|
||||
> sie fuer die Tafel unsichtbar und fuer jeden Agenten ausserhalb dieses
|
||||
> Repositorys unerreichbar.
|
||||
|
||||
## A — Supervisor und Befunde
|
||||
|
||||
@@ -187,8 +210,7 @@ Abschnitt 0.
|
||||
> **Error-Reporting und Bugtracker** besser aufgehoben. Die werden
|
||||
> weiterentwickelt.
|
||||
|
||||
|
||||
### [A] #65 — Symbol-Upload und `dc_symbolicate`
|
||||
### [A] #65 — Symbol-Upload und dc_symbolicate 📋
|
||||
|
||||
Der Packager lädt beim `publish` die PDB bzw. Sourcemap mit hoch, verknüpft mit
|
||||
`commit_sha`. Ausgeliefert wird ein gestripptes Binary. Der Kunde bekommt
|
||||
@@ -202,12 +224,18 @@ Fehlermeldung überhaupt etwas anfangen kann.
|
||||
Betroffen: `client-dotnet/Deploymentcenter.Packager`, ein Ablageort für
|
||||
Symboldateien, `dc_symbolicate` im MCP-Adapter.
|
||||
|
||||
### [A] #66 — `novel.log_template`
|
||||
### [A] #64 — Supervisor-Client für .NET und PHP ❌
|
||||
|
||||
Verworfen. Die Begründung steht in Abschnitt H, beim Beobachter im
|
||||
ausgelieferten Programm — dieser Client wäre sein Einbauteil gewesen und fällt
|
||||
mit ihm weg.
|
||||
|
||||
### [A] #66 — novel.log_template — Logzeilen-Schablonen, die noch nie auftraten 🧊
|
||||
|
||||
**Auslöser:** sobald Logauszüge strukturiert hereinkommen. Heute liefert nichts
|
||||
sie in einer Form, aus der sich Schablonen bilden ließen.
|
||||
|
||||
### [A] #67 — Längsschnitt für `regression.build`
|
||||
### [A] #67 — Längsschnitt für regression.build 🧊
|
||||
|
||||
`regression.build` vergleicht heute die beiden höchsten Fassungen im Feld im
|
||||
**Querschnitt** — verschiedene Maschinen zur selben Zeit, nicht dieselbe
|
||||
@@ -217,7 +245,7 @@ deshalb auch von der Maschine kommen statt von der Fassung.
|
||||
**Auslöser:** wenn Fehlzuordnungen auffallen. Nötig wäre eine Historie der
|
||||
`app_version` je Monitor; das Feld wird heute überschrieben.
|
||||
|
||||
### [A] #68 — Schwellen nachprüfen
|
||||
### [A] #68 — Schwellen der serverseitigen Detektoren an echten Daten nachprüfen 📋
|
||||
|
||||
Die serverseitigen Detektoren rechnen mit gesetzten, aber nirgends
|
||||
nachgeprüften Werten: `SIGMA = 3.0` für `deviation.self` und `deviation.fleet`,
|
||||
@@ -229,20 +257,9 @@ lautet für jeden Wert gleich: Wie viele der erzeugten Befunde hat jemand als
|
||||
brauchbar eingestuft? Der Status `ignored` an den Befunden beantwortet das von
|
||||
selbst, sobald gesichtet wird.
|
||||
|
||||
---
|
||||
## B — Oberflaeche
|
||||
|
||||
### [A] #64 — Supervisor-Client für .NET und PHP
|
||||
|
||||
Verworfen. Die Begründung steht in Abschnitt H, beim Beobachter im
|
||||
ausgelieferten Programm — dieser Client wäre sein Einbauteil gewesen und fällt
|
||||
mit ihm weg.
|
||||
|
||||
---
|
||||
|
||||
## B — Oberfläche
|
||||
|
||||
|
||||
### [B] #69 — Befunde im WebUI
|
||||
### [B] #69 — Befunde im WebUI: Liste, Filter nach Detektor, Einstufen 📋
|
||||
|
||||
Befunde sind heute nur über `/api/supervisor/v1/findings` und die MCP-Werkzeuge
|
||||
erreichbar. Für die Sichtung durch einen Menschen fehlt die Ansicht — und damit
|
||||
@@ -253,18 +270,15 @@ mit `observed`/`baseline`, und die Statuswechsel (`acknowledged`, `promoted`,
|
||||
`resolved`, `ignored`). Beim Übernehmen in ein Bugtracker-Item die Verknüpfung
|
||||
über `supervisor_findings.bug_item_id` setzen — die Spalte gibt es bereits.
|
||||
|
||||
### [B] #71 — Monitoren ein Projekt zuweisen
|
||||
### [B] #71 — Monitoren ein Projekt zuweisen 📋
|
||||
|
||||
`watchdog_monitors.project_slug` wird administrativ gesetzt, nicht vom Agenten
|
||||
gemeldet. Dafür fehlt das Bedienelement in der Monitor-Verwaltung. Bis dahin
|
||||
nur per SQL — siehe #81.
|
||||
|
||||
---
|
||||
|
||||
## C — Projektlandschaft
|
||||
|
||||
|
||||
### [C] #74 — Rückkanal
|
||||
### [C] #74 — Rückkanal: „was du gemeldet hast, ist behoben" ✅
|
||||
|
||||
Wird ein Item behoben, das ein anderes Projekt gemeldet hat, erfährt dieses
|
||||
Projekt heute nichts davon. Folge: Es meldet denselben Punkt in drei Monaten
|
||||
@@ -299,7 +313,7 @@ Was bleibt: Wer einen Workaround gebaut hat, muss den Prüfauftrag auch
|
||||
annehmen. Nimmt ihn niemand an, wird nach 30 Tagen ungeprüft geschlossen — mit
|
||||
einem Vermerk, der das ausdrücklich sagt.
|
||||
|
||||
### [C] #75 — Prompt-Vorlage entlasten
|
||||
### [C] #75 — Agent-Prompt-Vorlage in den Projekten durch MCP ablösen ✅
|
||||
|
||||
[AGENT_PROMPT_TEMPLATE.md](./AGENT_PROMPT_TEMPLATE.md) ist in jedes Projekt
|
||||
einkopiert, samt der von Hand gepflegten Versionszeile.
|
||||
@@ -323,12 +337,9 @@ Das ist der Punkt, an dem es sich lohnt, die Vorlage in drei Bausteine zu
|
||||
zerlegen statt in einen Block: Grundlagen, „mit MCP", „auf der Test-VM". Heute
|
||||
trägt jedes Projekt alles mit.
|
||||
|
||||
---
|
||||
|
||||
## D — Werkzeuganbindung (MCP)
|
||||
|
||||
|
||||
### [D] #78 — Abgleich Werkzeugliste ↔ OpenAPI
|
||||
### [D] #78 — Abgleich: Werkzeugliste gegen openapi.php 📋
|
||||
|
||||
Adapter und OpenAPI beschreiben dieselben Endpunkte. Ohne Abgleich laufen sie
|
||||
auseinander, und es fällt erst auf, wenn ein Werkzeug ins Leere greift.
|
||||
@@ -338,15 +349,13 @@ von Hand oder aus `deploy.py` heraus läuft. Eine echte Pipeline wäre ein
|
||||
eigenes Vorhaben und lohnt erst, wenn es mehr zu prüfen gibt als diese eine
|
||||
Zusage.
|
||||
|
||||
### [D] #79 — Entfernter Server
|
||||
### [D] #79 — Entfernter MCP-Server unter /mcp mit OAuth 🧊
|
||||
|
||||
**Auslöser:** wenn mehrere Maschinen oder Personen dranhängen. Dann aber mit
|
||||
OAuth statt Bearer-Token in einer Konfigurationsdatei — sonst liegt dasselbe
|
||||
Token auf jeder Maschine. Solange der stdio-Adapter je Projekt reicht, lohnt
|
||||
der Aufwand nicht.
|
||||
|
||||
---
|
||||
|
||||
## E — Betrieb
|
||||
|
||||
Kein Code, aber Voraussetzung dafür, dass Gebautes wirkt.
|
||||
@@ -382,15 +391,31 @@ Beobachterteil.
|
||||
Der Auftrag zum Einfügen steht in
|
||||
[SUPERVISOR_INTEGRATION_GUIDE.md](./SUPERVISOR_INTEGRATION_GUIDE.md).
|
||||
|
||||
---
|
||||
### [E] #666 — WebUI-Anmeldung: Hardwarekey (WebAuthn) als zweite Stufe, Notfalltoken als Ersatzweg, Konten umbenennbar machen 🔨
|
||||
|
||||
## F — Quelltext-Zugriff für die Diagnose
|
||||
Das Verwaltungskonto hing an einem einzigen Geheimnis. Wer das Passwort hat, hat Tokens, Lizenzen, Monitore und die Release-Strecke aller angebundenen Projekte - und ein Passwort wandert: in Passwortspeicher, in Zwischenablagen, gelegentlich in einen Chat.
|
||||
|
||||
Ein Hardwarekey behebt genau das und nichts anderes. Er laesst sich nicht kopieren, nicht abtippen und nicht per Fernzugriff abgreifen, und er ist an die Adresse gebunden, unter der er registriert wurde - eine nachgebaute Anmeldeseite laeuft deshalb ins Leere, auch wenn das Passwort dort eingegeben wird.
|
||||
|
||||
== Warum die Pflicht am Schluessel haengt und nicht an einem Schalter ==
|
||||
Ein Schalter "zweiter Faktor erforderlich" waere die naheliegende Bauart und die gefaehrlichere: Wer ihn umlegt, bevor ein Schluessel registriert ist, sperrt sich aus der eigenen Verwaltung aus - und zwar aus derjenigen, in der man den Schalter zuruecknehmen wuerde. So herum kann das nicht passieren: Wer einen Schluessel registriert hat, hat ihn auch in der Hand.
|
||||
|
||||
Der Preis ist, dass die zweite Stufe stillschweigend unwirksam bleibt, solange niemand registriert. Deshalb steht in der Benutzerverwaltung "nur Passwort" an jedem Konto ohne Schluessel, statt dass dort nichts steht.
|
||||
|
||||
== Warum ohne Fremdbibliothek ==
|
||||
Das Projekt hat bewusst kein Composer und kein vendor/. Ein Server, der sich ueber FTP aktualisieren laesst, soll nicht von einem Abhaengigkeitsbaum abhaengen, den niemand mehr liest. CBOR-Leser, COSE-nach-DER-Wandlung und Signaturpruefung sind zusammen rund fuenfhundert Zeilen - weniger, als eine Bibliothek an Pflege kostet, und pruefbar gegen einen Software-Authenticator.
|
||||
|
||||
== Der Notfalltoken ist ein Zugestaendnis, kein Komfort ==
|
||||
Ein zweiter Faktor ohne Ersatzweg ist ein Einzelpunktversagen mit Ansage: kaputter Stick, verlorener Schluessel, und die Verwaltung ist zu. Der Token ist deshalb absichtlich unbequem - einmal angezeigt, einmal verwendbar, danach weg. Er soll die Ausnahme bleiben und nicht der bequemere Hauptweg werden.
|
||||
|
||||
Dass er als gewoehnliches Formular ohne JavaScript laeuft, ist kein Zufall: Ein Ersatzweg mit denselben Voraussetzungen wie der Hauptweg ist keiner.
|
||||
|
||||
## F — Quelltext-Zugriff
|
||||
|
||||
Die Software bleibt geschlossen. Es gibt keine öffentliche Stelle, an der
|
||||
Quelltext zu sehen ist. Drei Stufen, in aufsteigender Preisgabe:
|
||||
|
||||
|
||||
### [F] #87 — Quelltext-Index Stufe 1
|
||||
### [F] #87 — Stufe 1 — Quelltext-Index im DC, nur Ausschnitte 🧊
|
||||
|
||||
**Auslöser:** wenn ein Agent autonom rund um die Uhr laufen soll, ohne dass
|
||||
eine eigene Maschine läuft. Solange Claude Code den Checkout ohnehin lokal hat,
|
||||
@@ -407,39 +432,18 @@ Neuer Scope `source:read`, nie an Client-Token, verschlüsselt abgelegt, Budget
|
||||
je Token und Tag, jeder Abruf im Prüfprotokoll. Der Unterschied zwischen „darf
|
||||
nachschlagen" und „darf mitnehmen".
|
||||
|
||||
### [F] #88 — Stufe 2
|
||||
### [F] #88 — Stufe 2 — nur Metadaten: Signaturen, Aufrufgraph, keine Rümpfe 🧊
|
||||
|
||||
**Auslöser:** falls selbst Ausschnitte zu viel sind. Signaturen, Aufrufgraph
|
||||
und Doc-Kommentare ohne Methodenkörper reichen zur Triage, aber nicht zum
|
||||
Nachbauen.
|
||||
|
||||
---
|
||||
|
||||
## G — Testumgebung und Selbstbedienung
|
||||
## G — Testumgebung
|
||||
|
||||
Die geteilte Linux-Test-VM. Anleitung: **[TESTUMGEBUNG.md](./TESTUMGEBUNG.md)** — dort
|
||||
steht auch, was auf der Maschine belegt ist.
|
||||
|
||||
|
||||
### [G] #89 — Kopfloser Installationsmodus
|
||||
|
||||
`update-agent --action install` ist ein Dialog mit Auswahllisten und Passwortabfrage. Ein
|
||||
Agent kann ihn nicht treiben: Auswahllisten brauchen Pfeiltasten an einem echten Terminal,
|
||||
und ein Agent darf grundsaetzlich kein Passwort eingeben.
|
||||
|
||||
Folge: `dc-testkit install` prueft die **Auslieferung** in ein leeres Verzeichnis — denselben
|
||||
Pfad, den der Installer intern nimmt, mit Download, SHA256, Signatur, Staging und Rollback.
|
||||
Ungeprueft bleiben damit die Aufloesung von `setup.json` und das Schreiben der Konfiguration.
|
||||
Das ist genau der Teil, der je Anwendung anders aussieht und deshalb am ehesten bricht.
|
||||
|
||||
Gebraucht wird `--action install --answers antworten.json`: dieselben Felder, die der Dialog
|
||||
erfragt, aus einer Datei. Fehlt ein Pflichtfeld, bricht der Lauf ab, statt zu fragen. Das
|
||||
Installationskonto bliebe dabei aussen vor — mit einem DEV-Lizenzschluessel kommt der Agent
|
||||
ohnehin an die Release-Ablage.
|
||||
|
||||
Betroffen: `client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs`.
|
||||
|
||||
### [G] #93 — `dc-testkit findings`
|
||||
### [G] #93 — dc-testkit supervisor — prüfen, ob eine Anbindung wirklich meldet 📋
|
||||
|
||||
Die Nahtstelle zwischen Testumgebung und Supervisor, die heute fehlt.
|
||||
|
||||
@@ -465,39 +469,7 @@ kann niemand entscheiden, ob einer ein Artefakt des Prüfstands war.
|
||||
Das Token braucht dafür zusätzlich `supervisor:read`; heute trägt es auf der
|
||||
VM nur `bugtracker:manage` und `license:dev` (§4.2 der Anleitung).
|
||||
|
||||
### [G] #94 — Test-Monitore aus dem Flottenvergleich
|
||||
|
||||
`deviation.fleet` bildet Kohorten aus (Projekt, `app_version`) und meldet, wer
|
||||
daraus ausschert. `watchdog_monitors` hat **kein** Feld für die Umgebung.
|
||||
|
||||
Bekommt die Test-VM einen Monitor mit demselben Projekt und derselben Fassung
|
||||
wie die Produktivinstallationen, landet sie in deren Kohorte — und fällt dort
|
||||
zwangsläufig auf: andere Maschine, andere Last, andere Plattenbelegung. Das
|
||||
Ergebnis wären Befunde, die nichts über die Software aussagen, dafür aber die
|
||||
Kohortenwerte verschieben und damit echte Ausreißer verdecken.
|
||||
|
||||
Zwei Wege, beide vertretbar:
|
||||
|
||||
- ein Feld `environment` an `watchdog_monitors`, und `deviation.fleet` sowie
|
||||
`regression.build` sehen nur `production` an
|
||||
- oder eine Kennzeichnung über `group_key`, ohne Schemaänderung
|
||||
|
||||
Solange auf der VM kein Monitor mit Produktivfassung läuft, ist nichts kaputt.
|
||||
Es fällt nur genau dann auf, wenn es zu spät ist — deshalb hier vermerkt.
|
||||
|
||||
### [G] #95 — Umgebung am Befund
|
||||
|
||||
Testläufe **sollen** Befunde erzeugen — ein Lauf, der einen Fehler auslöst, den
|
||||
es vorher nicht gab, hat genau das getan, wofür er da ist. Die Detektoren
|
||||
laufen deshalb in jeder Umgebung.
|
||||
|
||||
Unterschieden wird zweifach: über `environment` am Befund, und über den
|
||||
Schweregradhinweis. Was außerhalb von `production` auffällt, ist eine Stufe
|
||||
niedriger eingestuft — nicht weil es unwichtig wäre, sondern weil dort
|
||||
systematisch mehr Rauschen entsteht. Wer beides gleich hoch einstuft, liest
|
||||
bald keines von beiden mehr.
|
||||
|
||||
### [G] #96 — Testartefakt oder echter Befund?
|
||||
### [G] #96 — Testartefakt oder echter Befund? — die offene Frage 📋
|
||||
|
||||
**Die eigentlich schwierige Frage.** Ein Testlauf meldet „API-Aufruf
|
||||
fehlgeschlagen". Liegt das daran, dass auf der VM keine Live-Zugangsdaten
|
||||
@@ -548,7 +520,64 @@ ist keine Umsetzung, sondern eine Lesehaltung: beim Sichten nicht fragen
|
||||
„warum ist das fehlgeschlagen", sondern „wie hat sich die Software dabei
|
||||
verhalten".
|
||||
|
||||
### [G] #98 — Auswertung der Statuswechsel
|
||||
### [G] #89 — Kopfloser Installationsmodus fuer den update-agent ✅
|
||||
|
||||
`update-agent --action install` ist ein Dialog mit Auswahllisten und Passwortabfrage. Ein
|
||||
Agent kann ihn nicht treiben: Auswahllisten brauchen Pfeiltasten an einem echten Terminal,
|
||||
und ein Agent darf grundsaetzlich kein Passwort eingeben.
|
||||
|
||||
Folge: `dc-testkit install` prueft die **Auslieferung** in ein leeres Verzeichnis — denselben
|
||||
Pfad, den der Installer intern nimmt, mit Download, SHA256, Signatur, Staging und Rollback.
|
||||
Ungeprueft bleiben damit die Aufloesung von `setup.json` und das Schreiben der Konfiguration.
|
||||
Das ist genau der Teil, der je Anwendung anders aussieht und deshalb am ehesten bricht.
|
||||
|
||||
Gebraucht wird `--action install --answers antworten.json`: dieselben Felder, die der Dialog
|
||||
erfragt, aus einer Datei. Fehlt ein Pflichtfeld, bricht der Lauf ab, statt zu fragen. Das
|
||||
Installationskonto bliebe dabei aussen vor — mit einem DEV-Lizenzschluessel kommt der Agent
|
||||
ohnehin an die Release-Ablage.
|
||||
|
||||
Betroffen: `client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs`.
|
||||
|
||||
### [G] #92 — Werkbank-Token je Projekt auf der Test-VM hinterlegen 📋
|
||||
|
||||
Ohne das Token laufen `dc-testkit license` und `dc-testkit report` ins Leere. Je Projekt eines
|
||||
anlegen (Besitzerart `dev_agent`, Rechte `bugtracker:manage` und `license:dev`), auf der VM
|
||||
unter `~/.config/dc-testkit/` hinterlegen und in der Belegungsliste vermerken. Deckt sich mit
|
||||
#84 — dasselbe Token traegt beides.
|
||||
|
||||
### [G] #94 — Test-Monitore aus dem Flottenvergleich heraushalten 📋
|
||||
|
||||
`deviation.fleet` bildet Kohorten aus (Projekt, `app_version`) und meldet, wer
|
||||
daraus ausschert. `watchdog_monitors` hat **kein** Feld für die Umgebung.
|
||||
|
||||
Bekommt die Test-VM einen Monitor mit demselben Projekt und derselben Fassung
|
||||
wie die Produktivinstallationen, landet sie in deren Kohorte — und fällt dort
|
||||
zwangsläufig auf: andere Maschine, andere Last, andere Plattenbelegung. Das
|
||||
Ergebnis wären Befunde, die nichts über die Software aussagen, dafür aber die
|
||||
Kohortenwerte verschieben und damit echte Ausreißer verdecken.
|
||||
|
||||
Zwei Wege, beide vertretbar:
|
||||
|
||||
- ein Feld `environment` an `watchdog_monitors`, und `deviation.fleet` sowie
|
||||
`regression.build` sehen nur `production` an
|
||||
- oder eine Kennzeichnung über `group_key`, ohne Schemaänderung
|
||||
|
||||
Solange auf der VM kein Monitor mit Produktivfassung läuft, ist nichts kaputt.
|
||||
Es fällt nur genau dann auf, wenn es zu spät ist — deshalb hier vermerkt.
|
||||
|
||||
### [G] #95 — Befunde tragen die Umgebung, ausserhalb production eine Stufe niedriger ✅
|
||||
|
||||
Testläufe **sollen** Befunde erzeugen — ein Lauf, der einen Fehler auslöst, den
|
||||
es vorher nicht gab, hat genau das getan, wofür er da ist. Die Detektoren
|
||||
laufen deshalb in jeder Umgebung.
|
||||
|
||||
Unterschieden wird zweifach: über `environment` am Befund, und über den
|
||||
Schweregradhinweis. Was außerhalb von `production` auffällt, ist eine Stufe
|
||||
niedriger eingestuft — nicht weil es unwichtig wäre, sondern weil dort
|
||||
systematisch mehr Rauschen entsteht. Wer beides gleich hoch einstuft, liest
|
||||
bald keines von beiden mehr.
|
||||
|
||||
### [G] #98 — Auswertung über meta_json: wie oft fällt ein Fix durch die Prüfung? 🧊
|
||||
|
||||
**Auslöser:** wenn die Frage „wie oft trägt ein Fix nicht?" tatsächlich gestellt
|
||||
wird. Seit 2.6.1 schreibt jeder Statuswechsel strukturiertes `meta_json`
|
||||
@@ -559,15 +588,6 @@ Eine eigene Tabelle verdient sich erst bei Fragen über Items hinweg — Median
|
||||
Prüfdauer, Quote durchgefallener Fixes je Projekt. Vorher wäre sie eine zweite
|
||||
Wahrheit über denselben Vorgang.
|
||||
|
||||
### [G] #92 — Werkbank-Token auf der Test-VM
|
||||
|
||||
Ohne das Token laufen `dc-testkit license` und `dc-testkit report` ins Leere. Je Projekt eines
|
||||
anlegen (Besitzerart `dev_agent`, Rechte `bugtracker:manage` und `license:dev`), auf der VM
|
||||
unter `~/.config/dc-testkit/` hinterlegen und in der Belegungsliste vermerken. Deckt sich mit
|
||||
#84 — dasselbe Token traegt beides.
|
||||
|
||||
---
|
||||
|
||||
## I — Roadmap im Deploymentcenter
|
||||
|
||||
Diese Datei war bis 2.6.1 der einzige Ort für Planung — und pflegte damit
|
||||
@@ -576,8 +596,20 @@ genau diese drei Dinge bereits führt. Zwei Pfleger für dasselbe Feld.
|
||||
|
||||
Ab 2.7.0 gilt die Arbeitsteilung: **Zustand im DC, Begründung hier.**
|
||||
|
||||
### [I] #107 — Diese Datei selbst übernehmen — Einträge A1–H als Items anlegen, Kennungen auf Item-IDs umstellen ✅
|
||||
|
||||
### [I] #99 ff. — Migration, Modul, Endpunkt, Oberfläche, CLI und Leitfaden
|
||||
Die Einträge A1–G9 stehen noch als handvergebene Kürzel hier. Sie werden Items;
|
||||
die Abschnitte werden Themenbereiche (in Migration 016 bereits angelegt).
|
||||
Danach ersetzt ein erzeugter Block die Statustabellen, und die Prosa bleibt.
|
||||
|
||||
**Reihenfolge:** erst #106 (Migration), dann #107, dann #109 (Abgleich im Deploy).
|
||||
#108 kommt zuletzt — die anderen Projekte bekommen ein Verfahren, das hier
|
||||
einmal durchlaufen ist, nicht eines aus der Anleitung.
|
||||
|
||||
Bis dahin gilt diese Datei unverändert weiter. Sie ist erst abgelöst, wenn der
|
||||
Block darin steht.
|
||||
|
||||
### [I] #99 — Migration 016: epic, roadmap_rank, target_build, defer_trigger, doc_anchor, Status deferred, Tabelle roadmap_epics ✅
|
||||
|
||||
Kein neues Modul und keine Roadmap-Tabelle: Ein Roadmap-Eintrag **ist** ein
|
||||
`bugtracker_item`. Es fehlten nur vier Angaben, die ein Backlog nicht braucht
|
||||
@@ -600,69 +632,133 @@ Ohne benannten Anlass ist ein Vorhaben nicht zurückgestellt, sondern vergessen
|
||||
und die Regel gehört an die Stelle, an der geschrieben wird, nicht nur in ein
|
||||
Formular.
|
||||
|
||||
### [I] #107 — diese Datei übernehmen
|
||||
|
||||
Die Einträge A1–G9 stehen noch als handvergebene Kürzel hier. Sie werden Items;
|
||||
die Abschnitte werden Themenbereiche (in Migration 016 bereits angelegt).
|
||||
Danach ersetzt ein erzeugter Block die Statustabellen, und die Prosa bleibt.
|
||||
|
||||
**Reihenfolge:** erst #106 (Migration), dann #107, dann #109 (Abgleich im Deploy).
|
||||
#108 kommt zuletzt — die anderen Projekte bekommen ein Verfahren, das hier
|
||||
einmal durchlaufen ist, nicht eines aus der Anleitung.
|
||||
|
||||
Bis dahin gilt diese Datei unverändert weiter. Sie ist erst abgelöst, wenn der
|
||||
Block darin steht.
|
||||
|
||||
### [I] #110 — Auswertung der Verweildauern
|
||||
### [I] #110 — Auswertung: wie lange steht ein Vorhaben in welcher Lage? 🧊
|
||||
|
||||
**Auslöser:** wenn die Frage „warum dauert das so lange?" tatsächlich gestellt
|
||||
wird. `status_changed_at` und das strukturierte `meta_json` aus 2.6.1 tragen
|
||||
die Antwort bereits; eine Auswertung daneben lohnt erst, wenn jemand sie liest.
|
||||
|
||||
---
|
||||
### [I] #177 — roadmap_rank ist bereichslokal und projektweit zugleich: nextUp sortiert global, reorder zaehlt je Bereich neu ab 10 🔨
|
||||
|
||||
Dieselbe Spalte wurde an drei Stellen verschieden gelesen: nextUp() sortierte projektweit, reorder() zaehlte je Bereich ab 10 neu durch, und die Werkzeugbeschreibung sagte "Reihenfolge im Bereich". Von aussen liess sich der Rang deshalb nicht so vergeben, dass alle drei Lesarten zugleich stimmten.
|
||||
|
||||
Entschieden wurde fuer PROJEKTWEIT. Die Gegenrichtung waere gewesen, nextUp() nach (epic.sort_order, rank) sortieren zu lassen - dann haengt die Dringlichkeit eines Vorhabens aber an der Sortiernummer seines Bereichs, und eine Sicherheitsluecke in Bereich G stuende hinter jeder Kleinigkeit aus Bereich A. Das ist nicht die Aussage, die eine Prioritaetenliste treffen soll.
|
||||
|
||||
Ausserdem gab die gemeldete Beobachtung den Ausschlag: eine durchgehende Reihenfolge ueber sieben Abschnitte hinweg, in der Sicherheit, Betrieb und Faehigkeiten abwechseln. So eine Liste ist nur projektweit abbildbar.
|
||||
|
||||
Die Folge fuer reorder() ist die interessantere Haelfte. Es zaehlt jetzt nicht mehr durch, sondern vertauscht ausschliesslich die Raenge, die die genannten Eintraege ohnehin halten - ihre Plaetze in der projektweiten Reihenfolge bleiben als Menge unveraendert, nur wer auf welchem sitzt, aendert sich. Vorher hatte eine Geste innerhalb eines Bereichs eine projektweite Wirkung, die niemand angefordert hatte.
|
||||
|
||||
### [I] #664 — Roadmap-Tafel: Zeitstrahl nach Prioritaet aus den Items je Projekt erzeugen, mit klickbaren Eintraegen und Kommentar ... 🔨
|
||||
|
||||
Eine Liste beantwortet "was gibt es", eine Tafel beantwortet "was ist als Naechstes dran und warum in dieser Reihenfolge". Das sind verschiedene Fragen, und die zweite wird oefter gestellt.
|
||||
|
||||
== Warum der Server die Gliederung liefert ==
|
||||
Der naheliegende Weg waere gewesen, die Items als Liste zu holen und im Browser zu schneiden. Dann schneidet aber das WebUI anders als die CLI, und ein spaeterer Export ein drittes Mal anders - aus denselben Daten entstuenden drei Wahrheiten ueber dieselbe Planung. Deshalb liefert der Server unter ?action=board die fertige Struktur, und die Darstellung liest nur daraus.
|
||||
|
||||
== Warum die Phasengrenzen nicht im Code stehen ==
|
||||
roadmap_rank wird je Projekt frei vergeben. Wer in Zehnern zaehlt, hat andere Abstaende als jemand, der in Hundertern zaehlt. Eine Grenze im Code waere fuer genau ein Projekt richtig und fuer jedes andere falsch. Gepflegt werden sie in roadmap_phases; ist dort nichts hinterlegt, schneidet die Tafel an den groessten Luecken der Rangfolge - und sagt dazu, dass die Grenzen geraten sind. Eine geratene Grenze, die wie eine Entscheidung aussieht, waere schlimmer als gar keine.
|
||||
|
||||
== Nachpruefung ist ein eigener Zustand ==
|
||||
awaiting_verification, in_verification und fix_insufficient als "erledigt" zu zeichnen waere bequem und falsch: Ein Fix, der nicht getragen hat, ist keine Erledigung. Im Projekt clawddotnet stand deshalb in der Bastelloesung eine von Hand danebengeschriebene Marke "Status pruefen". Genau die faellt jetzt weg.
|
||||
|
||||
== Das Seitenpanel ist der eigentliche Gewinn ==
|
||||
Der Weg zurueck war gebrochen: Wer in der Uebersicht etwas bemerkte, musste sich die Nummer merken und im Bugtracker-Tab danach suchen. Ein Umweg, den man ein paarmal geht und danach nicht mehr - die Anmerkung unterbleibt dann einfach. Bemerken und anmerken gehoeren an dieselbe Stelle.
|
||||
|
||||
== Was das Ziehen voraussetzte ==
|
||||
Ohne #177 waere es nicht zu bauen gewesen: Solange roadmap_rank gleichzeitig projektweit und bereichslokal gelesen wurde, haette jede Verschiebung auf der Tafel gegen die vorhandene Sortierlogik gearbeitet. Der Client schickt deshalb nur die beiden Nachbarn; den Rang rechnet der Server, der als Einziger weiss, was sonst noch zwischen ihnen liegt.
|
||||
|
||||
### [I] #667 — Erzeugter Markdown-Block und Roadmap-Tafel benutzen verschiedene Phaseneinteilungen 📋
|
||||
|
||||
Seit 2.9.1 gibt es zwei erzeugte Sichten auf dieselbe Planung, und sie ordnen verschieden: Die Tafel schneidet Spalten aus roadmap_rank, der Markdown-Block gliedert nach Themenbereich. Wer docs/ROADMAP.md aufschlaegt und wer die Tafel ansieht, bekommt denselben Bestand in zwei Gliederungen.
|
||||
|
||||
Beide sind fuer sich richtig - das ist der Grund, warum es nicht dringend ist. Es ist aber genau die Sorte Abweichung, die die Umstellung auf eine erzeugte Datei beseitigen sollte: Der Zustandsteil wird nicht mehr gepflegt, aber er wird zweimal verschieden erzeugt, und irgendwann haelt das jemand fuer einen Fehler.
|
||||
|
||||
Vorschlag: renderBlock() RoadmapBoard::build() als Quelle nehmen lassen und daraus Ueberschriften je Phase erzeugen, mit den Bereichen als Unterpunkte. Dann ist die Phaseneinteilung an genau einer Stelle umgesetzt - und die geratenen Grenzen stehen auch in der Datei als geraten drin, statt dort wie eine Entscheidung auszusehen.
|
||||
|
||||
## H — Verworfen
|
||||
|
||||
Diese Punkte sind geprüft und abgelehnt. **Nicht erneut vorschlagen** — und
|
||||
falls doch, dann nur mit einem Argument, das hier noch nicht steht.
|
||||
|
||||
| | Vorschlag | Warum nicht |
|
||||
|---|---|---|
|
||||
| #111 ❌ **Supervisor-Beobachter im ausgelieferten Programm** (auch regelbasiert, ohne Sprachmodell) | Zu viele Baustellen für den Ertrag. Er hätte in jeder Anwendung Einbau, Rückbau bestehender Vorbereitungen, eigenen Zustand auf Kundenhardware, ein weiteres Token im Binary und eine zweite Fassungspflege gebraucht — und dabei überwiegend das gemeldet, was Error-Reporting und Bugtracker ohnehin abdecken. Der Code war fertig und ist am 29.08.2026 wieder entfernt worden; er liegt in der Historie, falls die Abwägung je anders ausfällt. **Stattdessen:** Error-Reporting und Bugtracker weiterentwickeln, Befunde serverseitig ermitteln |
|
||||
| #112 ❌ Sprachmodell-Supervisor auf der Kundenmaschine | API-Schlüssel läge auf fremder Hardware; Kontext im Speicher fremder Maschinen; kann nicht im Minutentakt hinsehen; hat keinen Verlauf und sieht die Flotte nicht; antwortet zweimal verschieden auf dieselbe Lage |
|
||||
| #113 ❌ Quelltext mit dem Client ausliefern, auch verschlüsselt | Der Client muss entschlüsseln können, also kann es der Kunde auch |
|
||||
| #114 ❌ Deploy-Key oder Repo-Token im Client | Dekompiliert in Minuten |
|
||||
| #115 ❌ Obfuskation als Schutz einplanen | Verzögert, verhindert nicht |
|
||||
| #116 ❌ MCP-Werkzeuge 1:1 aus OpenAPI erzeugen | 24 Endpunkte; Werkzeugdefinitionen liegen dauerhaft im Kontextfenster. Ein aufgeblähter Adapter wäre schlechter als die Prompt-Vorlage, die er ersetzt |
|
||||
| #117 ❌ `publish`, `provision`, `deactivate` als MCP-Werkzeug | Ein Release gehört in die CI, nicht an ein Werkzeug, das ein Modell im Zweifel „hilfreich" auslöst |
|
||||
| #118 ❌ Befunde puffern, wenn das DC ausfällt | Eine wachsende Warteschlange in einer ohnehin auffälligen Anwendung ist genau der Schaden, den der Supervisor verhindern soll |
|
||||
| #119 ❌ Benachrichtigung je Befund | Ein Befund behauptet nicht, dass etwas kaputt ist. Wer bei jeder Auffälligkeit alarmiert, hat nach einer Woche einen Kanal, den niemand mehr liest |
|
||||
| #120 ❌ Lokale Befund-Oberfläche in den Anwendungen | Zweite Wahrheit, die nur den eigenen Ausschnitt kennt. Der Client hält keine Historie vor |
|
||||
| #121 ❌ `enforceProject()` pauschal für `dev_agent` öffnen | Hätte auch `publish` und `setup` in fremden Projekten erlaubt. Die Ausnahme wird je Aufrufstelle gesetzt |
|
||||
| #122 ❌ Feste Schwellwerte je Kennzahl als Hauptweg | Finden nur, woran vorher jemand gedacht hat. Die tragenden Detektoren vergleichen gegen eigenen Verlauf, Vorversion oder Flotte |
|
||||
### [H] #111 — Supervisor-Beobachter im ausgelieferten Programm (auch regelbasiert, ohne Sprachmodell) ❌
|
||||
|
||||
Zu viele Baustellen für den Ertrag. Er hätte in jeder Anwendung Einbau, Rückbau bestehender Vorbereitungen, eigenen Zustand auf Kundenhardware, ein weiteres Token im Binary und eine zweite Fassungspflege gebraucht — und dabei überwiegend das gemeldet, was Error-Reporting und Bugtracker ohnehin abdecken. Der Code war fertig und ist am 29.08.2026 wieder entfernt worden; er liegt in der Historie, falls die Abwägung je anders ausfällt. **Stattdessen:** Error-Reporting und Bugtracker weiterentwickeln, Befunde serverseitig ermitteln
|
||||
|
||||
### [H] #112 — Sprachmodell-Supervisor auf der Kundenmaschine ❌
|
||||
|
||||
API-Schlüssel läge auf fremder Hardware; Kontext im Speicher fremder Maschinen; kann nicht im Minutentakt hinsehen; hat keinen Verlauf und sieht die Flotte nicht; antwortet zweimal verschieden auf dieselbe Lage
|
||||
|
||||
### [H] #113 — Quelltext mit dem Client ausliefern, auch verschlüsselt ❌
|
||||
|
||||
Der Client muss entschlüsseln können, also kann es der Kunde auch
|
||||
|
||||
### [H] #114 — Deploy-Key oder Repo-Token im Client ❌
|
||||
|
||||
Dekompiliert in Minuten
|
||||
|
||||
### [H] #115 — Obfuskation als Schutz einplanen ❌
|
||||
|
||||
Verzögert, verhindert nicht
|
||||
|
||||
### [H] #116 — MCP-Werkzeuge 1:1 aus OpenAPI erzeugen ❌
|
||||
|
||||
24 Endpunkte; Werkzeugdefinitionen liegen dauerhaft im Kontextfenster. Ein aufgeblähter Adapter wäre schlechter als die Prompt-Vorlage, die er ersetzt
|
||||
|
||||
### [H] #117 — publish, provision, deactivate als MCP-Werkzeug ❌
|
||||
|
||||
Ein Release gehört in die CI, nicht an ein Werkzeug, das ein Modell im Zweifel „hilfreich" auslöst
|
||||
|
||||
### [H] #118 — Befunde puffern, wenn das DC ausfällt ❌
|
||||
|
||||
Eine wachsende Warteschlange in einer ohnehin auffälligen Anwendung ist genau der Schaden, den der Supervisor verhindern soll
|
||||
|
||||
### [H] #119 — Benachrichtigung je Befund ❌
|
||||
|
||||
Ein Befund behauptet nicht, dass etwas kaputt ist. Wer bei jeder Auffälligkeit alarmiert, hat nach einer Woche einen Kanal, den niemand mehr liest
|
||||
|
||||
### [H] #120 — Lokale Befund-Oberfläche in den Anwendungen ❌
|
||||
|
||||
Zweite Wahrheit, die nur den eigenen Ausschnitt kennt. Der Client hält keine Historie vor
|
||||
|
||||
### [H] #121 — enforceProject() pauschal für dev_agent öffnen ❌
|
||||
|
||||
Hätte auch `publish` und `setup` in fremden Projekten erlaubt. Die Ausnahme wird je Aufrufstelle gesetzt
|
||||
|
||||
### [H] #122 — Feste Schwellwerte je Kennzahl als Hauptweg ❌
|
||||
|
||||
Finden nur, woran vorher jemand gedacht hat. Die tragenden Detektoren vergleichen gegen eigenen Verlauf, Vorversion oder Flotte
|
||||
|
||||
<!-- /dc:rationale -->
|
||||
|
||||
---
|
||||
|
||||
## Pflege dieser Datei
|
||||
|
||||
Die Umstellung ist vollzogen (#107, 29.08.2026). Es gilt die Arbeitsteilung
|
||||
aus Abschnitt I:
|
||||
Seit 2.9.1 hat diese Datei **keinen** von Hand gepflegten Inhaltsteil mehr.
|
||||
Die Arbeitsteilung lautet:
|
||||
|
||||
| | Wahrheit | Die andere Seite hält |
|
||||
| | Wahrheit | Diese Datei hält |
|
||||
|---|---|---|
|
||||
| Status, Reihenfolge, Zielfassung, Bearbeiter | **Deploymentcenter** | den erzeugten Block zwischen `<!-- dc:roadmap -->`-Markern |
|
||||
| Begründung, Auslöser, Verworfenes | **diese Datei**, außerhalb der Marker | den Verweis `doc_anchor` am Item |
|
||||
| Begründung, Auslöser, Verworfenes | **Deploymentcenter**, Feld `rationale` | den erzeugten Block zwischen `<!-- dc:rationale -->`-Markern |
|
||||
| Einleitung, dieser Abschnitt | **diese Datei** | sich selbst, außerhalb aller Marker |
|
||||
|
||||
- Ein Vorhaben entsteht **immer im DC**, nie hier. `dc_report` legt es an,
|
||||
`dc_roadmap_set` hebt es auf die Roadmap, dann erst kommt die Prosa dazu.
|
||||
`dc_roadmap_set` hebt es auf die Roadmap und trägt auch die Begründung ein.
|
||||
So kann ein Punkt, der nur in einer Datei steht und den niemand angeht, gar
|
||||
nicht erst entstehen.
|
||||
- Zwischen den Markern **nichts von Hand ändern**. `php cli/roadmap.php --write`
|
||||
überschreibt es beim nächsten Lauf.
|
||||
- Vor dem Ausliefern `php cli/roadmap.php --check --project=deploymentcenter`.
|
||||
Rückgabewert 1 heißt: Ein Absatz verweist auf ein Item, das es nicht gibt.
|
||||
- 🧊 und ❌ behalten ihren Absatz hier. Der Statuswert allein sagt nicht, warum —
|
||||
und ohne das Warum schlägt es der nächste Agent wieder vor.
|
||||
- **Geschrieben wird im Deploymentcenter**, am bequemsten in der Roadmap-Tafel:
|
||||
Kärtchen anklicken, Begründung ins Seitenpanel tippen, speichern. Danach
|
||||
`python scripts/dc.py roadmap-write`, damit diese Datei nachzieht.
|
||||
- Zwischen den Markern **nichts von Hand ändern** — es wird beim nächsten Lauf
|
||||
überschrieben.
|
||||
- 🧊 und ❌ behalten ihre Begründung. Der Statuswert allein sagt nicht, warum —
|
||||
und ohne das Warum schlägt es der nächste Agent wieder vor. Sie steht jetzt
|
||||
am Item selbst und ist damit auch für Agenten außerhalb dieses Repositorys
|
||||
lesbar; das war der Anlass der Umstellung.
|
||||
- `python scripts/dc.py roadmap-import` ist der Weg **herauf** und war einmalig
|
||||
gedacht. Er überschreibt nichts, was im DC schon steht, außer mit
|
||||
`--overwrite`.
|
||||
- Ausführliche Herleitungen gehören ins Archiv, nicht hierher. Diese Datei soll
|
||||
lesbar bleiben.
|
||||
- **Keine erfundenen Item-Nummern im Fließtext.** Bei der Übernahme stand im
|
||||
|
||||
@@ -61,7 +61,7 @@ const SERVER_NAME = 'deploymentcenter';
|
||||
//
|
||||
// 1.2.0: fields=compact|full fuer dc_roadmap (Vorgabe compact) und dc_search
|
||||
// (Vorgabe full). Neue Faehigkeit, abwaertsvertraeglich.
|
||||
const SERVER_VERSION = '1.2.0';
|
||||
const SERVER_VERSION = '1.3.0';
|
||||
|
||||
// Protokollfassungen, die dieser Adapter tatsaechlich bedient - neueste zuerst.
|
||||
//
|
||||
@@ -405,10 +405,15 @@ const TOOLS = [
|
||||
+ 'Naechstes dran" und "arbeitet schon jemand daran" - beides, bevor man anfaengt. '
|
||||
+ 'Der Zustand steht hier, nicht in einer Markdown-Datei; eine Datei im Repository '
|
||||
+ 'zeigt nur eine erzeugte Kopie, die aelter sein kann.\n\n'
|
||||
+ 'Was hier NICHT steht, ist die Begruendung: warum etwas so entschieden, '
|
||||
+ 'zurueckgestellt oder verworfen wurde. Die steht in der Projektdatei, auf die '
|
||||
+ 'doc_anchor verweist. Bevor ein zurueckgestelltes oder verworfenes Vorhaben '
|
||||
+ 'erneut vorgeschlagen wird, gehoert dieser Absatz gelesen.',
|
||||
+ 'Die Begruendung - warum etwas so entschieden, zurueckgestellt oder verworfen '
|
||||
+ 'wurde - steht seit 2.9.1 ebenfalls hier, im Feld rationale. Sie kommt nicht in '
|
||||
+ 'dieser Uebersicht mit, weil sie lang ist; dc_get_item liefert sie vollstaendig. '
|
||||
+ 'Bevor ein zurueckgestelltes oder verworfenes Vorhaben erneut vorgeschlagen wird, '
|
||||
+ 'gehoert sie gelesen.\n\n'
|
||||
+ 'Vorher stand sie ausschliesslich in einer Markdown-Datei im Repository des '
|
||||
+ 'Projekts, auf die doc_anchor verwies. Diese Anweisung war damit fuer jeden Agenten '
|
||||
+ 'ausserhalb genau dieses Repositorys nicht befolgbar - der Absatz lag auf einer '
|
||||
+ 'fremden Maschine. Die Datei wird jetzt aus diesen Feldern erzeugt.',
|
||||
annotations: { readOnlyHint: true },
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
@@ -472,9 +477,17 @@ const TOOLS = [
|
||||
type: 'string',
|
||||
description: 'Pflicht bei status="deferred": der Anlass, an dem es wieder ansteht.',
|
||||
},
|
||||
rationale: {
|
||||
type: 'string',
|
||||
description: 'Die Begruendung selbst: warum so und nicht anders, was dagegen '
|
||||
+ 'sprach, woran man spaeter erkennt, dass es richtig war. Markdown. Ersetzt '
|
||||
+ 'den bisherigen Absatz in der Projektdatei - die wird daraus erzeugt.',
|
||||
},
|
||||
doc_anchor: {
|
||||
type: 'string',
|
||||
description: 'Wo steht die Begruendung? z. B. "docs/ROADMAP.md#142-symbol-upload".',
|
||||
description: 'Historisch: der Verweis auf den Absatz in der Projektdatei. Seit '
|
||||
+ '2.9.1 steht die Begruendung in rationale; dieses Feld bleibt nur fuer alte '
|
||||
+ 'Verweise erhalten.',
|
||||
},
|
||||
},
|
||||
required: ['id'],
|
||||
|
||||
@@ -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');
|
||||
|
||||
+122
@@ -13,6 +13,7 @@ import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
DEFAULT_BASE_URL = "https://dc.mhdf.de"
|
||||
@@ -368,6 +369,105 @@ def cmd_roadmap(client: DcClient, args):
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_roadmap_import(client: DcClient, args):
|
||||
"""
|
||||
Die Begruendungsprosa einer Roadmap-Datei ins Deploymentcenter holen.
|
||||
|
||||
Einmalig gedacht. Bis 2.9.0 stand die Begruendung nur in der Datei - die
|
||||
Roadmap-Tafel konnte sie nicht zeigen, und ein Agent in einem anderen
|
||||
Projekt kam gar nicht an sie heran, obwohl die Werkzeugbeschreibung ihn
|
||||
anwies, sie zu lesen.
|
||||
"""
|
||||
pfad = Path(args.file)
|
||||
if not pfad.is_file():
|
||||
print(f"{Colors.RED}FEHLER:{Colors.RESET} Datei nicht gefunden: {pfad}")
|
||||
return 1
|
||||
|
||||
inhalt = pfad.read_text(encoding="utf-8")
|
||||
|
||||
url = "/api/bugtracker/v1/roadmap?" + urllib.parse.urlencode({"action": "import_rationale"})
|
||||
res = client.request(url, method="POST", data={
|
||||
"project_slug": args.project,
|
||||
"content": inhalt,
|
||||
"overwrite": bool(args.overwrite),
|
||||
})
|
||||
|
||||
if res.get("_error") or res.get("status") != "success":
|
||||
print(f"{Colors.RED}Fehler beim Einlesen:{Colors.RESET} {res}")
|
||||
return 1
|
||||
|
||||
bericht = res.get("report", {})
|
||||
items = bericht.get("items", {})
|
||||
epics = bericht.get("epics", {})
|
||||
|
||||
print(f"{Colors.BOLD}{res.get('message', '')}{Colors.RESET}")
|
||||
print(f" Vorhaben : {items.get('gespeichert', 0)} uebernommen, "
|
||||
f"{items.get('unveraendert', 0)} unveraendert")
|
||||
print(f" Bereiche : {epics.get('gespeichert', 0)} uebernommen, "
|
||||
f"{epics.get('unveraendert', 0)} unveraendert")
|
||||
|
||||
uebersprungen = list(items.get("uebersprungen", [])) + list(epics.get("uebersprungen", []))
|
||||
if uebersprungen:
|
||||
print(f"\n{Colors.YELLOW}Uebersprungen:{Colors.RESET}")
|
||||
for zeile in uebersprungen:
|
||||
print(f" - {zeile}")
|
||||
if not args.overwrite and any("overwrite=false" in z for z in uebersprungen):
|
||||
print(f"\n Mit {Colors.BOLD}--overwrite{Colors.RESET} wuerde der Stand der Datei gewinnen.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_roadmap_write(client: DcClient, args):
|
||||
"""
|
||||
Die Roadmap-Datei aus dem Deploymentcenter erzeugen.
|
||||
|
||||
Beide erzeugten Bloecke werden eingesetzt: der Zustand zwischen den
|
||||
dc:roadmap-Markern und die Begruendungen zwischen den dc:rationale-
|
||||
Markern. Was ausserhalb steht, bleibt unberuehrt.
|
||||
|
||||
Das Einsetzen macht der Server - dieselbe Markersuche zweimal umzusetzen
|
||||
hiesse, sie beim zweiten Mal anders umzusetzen.
|
||||
"""
|
||||
pfad = Path(args.file)
|
||||
if not pfad.is_file():
|
||||
print(f"{Colors.RED}FEHLER:{Colors.RESET} Datei nicht gefunden: {pfad}")
|
||||
return 1
|
||||
|
||||
vorher = pfad.read_text(encoding="utf-8")
|
||||
|
||||
url = "/api/bugtracker/v1/roadmap?" + urllib.parse.urlencode({"action": "render_file"})
|
||||
res = client.request(url, method="POST", data={
|
||||
"project_slug": args.project,
|
||||
"content": vorher,
|
||||
})
|
||||
|
||||
if res.get("_error") or res.get("status") != "success":
|
||||
print(f"{Colors.RED}Fehler beim Erzeugen:{Colors.RESET} {res}")
|
||||
return 1
|
||||
|
||||
nachher = res.get("markdown", "")
|
||||
|
||||
if not res.get("changed"):
|
||||
print(f"{Colors.GREEN}Unveraendert{Colors.RESET} - die Datei ist auf dem Stand des Deploymentcenters.")
|
||||
return 0
|
||||
|
||||
if args.dry_run:
|
||||
alt, neu = len(vorher.splitlines()), len(nachher.splitlines())
|
||||
print(f"{Colors.YELLOW}Probelauf:{Colors.RESET} {pfad} wuerde sich aendern "
|
||||
f"({alt} -> {neu} Zeilen). Nichts geschrieben.")
|
||||
return 0
|
||||
|
||||
# Zeilenenden der vorhandenen Datei beibehalten - sonst steht nach dem
|
||||
# ersten Lauf die ganze Datei als Aenderung im Diff.
|
||||
if "\r\n" in vorher:
|
||||
nachher = nachher.replace("\r\n", "\n").replace("\n", "\r\n")
|
||||
|
||||
pfad.write_text(nachher, encoding="utf-8", newline="")
|
||||
print(f"{Colors.GREEN}Geschrieben:{Colors.RESET} {pfad} "
|
||||
f"({len(nachher.splitlines())} Zeilen)")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_projects(client: DcClient, args):
|
||||
res = client.request("/api/bugtracker/v1/projects")
|
||||
if res.get("_error"):
|
||||
@@ -453,6 +553,24 @@ def main():
|
||||
p_road.add_argument("--action", choices=["list", "next", "render", "stats"], default="list", help="Roadmap-Aktion")
|
||||
p_road.add_argument("--limit", "-n", type=int, default=30, help="Anzahl Einträge")
|
||||
|
||||
# roadmap-import: Begründungen aus der Datei ins Deploymentcenter holen
|
||||
p_imp = subparsers.add_parser(
|
||||
"roadmap-import",
|
||||
help="Begründungsprosa aus einer Roadmap-Datei ins Deploymentcenter einlesen (einmalig)")
|
||||
p_imp.add_argument("--project", "-p", default=DEFAULT_PROJECT, help="Projekt-Slug")
|
||||
p_imp.add_argument("--file", "-f", default="docs/ROADMAP.md", help="Pfad zur Roadmap-Datei")
|
||||
p_imp.add_argument("--overwrite", action="store_true",
|
||||
help="Bereits hinterlegte Begründungen durch den Stand der Datei ersetzen")
|
||||
|
||||
# roadmap-write: Datei aus dem Deploymentcenter erzeugen
|
||||
p_wr = subparsers.add_parser(
|
||||
"roadmap-write",
|
||||
help="Roadmap-Datei aus dem Deploymentcenter erzeugen (Zustand + Begründungen)")
|
||||
p_wr.add_argument("--project", "-p", default=DEFAULT_PROJECT, help="Projekt-Slug")
|
||||
p_wr.add_argument("--file", "-f", default="docs/ROADMAP.md", help="Pfad zur Roadmap-Datei")
|
||||
p_wr.add_argument("--dry-run", "-n", action="store_true",
|
||||
help="Nur anzeigen, ob sich etwas ändern würde")
|
||||
|
||||
# projects
|
||||
subparsers.add_parser("projects", help="Alle Projekte auflisten")
|
||||
|
||||
@@ -487,6 +605,10 @@ def main():
|
||||
sys.exit(cmd_report(client, args))
|
||||
elif args.command == "roadmap":
|
||||
sys.exit(cmd_roadmap(client, args))
|
||||
elif args.command == "roadmap-import":
|
||||
sys.exit(cmd_roadmap_import(client, args))
|
||||
elif args.command == "roadmap-write":
|
||||
sys.exit(cmd_roadmap_write(client, args))
|
||||
elif args.command == "projects":
|
||||
sys.exit(cmd_projects(client, args))
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Migration 020: Die Begruendung zieht in die Datenbank
|
||||
--
|
||||
-- Additive Migration. Der Migrator toleriert 1050/1060/1061/1062/1091 ("existiert bereits").
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Warum ueberhaupt
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Bisher galt eine Zweiteilung: Der Zustand eines Vorhabens stand hier, die
|
||||
-- Begruendung in einer Markdown-Datei im Repository des Projekts, und
|
||||
-- doc_anchor verwies darauf. Die Beschreibung der uebernommenen Eintraege
|
||||
-- sagt es woertlich: "Die Begruendung steht weiterhin in der Datei - hier
|
||||
-- steht nur der Zustand."
|
||||
--
|
||||
-- Das hatte zwei Folgen, die erst im Betrieb sichtbar wurden:
|
||||
--
|
||||
-- 1. Die Roadmap-Tafel kann die Begruendung nicht zeigen. Sie sieht nur
|
||||
-- die Datenbank; die Datei liegt auf einem Entwicklungsrechner.
|
||||
--
|
||||
-- 2. Schwerer wiegt: Die Beschreibung des Werkzeugs dc_roadmap weist
|
||||
-- Agenten an, vor dem erneuten Vorschlagen eines zurueckgestellten
|
||||
-- Vorhabens "diesen Absatz" zu lesen. Ein Agent im Projekt polytrader
|
||||
-- kann den Absatz nicht lesen - er liegt in einem fremden Repository.
|
||||
-- Die Anweisung war nicht befolgbar.
|
||||
--
|
||||
-- Ab hier fuehrt das Deploymentcenter die Begruendung, und die Datei wird
|
||||
-- daraus erzeugt (zweites Markerpaar dc:rationale). Damit gibt es einen Ort
|
||||
-- statt zweier, und der Ort ist der, den alle erreichen.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Ein eigenes Feld, nicht description
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- description und rationale sind nicht dasselbe:
|
||||
-- description Was gemeldet wurde. Kommt vom Melder, oft von einer fremden
|
||||
-- Maschine, und wird nicht umgeschrieben.
|
||||
-- rationale Warum wir uns so entschieden haben. Kommt von uns.
|
||||
--
|
||||
-- Zusammengelegt haette jede redaktionelle Ueberarbeitung der Begruendung
|
||||
-- den urspruenglichen Wortlaut der Meldung ueberschrieben - und damit die
|
||||
-- Grundlage, auf der entschieden wurde.
|
||||
--
|
||||
-- MEDIUMTEXT und nicht TEXT: Die laengsten vorhandenen Abschnitte liegen bei
|
||||
-- rund 2 KB, aber eine Begruendung darf wachsen, und die 64-KB-Grenze von
|
||||
-- TEXT ist keine, die man beim Schreiben im Kopf haben moechte.
|
||||
ALTER TABLE bugtracker_items
|
||||
ADD COLUMN rationale MEDIUMTEXT NULL AFTER doc_anchor;
|
||||
|
||||
-- Wann die Begruendung zuletzt angefasst wurde. Getrennt von updated_at,
|
||||
-- weil dieses bei jeder Statusaenderung hochgeht und dann nicht mehr
|
||||
-- beantwortet, ob die Begruendung noch dem entspricht, was heute gilt.
|
||||
ALTER TABLE bugtracker_items
|
||||
ADD COLUMN rationale_updated_at DATETIME NULL AFTER rationale;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Auch Bereiche haben eine Begruendung
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Beim Einlesen der vorhandenen Datei stellte sich heraus, dass ein guter
|
||||
-- Teil der Prosa gar nicht an einem Vorhaben haengt, sondern am Bereich:
|
||||
-- neun Abschnitte der Form "## A - Supervisor und Befunde" mit mehreren
|
||||
-- Absaetzen, in denen die Grundentscheidung des jeweiligen Bereichs steht.
|
||||
--
|
||||
-- roadmap_epics.summary gibt es bereits, aber das ist eine Zeile fuer die
|
||||
-- Uebersichtstabelle. Diese Abschnitte sind bis zu dreissig Zeilen lang.
|
||||
-- Ohne ein eigenes Feld haette der erzeugte Block sie beim ersten Lauf
|
||||
-- ueberschrieben - also genau den Text, der am schwersten wieder zu
|
||||
-- beschaffen waere.
|
||||
ALTER TABLE roadmap_epics
|
||||
ADD COLUMN rationale MEDIUMTEXT NULL AFTER summary;
|
||||
|
||||
ALTER TABLE roadmap_epics
|
||||
ADD COLUMN rationale_updated_at DATETIME NULL AFTER rationale;
|
||||
@@ -1837,6 +1837,35 @@ final class BugRepo
|
||||
}
|
||||
}
|
||||
|
||||
// Die Begruendung laeuft nicht ueber die Schleife oben: Sie ist bis zu
|
||||
// einigen tausend Zeichen lang, und der Verlauf soll festhalten, DASS
|
||||
// sie sich geaendert hat - nicht den ganzen Absatz ein zweites Mal
|
||||
// tragen. Nach fuenf Ueberarbeitungen stuende er sonst sechsmal am
|
||||
// Item und der Verlauf waere unlesbar.
|
||||
if (array_key_exists('rationale', $updates)) {
|
||||
$value = self::text($updates['rationale']);
|
||||
$fields[] = 'rationale = :rationale';
|
||||
$params[':rationale'] = $value;
|
||||
$fields[] = 'rationale_updated_at = UTC_TIMESTAMP()';
|
||||
|
||||
$vorher = (string)($existing['rationale'] ?? '');
|
||||
$nachher = (string)($value ?? '');
|
||||
|
||||
if ($vorher !== $nachher) {
|
||||
if ($nachher === '') {
|
||||
$changes[] = 'Begruendung geleert';
|
||||
} elseif ($vorher === '') {
|
||||
$changes[] = sprintf('Begruendung hinterlegt (%d Zeichen)', mb_strlen($nachher));
|
||||
} else {
|
||||
$changes[] = sprintf(
|
||||
'Begruendung ueberarbeitet (%d -> %d Zeichen)',
|
||||
mb_strlen($vorher),
|
||||
mb_strlen($nachher)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('line_no', $updates)) {
|
||||
$fields[] = 'line_no = :line_no';
|
||||
$params[':line_no'] = is_numeric($updates['line_no']) ? (int)$updates['line_no'] : null;
|
||||
|
||||
@@ -108,6 +108,27 @@ final class Roadmap
|
||||
public const BLOCK_START = '<!-- dc:roadmap';
|
||||
public const BLOCK_END = '<!-- /dc:roadmap -->';
|
||||
|
||||
/**
|
||||
* Marker des erzeugten Begruendungsteils.
|
||||
*
|
||||
* Ein ZWEITES Paar und nicht eine Erweiterung des ersten: Zwischen den
|
||||
* beiden Bloecken steht in jeder gewachsenen Roadmap-Datei etwas von
|
||||
* Hand Geschriebenes - eine Einleitung, eine Leitlinie, eine Erklaerung
|
||||
* der Statuszeichen. Wuerde ein einziger Block beides umfassen, ginge
|
||||
* dieser Text beim naechsten Erzeugen verloren.
|
||||
*/
|
||||
public const RATIONALE_START = '<!-- dc:rationale';
|
||||
public const RATIONALE_END = '<!-- /dc:rationale -->';
|
||||
|
||||
/**
|
||||
* Erkennt eine Begruendungsueberschrift: "### [A] #65 — Symbol-Upload".
|
||||
*
|
||||
* Der Bereich in Klammern ist wahlfrei, die Nummer nicht - sie ist das
|
||||
* Band zum Item. Ueberschriften ohne Nummer gehoeren zu keinem Vorhaben
|
||||
* und werden beim Einlesen uebergangen.
|
||||
*/
|
||||
private const RATIONALE_HEADING = '/^(#{2,4})\s*(?:\[[^\]]*\]\s*)?#(\d{1,9})\b[^\n]*$/m';
|
||||
|
||||
/**
|
||||
* Erkennt einen Verweis auf ein Item im Fliesstext: "#142".
|
||||
*
|
||||
@@ -768,9 +789,9 @@ final class Roadmap
|
||||
);
|
||||
$lines[] = '';
|
||||
$lines[] = '<!-- ERZEUGT - nicht von Hand aendern. Dieser Block wird bei jedem Lauf von';
|
||||
$lines[] = ' `php cli/roadmap.php --write` ueberschrieben. Der Zustand wird im';
|
||||
$lines[] = ' Deploymentcenter gepflegt, nicht hier. Prosa gehoert AUSSERHALB der';
|
||||
$lines[] = ' Marker - dort bleibt sie unberuehrt. -->';
|
||||
$lines[] = ' `python scripts/dc.py roadmap-write` ueberschrieben. Der Zustand wird';
|
||||
$lines[] = ' im Deploymentcenter gepflegt, nicht hier - und seit 2.9.1 auch die';
|
||||
$lines[] = ' Begruendungen, die im Block dc:rationale weiter unten stehen. -->';
|
||||
$lines[] = '';
|
||||
|
||||
$next = $this->nextUp($projectSlug, 4);
|
||||
@@ -885,21 +906,497 @@ final class Roadmap
|
||||
*/
|
||||
private static function findeBlock(string $content): array
|
||||
{
|
||||
// Zeilenanfang, Marker, dann Leerraum und "stand=" - so schreibt ihn
|
||||
// renderBlock() und nur so.
|
||||
if (!preg_match('/^' . preg_quote(self::BLOCK_START, '/') . '\s+stand=/m',
|
||||
return self::findeMarker($content, self::BLOCK_START, self::BLOCK_END);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie findeBlock(), aber fuer ein beliebiges Markerpaar.
|
||||
*
|
||||
* Seit es zwei erzeugte Bloecke gibt - Zustand und Begruendungen -
|
||||
* braucht die Suche die Marker als Parameter. Die Vorsichtsmassnahme
|
||||
* bleibt dieselbe: gesucht wird nur ein Marker am Zeilenanfang, dem
|
||||
* "stand=" folgt. Eine Erwaehnung im Fliesstext tut das nicht.
|
||||
*
|
||||
* @return array{0:?int,1:?int} Anfang und Ende (hinter dem Endmarker)
|
||||
*/
|
||||
private static function findeMarker(string $content, string $startMarker, string $endMarker): array
|
||||
{
|
||||
if (!preg_match('/^' . preg_quote($startMarker, '/') . '\s+stand=/m',
|
||||
$content, $treffer, PREG_OFFSET_CAPTURE)) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$start = (int)$treffer[0][1];
|
||||
$end = strpos($content, self::BLOCK_END, $start);
|
||||
$end = strpos($content, $endMarker, $start);
|
||||
|
||||
if ($end === false) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [$start, $end + strlen(self::BLOCK_END)];
|
||||
return [$start, $end + strlen($endMarker)];
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Begruendungen
|
||||
// ==================================================================
|
||||
|
||||
/**
|
||||
* Liest die Begruendungsprosa einer Datei ein.
|
||||
*
|
||||
* Einmalig gedacht: Die Datei war bis 2.9.1 der Ort, an dem die
|
||||
* Begruendung stand. Ab hier fuehrt sie das Deploymentcenter, und dieser
|
||||
* Aufruf holt den Bestand herauf.
|
||||
*
|
||||
* Erkannt werden drei Formen:
|
||||
* "## A - Titel" Begruendung eines Bereichs
|
||||
* "### [A] #65 - Titel" Begruendung eines Vorhabens
|
||||
* "| #111 ... | Warum ... |" Tabellenzeile, wie sie der Abschnitt
|
||||
* "Verworfen" benutzt - dort steht die
|
||||
* Begruendung nicht als Absatz, sondern in
|
||||
* einer Zelle. Ohne diese dritte Form bliebe
|
||||
* ausgerechnet das liegen, was am haeufigsten
|
||||
* gebraucht wird: warum etwas abgelehnt
|
||||
* wurde.
|
||||
*
|
||||
* @param bool $overwrite Bereits hinterlegte Begruendungen ersetzen?
|
||||
* @return array<string,mixed> Bericht darueber, was uebernommen wurde
|
||||
*/
|
||||
public function importRationale(string $projectSlug, string $content, bool $overwrite = false): array
|
||||
{
|
||||
$prose = self::withoutBlock($content, self::BLOCK_START, self::BLOCK_END);
|
||||
|
||||
$bericht = [
|
||||
'items' => ['gespeichert' => 0, 'unveraendert' => 0, 'uebersprungen' => []],
|
||||
'epics' => ['gespeichert' => 0, 'unveraendert' => 0, 'uebersprungen' => []],
|
||||
'gefunden' => 0,
|
||||
];
|
||||
|
||||
foreach (self::parseSections($prose) as $abschnitt) {
|
||||
$bericht['gefunden']++;
|
||||
|
||||
if ($abschnitt['kind'] === 'item') {
|
||||
$this->storeItemRationale(
|
||||
$projectSlug, (int)$abschnitt['key'], $abschnitt['text'], $overwrite, $bericht
|
||||
);
|
||||
} else {
|
||||
$this->storeEpicRationale(
|
||||
$projectSlug, (string)$abschnitt['key'], $abschnitt['text'], $overwrite, $bericht
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $bericht;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zerlegt die Prosa in Abschnitte.
|
||||
*
|
||||
* @return list<array{kind:string,key:int|string,text:string}>
|
||||
*/
|
||||
private static function parseSections(string $prose): array
|
||||
{
|
||||
$abschnitte = [];
|
||||
|
||||
// Alle Ueberschriften der Ebenen 2 bis 4 mit ihrer Position. Der
|
||||
// Abschnitt reicht jeweils bis zur naechsten Ueberschrift - egal
|
||||
// welcher Ebene. Nur bis zur naechsten GLEICHER Ebene zu gehen waere
|
||||
// falsch: Die Bereichsprosa steht vor den Vorhaben des Bereichs und
|
||||
// endet an deren erster Ueberschrift.
|
||||
preg_match_all('/^(#{2,4})[ \t]+([^\n]*)$/m', $prose, $treffer, PREG_OFFSET_CAPTURE);
|
||||
|
||||
$anzahl = count($treffer[0]);
|
||||
|
||||
for ($i = 0; $i < $anzahl; $i++) {
|
||||
$zeile = $treffer[0][$i][0];
|
||||
$offset = (int)$treffer[0][$i][1];
|
||||
$titel = trim($treffer[2][$i][0]);
|
||||
|
||||
$ende = $i + 1 < $anzahl ? (int)$treffer[0][$i + 1][1] : strlen($prose);
|
||||
$text = self::ohneTrenner(
|
||||
substr($prose, $offset + strlen($zeile), $ende - $offset - strlen($zeile))
|
||||
);
|
||||
|
||||
// Ueberschrift mit Item-Nummer -> Begruendung eines Vorhabens
|
||||
if (preg_match('/(?:\[[^\]]*\]\s*)?#(\d{1,9})\b/', $titel, $m) === 1) {
|
||||
if ($text !== '') {
|
||||
$abschnitte[] = ['kind' => 'item', 'key' => (int)$m[1], 'text' => $text];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tabellenzeilen tragen je eine Item-Begruendung. Sie werden aus
|
||||
// JEDEM Rumpf gezogen, auch aus dem eines Bereichsabschnitts:
|
||||
// Der Abschnitt "Verworfen" fuehrt seine zwoelf Ablehnungen als
|
||||
// Tabelle, nicht als Absaetze. Blieben sie im Bereichstext
|
||||
// stecken, waere ausgerechnet das nicht am Item auffindbar, was
|
||||
// am haeufigsten gebraucht wird - naemlich warum etwas schon
|
||||
// einmal abgelehnt wurde.
|
||||
$tabelle = self::parseTableRows($text);
|
||||
|
||||
foreach ($tabelle as $id => $grund) {
|
||||
$abschnitte[] = ['kind' => 'item', 'key' => $id, 'text' => $grund];
|
||||
}
|
||||
|
||||
// "## A - Titel" bzw. "## A — Titel" -> Begruendung eines Bereichs.
|
||||
// Der Bereichsschluessel ist kurz und steht vorn; ein Abschnitt
|
||||
// wie "## Begruendungen" oder "## Pflege dieser Datei" faellt
|
||||
// damit heraus, und das ist gewollt - er gehoert zu keinem
|
||||
// Bereich.
|
||||
if (preg_match('/^([A-Za-z0-9]{1,4})\s*[-\x{2013}\x{2014}]\s*\S/u', $titel, $m) === 1) {
|
||||
// Die eben herausgeloesten Zeilen fallen aus dem Bereichstext
|
||||
// heraus - sonst stuenden sie nach dem naechsten Erzeugen
|
||||
// zweimal in der Datei: einmal in der Tabelle des Bereichs
|
||||
// und einmal als Abschnitt je Vorhaben.
|
||||
$rest = $tabelle === [] ? $text : self::withoutTableRows($text);
|
||||
|
||||
if ($rest !== '') {
|
||||
$abschnitte[] = ['kind' => 'epic', 'key' => $m[1], 'text' => $rest];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $abschnitte;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item-Begruendungen aus Tabellenzeilen.
|
||||
*
|
||||
* Erwartet wird die Form
|
||||
* | #111 ❌ **Titel** | Warum nicht |
|
||||
* also: In der ersten Zelle steht die Nummer, in der letzten die
|
||||
* Begruendung. Zeilen ohne Nummer und Trennzeilen werden uebergangen.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private static function parseTableRows(string $text): array
|
||||
{
|
||||
$gefunden = [];
|
||||
|
||||
foreach (preg_split('/\R/', $text) ?: [] as $zeile) {
|
||||
$zeile = trim($zeile);
|
||||
if ($zeile === '' || $zeile[0] !== '|') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$zellen = array_map('trim', explode('|', trim($zeile, '|')));
|
||||
if (count($zellen) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trennzeile einer Markdown-Tabelle
|
||||
if (preg_match('/^:?-{3,}:?$/', $zellen[0]) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/#(\d{1,9})\b/', $zellen[0], $m) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$grund = trim((string)end($zellen));
|
||||
if ($grund !== '') {
|
||||
$gefunden[(int)$m[1]] = $grund;
|
||||
}
|
||||
}
|
||||
|
||||
return $gefunden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt Tabellenzeilen mit Item-Nummer samt Tabellengeruest.
|
||||
*
|
||||
* Bleibt von einer Tabelle nur noch der Kopf uebrig, faellt auch er weg -
|
||||
* eine Ueberschriftszeile ohne Zeilen darunter ist kein Rest, sondern
|
||||
* Bauschutt.
|
||||
*/
|
||||
private static function withoutTableRows(string $text): string
|
||||
{
|
||||
$zeilen = preg_split('/\R/', $text) ?: [];
|
||||
$behalten = [];
|
||||
|
||||
foreach ($zeilen as $zeile) {
|
||||
$roh = trim($zeile);
|
||||
|
||||
if ($roh !== '' && $roh[0] === '|') {
|
||||
$zellen = array_map('trim', explode('|', trim($roh, '|')));
|
||||
$erste = $zellen[0] ?? '';
|
||||
|
||||
// Zeile mit Item-Nummer, Trennzeile oder leerer Kopf - alles
|
||||
// Teil derselben Tabelle.
|
||||
if (preg_match('/#\d{1,9}\b/', $erste) === 1
|
||||
|| preg_match('/^:?-{3,}:?$/', $erste) === 1
|
||||
|| $erste === '') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$behalten[] = $zeile;
|
||||
}
|
||||
|
||||
return self::ohneTrenner(implode("\n", $behalten));
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt waagerechte Trennlinien am Anfang und Ende.
|
||||
*
|
||||
* "---" trennt in einer Markdown-Datei Abschnitte voneinander. Es gehoert
|
||||
* damit zur Gliederung der Datei und nicht zur Begruendung - und die
|
||||
* Gliederung erzeugt kuenftig der Server. Bliebe der Strich am Text
|
||||
* haengen, stuende er nach dem naechsten Lauf mitten im Abschnitt.
|
||||
*/
|
||||
private static function ohneTrenner(string $text): string
|
||||
{
|
||||
$zeilen = preg_split('/\R/', trim($text)) ?: [];
|
||||
|
||||
$istTrenner = static fn (string $z): bool =>
|
||||
preg_match('/^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/', $z) === 1;
|
||||
|
||||
while ($zeilen !== [] && ($istTrenner($zeilen[0]) || trim($zeilen[0]) === '')) {
|
||||
array_shift($zeilen);
|
||||
}
|
||||
|
||||
while ($zeilen !== [] && ($istTrenner(end($zeilen)) || trim((string)end($zeilen)) === '')) {
|
||||
array_pop($zeilen);
|
||||
}
|
||||
|
||||
return implode("\n", $zeilen);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $bericht */
|
||||
private function storeItemRationale(
|
||||
string $projectSlug,
|
||||
int $itemId,
|
||||
string $text,
|
||||
bool $overwrite,
|
||||
array &$bericht
|
||||
): void {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, project_slug, rationale FROM bugtracker_items WHERE id = :id LIMIT 1
|
||||
');
|
||||
$stmt->execute([':id' => $itemId]);
|
||||
$item = $stmt->fetch();
|
||||
|
||||
if (!is_array($item)) {
|
||||
$bericht['items']['uebersprungen'][] = sprintf('#%d gibt es nicht', $itemId);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((string)$item['project_slug'] !== $projectSlug) {
|
||||
$bericht['items']['uebersprungen'][] = sprintf(
|
||||
'#%d gehoert zu "%s"', $itemId, $item['project_slug']
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$vorhanden = (string)($item['rationale'] ?? '');
|
||||
|
||||
if ($vorhanden === $text) {
|
||||
$bericht['items']['unveraendert']++;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($vorhanden !== '' && !$overwrite) {
|
||||
$bericht['items']['uebersprungen'][] = sprintf(
|
||||
'#%d hat schon eine Begruendung (overwrite=false)', $itemId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$upd = $this->db->prepare('
|
||||
UPDATE bugtracker_items
|
||||
SET rationale = :r, rationale_updated_at = UTC_TIMESTAMP()
|
||||
WHERE id = :id
|
||||
');
|
||||
$upd->execute([':r' => $text, ':id' => $itemId]);
|
||||
|
||||
$bericht['items']['gespeichert']++;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $bericht */
|
||||
private function storeEpicRationale(
|
||||
string $projectSlug,
|
||||
string $epic,
|
||||
string $text,
|
||||
bool $overwrite,
|
||||
array &$bericht
|
||||
): void {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT epic, rationale FROM roadmap_epics
|
||||
WHERE project_slug = :slug AND epic = :epic LIMIT 1
|
||||
');
|
||||
$stmt->execute([':slug' => $projectSlug, ':epic' => $epic]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!is_array($row)) {
|
||||
$bericht['epics']['uebersprungen'][] = sprintf('Bereich "%s" ist nicht angelegt', $epic);
|
||||
return;
|
||||
}
|
||||
|
||||
$vorhanden = (string)($row['rationale'] ?? '');
|
||||
|
||||
if ($vorhanden === $text) {
|
||||
$bericht['epics']['unveraendert']++;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($vorhanden !== '' && !$overwrite) {
|
||||
$bericht['epics']['uebersprungen'][] = sprintf(
|
||||
'Bereich "%s" hat schon eine Begruendung (overwrite=false)', $epic
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$upd = $this->db->prepare('
|
||||
UPDATE roadmap_epics
|
||||
SET rationale = :r, rationale_updated_at = UTC_TIMESTAMP()
|
||||
WHERE project_slug = :slug AND epic = :epic
|
||||
');
|
||||
$upd->execute([':r' => $text, ':slug' => $projectSlug, ':epic' => $epic]);
|
||||
|
||||
$bericht['epics']['gespeichert']++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt den Begruendungsteil der Datei.
|
||||
*
|
||||
* Ab 2.9.1 ist das Deploymentcenter fuer die Begruendung massgeblich und
|
||||
* die Datei die Ausgabe. Umgekehrt war es bis dahin - und genau deshalb
|
||||
* konnte die Roadmap-Tafel sie nicht zeigen und ein Agent in einem
|
||||
* anderen Projekt sie nicht lesen.
|
||||
*/
|
||||
public function renderRationaleBlock(string $projectSlug): string
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT epic, title, rationale, sort_order
|
||||
FROM roadmap_epics
|
||||
WHERE project_slug = :slug
|
||||
ORDER BY sort_order ASC, epic ASC
|
||||
');
|
||||
$stmt->execute([':slug' => $projectSlug]);
|
||||
$bereiche = $stmt->fetchAll() ?: [];
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, epic, title, status, rationale
|
||||
FROM bugtracker_items
|
||||
WHERE project_slug = :slug
|
||||
AND rationale IS NOT NULL AND rationale <> ""
|
||||
ORDER BY roadmap_rank IS NULL ASC, roadmap_rank ASC, id ASC
|
||||
');
|
||||
$stmt->execute([':slug' => $projectSlug]);
|
||||
|
||||
$nachBereich = [];
|
||||
$anzahl = 0;
|
||||
foreach ($stmt->fetchAll() ?: [] as $row) {
|
||||
$nachBereich[(string)($row['epic'] ?? '')][] = $row;
|
||||
$anzahl++;
|
||||
}
|
||||
|
||||
$zeilen = [];
|
||||
$zeilen[] = sprintf(
|
||||
'%s stand=%s items=%d projekt=%s -->',
|
||||
self::RATIONALE_START,
|
||||
gmdate('Y-m-d\TH:i\Z'),
|
||||
$anzahl,
|
||||
$projectSlug
|
||||
);
|
||||
$zeilen[] = '';
|
||||
$zeilen[] = '> **Erzeugt — hier nicht von Hand aendern.** Die Begruendungen stehen im';
|
||||
$zeilen[] = '> Deploymentcenter und werden von dort geschrieben: in der Roadmap-Tafel am';
|
||||
$zeilen[] = '> Kaertchen oder ueber `dc_roadmap_set`. Der naechste';
|
||||
$zeilen[] = '> `python scripts/dc.py roadmap write` ueberschreibt diesen Abschnitt.';
|
||||
$zeilen[] = '>';
|
||||
$zeilen[] = '> Der Grund fuer die Umstellung: Solange die Begruendung nur hier stand, war';
|
||||
$zeilen[] = '> sie fuer die Tafel unsichtbar und fuer jeden Agenten ausserhalb dieses';
|
||||
$zeilen[] = '> Repositorys unerreichbar.';
|
||||
$zeilen[] = '';
|
||||
|
||||
foreach ($bereiche as $bereich) {
|
||||
$epic = (string)$bereich['epic'];
|
||||
$items = $nachBereich[$epic] ?? [];
|
||||
$text = trim((string)($bereich['rationale'] ?? ''));
|
||||
|
||||
if ($items === [] && $text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$zeilen[] = sprintf('## %s — %s', $epic, (string)$bereich['title']);
|
||||
$zeilen[] = '';
|
||||
|
||||
if ($text !== '') {
|
||||
$zeilen[] = $text;
|
||||
$zeilen[] = '';
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$zeilen[] = sprintf(
|
||||
'### [%s] #%d — %s %s',
|
||||
$epic,
|
||||
(int)$item['id'],
|
||||
self::inlineText((string)$item['title']),
|
||||
self::MARKERS[(string)$item['status']] ?? ''
|
||||
);
|
||||
$zeilen[] = '';
|
||||
$zeilen[] = trim((string)$item['rationale']);
|
||||
$zeilen[] = '';
|
||||
}
|
||||
|
||||
unset($nachBereich[$epic]);
|
||||
}
|
||||
|
||||
// Begruendungen an Vorhaben ohne angelegten Bereich. Sie gehen nicht
|
||||
// verloren, sondern bekommen einen eigenen Abschnitt - sonst waere
|
||||
// die Datei nach einem Lauf stillschweigend kuerzer als vorher.
|
||||
$rest = [];
|
||||
foreach ($nachBereich as $items) {
|
||||
foreach ($items as $item) {
|
||||
$rest[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if ($rest !== []) {
|
||||
$zeilen[] = '## Ohne Themenbereich';
|
||||
$zeilen[] = '';
|
||||
|
||||
foreach ($rest as $item) {
|
||||
$zeilen[] = sprintf(
|
||||
'### #%d — %s %s',
|
||||
(int)$item['id'],
|
||||
self::inlineText((string)$item['title']),
|
||||
self::MARKERS[(string)$item['status']] ?? ''
|
||||
);
|
||||
$zeilen[] = '';
|
||||
$zeilen[] = trim((string)$item['rationale']);
|
||||
$zeilen[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$zeilen[] = self::RATIONALE_END;
|
||||
|
||||
return rtrim(implode("\n", $zeilen)) . "\n";
|
||||
}
|
||||
|
||||
/** Setzt den erzeugten Begruendungsteil in eine Datei ein. */
|
||||
public static function spliceRationaleBlock(string $content, string $block): string
|
||||
{
|
||||
[$start, $end] = self::findeMarker($content, self::RATIONALE_START, self::RATIONALE_END);
|
||||
|
||||
if ($start === null || $end === null) {
|
||||
$separator = rtrim($content) === '' ? '' : "\n\n";
|
||||
return rtrim($content) . $separator . $block;
|
||||
}
|
||||
|
||||
return substr($content, 0, $start) . rtrim($block) . substr($content, $end);
|
||||
}
|
||||
|
||||
/** Entfernt einen erzeugten Block aus dem Text. */
|
||||
private static function withoutBlock(string $content, string $startMarker, string $endMarker): string
|
||||
{
|
||||
[$start, $end] = self::findeMarker($content, $startMarker, $endMarker);
|
||||
|
||||
if ($start === null || $end === null) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return substr($content, 0, $start) . substr($content, $end);
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
@@ -150,7 +150,12 @@ final class RoadmapBoard
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, project_slug, type, title, status, severity, epic, roadmap_rank,
|
||||
target_build, defer_trigger, doc_anchor, claimed_by, lease_until,
|
||||
occurrence_count, created_at, updated_at
|
||||
occurrence_count, created_at, updated_at,
|
||||
-- Nur ob, nicht was: Die Begruendungen sind bis zu einigen
|
||||
-- tausend Zeichen lang, und die Tafel zeigt hundert
|
||||
-- Kaertchen auf einmal. Den Volltext holt das Seitenpanel
|
||||
-- fuer das eine Vorhaben, das angeklickt wurde.
|
||||
(rationale IS NOT NULL AND rationale <> "") AS has_rationale
|
||||
FROM bugtracker_items
|
||||
WHERE project_slug = :slug
|
||||
ORDER BY roadmap_rank IS NULL ASC, roadmap_rank ASC, id ASC
|
||||
@@ -526,6 +531,12 @@ final class RoadmapBoard
|
||||
'lease_until' => $claimed && is_string($leaseUntil) ? $leaseUntil : null,
|
||||
'lease_expired' => $leaseExpired,
|
||||
'is_new' => $isNew,
|
||||
// Fehlt die Begruendung, soll man das sehen. Nicht als Vorwurf,
|
||||
// sondern weil "keine hinterlegt" und "noch nicht geladen" sonst
|
||||
// gleich aussehen - und weil eine fehlende Begruendung genau die
|
||||
// Stelle ist, an der ein Vorhaben spaeter neu verhandelt wird,
|
||||
// ohne dass jemand weiss, was beim letzten Mal galt.
|
||||
'has_rationale' => (int)($row['has_rationale'] ?? 0) === 1,
|
||||
'updated_at' => (string)$row['updated_at'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user