From c9bad697e5a6ad760cc3771471f762b28c35c59e Mon Sep 17 00:00:00 2001 From: Deploymentcenter Bot Date: Sun, 30 Aug 2026 00:10:32 +0200 Subject: [PATCH] feat(roadmap): Roadmap des Deploymentcenters uebernommen (I9) 61 Eintraege aus docs/ROADMAP.md stehen jetzt als Items im Deploymentcenter: 17 offen, 24 erledigt, 7 zurueckgestellt, 13 verworfen. Keine Waisen, jedes Item mit doc_anchor. Der Abgleich laeuft ohne Befund durch. Die Datei behaelt, was eine Tabellenzelle nicht traegt: die Begruendungen. Der Zustand steht zwischen dc:roadmap-Markern und wird erzeugt. - scripts/roadmap_import.py: wertet die Datei aus und uebertraegt sie. Wiederholbar ueber client_ref, legt fehlende Themenbereiche an, wartet bei Drosselung. Die Prioritaet liest es aus dem Abschnitt "Als Naechstes" - solange die Datei die Quelle ist, soll auch die Prioritaet von dort kommen. - docs/ROADMAP.md: Statustabellen raus, erzeugter Block rein, 21 Ueberschriften und 14 Querverweise auf Item-Nummern umgestellt, Abschnitt H mit Item-Nummern versehen. WAS DIE UEBERNAHME ZUTAGE FOERDERTE - G8 war zweimal vergeben: einmal offen, einmal erledigt. Genau die Kollision handvergebener Kennungen, gegen die der Entwurf argumentiert. Beide sind eigene Items. - Bereich H fehlte in roadmap_epics - Migration 016 hatte nur A-G und I gesetzt. Die Waisenerkennung meldete es. Das Importskript legt fehlende Bereiche jetzt an, ueberschreibt aber keine vorhandenen: Ein Uebernahmeskript darf anlegen, kuratierte Angaben nicht ueberschreiben. - "Item #42" stand als erfundenes Beispiel im Rueckkanal-Absatz - und #42 gibt es wirklich, in predictalytics. Der Abgleich hat es gemeldet, ein Mensch haette es nie bemerkt. - Vier Marker waren veraltet (E1, E2, I8, D1). BEHOBEN: spliceBlock() zerstoerte Dateien, die den Marker erwaehnen strpos() nahm die erste Fundstelle von "`-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. + +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. + +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 +derselben Folge: Ein Punkt galt als offen, obwohl der Code ihn längst erledigt +hatte. Seither hat kein Feld zwei Pfleger. Die abgelösten Entwürfe liegen unter [archiv/](./archiv/) — sie enthalten die ausführliche Begründung, aber keinen gültigen Stand mehr. @@ -25,33 +36,135 @@ ausführliche Begründung, aber keinen gültigen Stand mehr. --- -## Als Nächstes + -1. **[I8] Migration 016 ausführen** — ohne sie steht das Roadmap-Modul, das - 2.7.0 mitbringt, vor leeren Spalten -2. **[I9] Diese Datei selbst übernehmen** — das Verfahren einmal hier - durchlaufen, bevor die anderen Projekte es aus der Anleitung bekommen -3. **[B1] Befunde in der Oberfläche** — sie sind derzeit nur über API und MCP - erreichbar. Ohne Sicht auf die Befunde ist alles Weitere blind -4. **[G5] `dc-testkit findings`** — dasselbe von der Test-VM aus, und - Voraussetzung für G8 -5. **[G8] Testartefakt oder echter Befund?** — die Frage, an der sich - entscheidet, ob Befunde aus Testläufen etwas wert sind -6. **[A4] Symbol-Upload und `dc_symbolicate`** — Stacktraces aus gestrippten - Binaries auflösen, ohne dass Quelltext das Haus verlässt + -Davor stehen die Betriebsaufgaben ⚙️ aus Abschnitt E — ohne sie laufen zwei -der fünf serverseitigen Detektoren gar nicht. +### Als Naechstes -**Diese Liste ist die letzte ihrer Art.** Ab I9 ergibt sie sich aus Reihenfolge -und Status und wird erzeugt, statt hier fortgeschrieben zu werden — sie ist -genau die Sorte Angabe, die von Hand zuverlässig veraltet. +1. **[B] #69 — Befunde im WebUI: Liste, Filter nach Detektor, Einstufen** +2. **[G] #93 — dc-testkit supervisor — prüfen, ob eine Anbindung wirklich meldet** +3. **[G] #96 — Testartefakt oder echter Befund? — die offene Frage** +4. **[A] #65 — Symbol-Upload und dc_symbolicate** -Nicht mehr auf der Liste: der Supervisor im ausgelieferten Programm. Verworfen, -siehe H. +### A — Supervisor und Befunde + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #65 | Symbol-Upload und dc_symbolicate | 📋 Offen | — | +| #62 | Detektor-Katalog mit stabilen Namen (Modules/Supervisor/Catalog.php) | ✅ Erledigt | `2.6.0` | +| #63 | Fünf serverseitige Detektoren im Tick: novel.error_signature, silence.heartbeat, deviation.self, deviation.fleet, reg... | ✅ Erledigt | `2.6.0` | +| #64 | Supervisor-Client für .NET und PHP | ❌ Verworfen | — | +| #66 | novel.log_template — Logzeilen-Schablonen, die noch nie auftraten | 🧊 Zurueckgestellt — Ausloeser: sobald Logauszüge strukturiert hereinkommen. Heute liefert nichts sie in einer Form, aus der sich Schablonen bilden l... | — | +| #67 | Längsschnitt für regression.build | 🧊 Zurueckgestellt — Ausloeser: wenn Fehlzuordnungen auffallen. Nötig wäre eine Historie der app_version je Monitor; das Feld wird heute überschrieben. | — | +| #68 | Schwellen der serverseitigen Detektoren an echten Daten nachprüfen | 📋 Offen | — | + +### B — Oberflaeche + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #69 | Befunde im WebUI: Liste, Filter nach Detektor, Einstufen | 📋 Offen | — | +| #70 | Fremdmeldungen sichtbar (triage_external in Filter, Abzeichen, Bearbeiten) | ✅ Erledigt | `2.6.0` | +| #71 | Monitoren ein Projekt zuweisen | 📋 Offen | — | + +### C — Projektlandschaft + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #72 | Projektübergreifend melden (dev_agent, evidence, triage_external, affects) | ✅ Erledigt | `2.6.0` | +| #73 | Beziehungen zwischen Projekten (dc_project_links, integrated_version) | ✅ Erledigt | `2.6.0` | +| #74 | Rückkanal: „was du gemeldet hast, ist behoben" | ✅ Erledigt | `2.6.1` | +| #75 | Agent-Prompt-Vorlage in den Projekten durch MCP ablösen | 📋 Offen | — | + +### D — Werkzeuganbindung (MCP) + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #76 | stdio-Adapter ohne Fremdabhängigkeiten | ✅ Erledigt | `2.6.0` | +| #77 | Fremddaten im Werkzeugergebnis abgegrenzt kennzeichnen | ✅ Erledigt | `2.6.0` | +| #78 | Abgleich: Werkzeugliste gegen openapi.php | 📋 Offen | — | +| #79 | Entfernter MCP-Server unter /mcp mit OAuth | 🧊 Zurueckgestellt — Ausloeser: wenn mehrere Maschinen oder Personen dranhängen. Dann aber mit OAuth statt Bearer-Token in einer Konfigurationsdatei ... | — | +| #80 | dc_roadmap und dc_roadmap_set — Planungsstand als Werkzeug | ✅ Erledigt | `2.7.0` | + +### E — Betrieb + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #81 | Migrationen 013 und 014 auf dem Server ausführen | ✅ Erledigt | `2.7.0` | +| #82 | PHP-Seite prüfen — die Testinstanz tut das jetzt bei jedem Deployment | ✅ Erledigt | `2.7.0` | +| #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 | — | + +### F — Quelltext-Zugriff + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #86 | Stufe 0 — Diagnose-Agent läuft auf eigener Maschine mit lokalem Checkout | ✅ Erledigt | — | +| #87 | Stufe 1 — Quelltext-Index im DC, nur Ausschnitte | 🧊 Zurueckgestellt — Ausloeser: wenn ein Agent autonom rund um die Uhr laufen soll, ohne dass eine eigene Maschine läuft. Solange Claude Code den Che... | — | +| #88 | Stufe 2 — nur Metadaten: Signaturen, Aufrufgraph, keine Rümpfe | 🧊 Zurueckgestellt — Ausloeser: falls selbst Ausschnitte zu viel sind. Signaturen, Aufrufgraph und Doc-Kommentare ohne Methodenkörper reichen zur Tri... | — | + +### G — Testumgebung + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #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 | — | +| #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 | — | +| #94 | Test-Monitore aus dem Flottenvergleich heraushalten | 📋 Offen | — | +| #95 | Befunde tragen die Umgebung, ausserhalb production eine Stufe niedriger | ✅ Erledigt | `2.6.1` | +| #97 | Nachpruefung als Arbeitsschlange (awaiting_verification, in_verification, fix_insufficient) | ✅ Erledigt | `2.6.1` | +| #98 | Auswertung über meta_json: wie oft fällt ein Fix durch die Prüfung? | 🧊 Zurueckgestellt — Ausloeser: wenn die Frage „wie oft trägt ein Fix nicht?" tatsächlich gestellt wird. Seit 2.6.1 schreibt jeder Statuswechsel stru... | — | + +### I — Roadmap im Deploymentcenter + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #107 | Diese Datei selbst übernehmen — Einträge A1–H als Items anlegen, Kennungen auf Item-IDs umstellen | ✅ Erledigt | `2.7.0` | +| #99 | Migration 016: epic, roadmap_rank, target_build, defer_trigger, doc_anchor, Status deferred, Tabelle roadmap_epics | ✅ Erledigt | `2.7.0` | +| #100 | Modules/Bugtracker/Roadmap.php — Sicht, Reihenfolge, Erzeugung, Abgleich | ✅ Erledigt | `2.7.0` | +| #101 | /api/bugtracker/v1/roadmap.php — list, epics, next, active, render, check, set, move, reorder, epic | ✅ Erledigt | `2.7.0` | +| #102 | WebUI-Modul Roadmap: Übersicht, Liste, Aufnahmeliste, Themenbereiche | ✅ Erledigt | `2.7.0` | +| #103 | cli/roadmap.php --write/--check/--list | ✅ Erledigt | `2.7.0` | +| #104 | dc_roadmap, dc_roadmap_set im MCP-Adapter | ✅ Erledigt | `2.7.0` | +| #105 | Übernahmeleitfaden für die anderen Projekte | ✅ Erledigt | `2.7.0` | +| #106 | Migration 016 auf dem Server ausführen | ✅ Erledigt | `2.7.0` | +| #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... | — | + +### H — Verworfen + +| | Vorhaben | Stand | Ziel | +|---|---|---|---| +| #111 | Supervisor-Beobachter im ausgelieferten Programm (auch regelbasiert, ohne Sprachmodell) | ❌ Verworfen | — | +| #112 | Sprachmodell-Supervisor auf der Kundenmaschine | ❌ Verworfen | — | +| #113 | Quelltext mit dem Client ausliefern, auch verschlüsselt | ❌ Verworfen | — | +| #114 | Deploy-Key oder Repo-Token im Client | ❌ Verworfen | — | +| #115 | Obfuskation als Schutz einplanen | ❌ Verworfen | — | +| #116 | MCP-Werkzeuge 1:1 aus OpenAPI erzeugen | ❌ Verworfen | — | +| #117 | publish, provision, deactivate als MCP-Werkzeug | ❌ Verworfen | — | +| #118 | Befunde puffern, wenn das DC ausfällt | ❌ Verworfen | — | +| #119 | Benachrichtigung je Befund | ❌ Verworfen | — | +| #120 | Lokale Befund-Oberfläche in den Anwendungen | ❌ Verworfen | — | +| #121 | enforceProject() pauschal für dev_agent öffnen | ❌ Verworfen | — | +| #122 | Feste Schwellwerte je Kennzahl als Hauptweg | ❌ Verworfen | — | + + --- +## Begründungen + +Ab hier von Hand. Jede Überschrift trägt die Item-Nummer, unter der das +Vorhaben im Deploymentcenter steht. + ## A — Supervisor und Befunde Der Supervisor beantwortet die Frage „ist das ungewöhnlich?". Die Frage „ist @@ -74,17 +187,8 @@ Abschnitt 0. > **Error-Reporting und Bugtracker** besser aufgehoben. Die werden > weiterentwickelt. -| | Vorhaben | Stand | -|---|---|---| -| A1 | Detektor-Katalog mit stabilen Namen (`Modules/Supervisor/Catalog.php`) | ✅ 2.6.0 | -| A2 | Fünf serverseitige Detektoren im Tick: `novel.error_signature`, `silence.heartbeat`, `deviation.self`, `deviation.fleet`, `regression.build` | ✅ 2.6.0 | -| A3 | ~~Supervisor-Client für .NET und PHP~~ | ❌ verworfen, siehe H | -| A4 | **Symbol-Upload und `dc_symbolicate`** | 📋 | -| A5 | `novel.log_template` — Logzeilen-Schablonen, die noch nie auftraten | 🧊 | -| A6 | Längsschnitt für `regression.build` | 🧊 | -| A7 | Schwellen der serverseitigen Detektoren an echten Daten nachprüfen | 📋 | -### A4 — 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 @@ -98,12 +202,12 @@ Fehlermeldung überhaupt etwas anfangen kann. Betroffen: `client-dotnet/Deploymentcenter.Packager`, ein Ablageort für Symboldateien, `dc_symbolicate` im MCP-Adapter. -### A5 — `novel.log_template` 🧊 +### [A] #66 — `novel.log_template` **Auslöser:** sobald Logauszüge strukturiert hereinkommen. Heute liefert nichts sie in einer Form, aus der sich Schablonen bilden ließen. -### A6 — 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 @@ -113,7 +217,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. -### A7 — Schwellen nachprüfen 📋 +### [A] #68 — Schwellen nachprüfen Die serverseitigen Detektoren rechnen mit gesetzten, aber nirgends nachgeprüften Werten: `SIGMA = 3.0` für `deviation.self` und `deviation.fleet`, @@ -127,15 +231,18 @@ selbst, sobald gesichtet wird. --- +### [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 -| | Vorhaben | Stand | -|---|---|---| -| B1 | **Befunde im WebUI: Liste, Filter nach Detektor, Einstufen** | 📋 | -| B2 | Fremdmeldungen sichtbar (`triage_external` in Filter, Abzeichen, Bearbeiten) | ✅ 2.6.0 | -| B3 | Monitoren ein Projekt zuweisen | 📋 | -### B1 — Befunde im WebUI 📋 +### [B] #69 — Befunde im WebUI 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 @@ -146,34 +253,28 @@ 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. -### B3 — 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 E1. +nur per SQL — siehe #81. --- ## C — Projektlandschaft -| | Vorhaben | Stand | -|---|---|---| -| C1 | Projektübergreifend melden (`dev_agent`, `evidence`, `triage_external`, `affects`) | ✅ 2.6.0 | -| C2 | Beziehungen zwischen Projekten (`dc_project_links`, `integrated_version`) | ✅ 2.6.0 | -| C3 | **Rückkanal: „was du gemeldet hast, ist behoben"** | ✅ 2.6.1 — als Arbeitsschlange, nicht als Benachrichtigung | -| C4 | Agent-Prompt-Vorlage in den Projekten durch MCP ablösen | 📋 | -### C3 — Rückkanal ✅ +### [C] #74 — Rückkanal 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 erneut und baut seinen Workaround nie zurück. `GET /api/updateservice/v1/changelog?since=…` liefert bereits `action_items`. -Zu ergänzen ist der Abgleich über `bugtracker_item_affects`: „Item #42, das du +Zu ergänzen ist der Abgleich über `bugtracker_item_affects`: „Das Item, das du gemeldet hast, ist in 2.7.0 erledigt." -Das schließt den Kreis, den C1 geöffnet hat. Ohne ihn ist der Meldeweg ein +Das schließt den Kreis, den #72 geöffnet hat. Ohne ihn ist der Meldeweg ein Briefkasten ohne Leerung. **Gelöst in 2.6.1 — aber anders als hier ursprünglich gedacht.** Der Entwurf @@ -198,7 +299,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. -### C4 — Prompt-Vorlage entlasten 📋 +### [C] #75 — Prompt-Vorlage entlasten [AGENT_PROMPT_TEMPLATE.md](./AGENT_PROMPT_TEMPLATE.md) ist in jedes Projekt einkopiert, samt der von Hand gepflegten Versionszeile. @@ -226,15 +327,8 @@ trägt jedes Projekt alles mit. ## D — Werkzeuganbindung (MCP) -| | Vorhaben | Stand | -|---|---|---| -| D1 | stdio-Adapter mit 13 Werkzeugen, ohne Fremdabhängigkeiten | ✅ 2.6.0 | -| D2 | Fremddaten im Werkzeugergebnis abgegrenzt kennzeichnen | ✅ 2.6.0 | -| D3 | Abgleich: Werkzeugliste gegen `openapi.php` | 📋 | -| D4 | Entfernter MCP-Server unter `/mcp` mit OAuth | 🧊 | -| D5 | `dc_roadmap` und `dc_roadmap_set` — Planungsstand als Werkzeug | ✅ 2.7.0 (siehe I) | -### D3 — Abgleich Werkzeugliste ↔ OpenAPI 📋 +### [D] #78 — Abgleich Werkzeugliste ↔ OpenAPI Adapter und OpenAPI beschreiben dieselben Endpunkte. Ohne Abgleich laufen sie auseinander, und es fällt erst auf, wenn ein Werkzeug ins Leere greift. @@ -244,7 +338,7 @@ 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. -### D4 — Entfernter Server 🧊 +### [D] #79 — Entfernter Server **Auslöser:** wenn mehrere Maschinen oder Personen dranhängen. Dann aber mit OAuth statt Bearer-Token in einer Konfigurationsdatei — sonst liegt dasselbe @@ -257,22 +351,15 @@ der Aufwand nicht. Kein Code, aber Voraussetzung dafür, dass Gebautes wirkt. -| | Aufgabe | Stand | -|---|---|---| -| E1 | Migrationen 013 und 014 auf dem Server ausführen | ⚙️ | -| E2 | PHP-Seite von 2.6.0 prüfen — auf der Entwicklungsmaschine lief kein Lint | ⚙️ | -| E3 | Monitoren ein Projekt zuordnen (`watchdog_monitors.project_slug`) | ⚙️ | -| E4 | Je Projekt ein Werkbank-Token (`dev_agent`) anlegen | ⚙️ | -| E5 | Supervisor-Vorbereitungen in den Projekten zurückbauen | ⚙️ | -**E3 ist Voraussetzung für `deviation.fleet` und `regression.build`.** Ohne +**#83 ist Voraussetzung für `deviation.fleet` und `regression.build`.** Ohne Zuordnung überspringen beide Detektoren die betroffenen Monitore; die übrigen drei laufen weiter. -**E4 vor E5.** Ohne Werkbank-Token laufen `dc_link_project` und `dc_report` aus +**#84 vor #85.** Ohne Werkbank-Token laufen `dc_link_project` und `dc_report` aus den Projekten heraus ins Leere. -**E2 gehört auf die Test-VM.** Die gesamte PHP-Seite von 2.6.0 — Migrationen +**#82 gehört auf die Test-VM.** Die gesamte PHP-Seite von 2.6.0 — Migrationen 013 und 014, der Supervisor-Endpunkt, das projektübergreifende Melden — wurde auf einer Maschine ohne PHP geschrieben. Weder Lint noch ein Lauf haben sie je gesehen. Seit 2.6.1 gibt es die geteilte Test-VM (Abschnitt G); dort gehört das @@ -284,7 +371,7 @@ php cli/migrate.php php cli/tick.php # ohne --quiet: zeigt den Detektorlauf ``` -**E5 ist reiner Rückbau geworden.** In mehreren Projekten wurden +**#85 ist reiner Rückbau geworden.** In mehreren Projekten wurden Vorbereitungen für einen lokalen Supervisor begonnen — UI-Steuerelemente, teilweise ein KI-Agent. Da der Client-Supervisor verworfen ist (H), fällt das ersatzlos weg; nichts tritt an seine Stelle. Was an den Vorbereitungen wertvoll @@ -302,13 +389,8 @@ Der Auftrag zum Einfügen steht in Die Software bleibt geschlossen. Es gibt keine öffentliche Stelle, an der Quelltext zu sehen ist. Drei Stufen, in aufsteigender Preisgabe: -| | Stufe | Stand | -|---|---|---| -| F1 | **Stufe 0** — Diagnose-Agent läuft auf eigener Maschine mit lokalem Checkout | ✅ Der gegenwärtige Weg | -| F2 | **Stufe 1** — Quelltext-Index im DC, nur Ausschnitte | 🧊 | -| F3 | **Stufe 2** — nur Metadaten: Signaturen, Aufrufgraph, keine Rümpfe | 🧊 | -### F2 — Quelltext-Index Stufe 1 🧊 +### [F] #87 — Quelltext-Index Stufe 1 **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, @@ -325,7 +407,7 @@ 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". -### F3 — Stufe 2 🧊 +### [F] #88 — Stufe 2 **Auslöser:** falls selbst Ausschnitte zu viel sind. Signaturen, Aufrufgraph und Doc-Kommentare ohne Methodenkörper reichen zur Triage, aber nicht zum @@ -338,20 +420,8 @@ Nachbauen. Die geteilte Linux-Test-VM. Anleitung: **[TESTUMGEBUNG.md](./TESTUMGEBUNG.md)** — dort steht auch, was auf der Maschine belegt ist. -| | Vorhaben | Stand | -|---|---|---| -| G1 | **Kopfloser Installationsmodus fuer den `update-agent`** | 📋 | -| G2 | DEV-Lizenzschluessel zur Selbstbedienung (`POST /api/license/v1/dev-key`, Scope `license:dev`) | ✅ 2.6.1 | -| G3 | `dc-testkit` — ein Weg fuer Lizenz, Auslieferung und Meldung | ✅ 2.6.1 | -| G4 | Werkbank-Token je Projekt auf der Test-VM hinterlegen | ⚙️ | -| G5 | **`dc-testkit supervisor` — prüfen, ob eine Anbindung wirklich meldet** | 📋 | -| G6 | Test-Monitore aus dem Flottenvergleich heraushalten | 📋 | -| G7 | Befunde tragen die Umgebung, ausserhalb `production` eine Stufe niedriger | ✅ 2.6.1 | -| G8 | **Testartefakt oder echter Befund? — die offene Frage** | 📋 | -| G8 | Nachpruefung als Arbeitsschlange (`awaiting_verification`, `in_verification`, `fix_insufficient`) | ✅ 2.6.1 | -| G9 | Auswertung über `meta_json`: wie oft fällt ein Fix durch die Prüfung? | 🧊 | -### G1 — Kopfloser Installationsmodus 📋 +### [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, @@ -369,7 +439,7 @@ ohnehin an die Release-Ablage. Betroffen: `client-dotnet/Deploymentcenter.UpdateAgent/Installation.cs`. -### G5 — `dc-testkit findings` 📋 +### [G] #93 — `dc-testkit findings` Die Nahtstelle zwischen Testumgebung und Supervisor, die heute fehlt. @@ -389,13 +459,13 @@ Dahinter `GET /api/supervisor/v1/findings?project_slug=…`. Derselbe Zuschnitt wie `dc_findings` im MCP-Adapter — die Abfrage existiert, es fehlt nur der Weg dorthin von der VM aus. -Das ist zugleich die Voraussetzung für G8: Ohne Sicht auf die eigenen Befunde +Das ist zugleich die Voraussetzung für #96: Ohne Sicht auf die eigenen Befunde 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). -### G6 — Test-Monitore aus dem Flottenvergleich 📋 +### [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. @@ -415,7 +485,7 @@ Zwei Wege, beide vertretbar: 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. -### G7 — Umgebung am Befund ✅ 2.6.1 +### [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 @@ -427,7 +497,7 @@ 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. -### G8 — Testartefakt oder echter Befund? 📋 +### [G] #96 — Testartefakt oder echter Befund? **Die eigentlich schwierige Frage.** Ein Testlauf meldet „API-Aufruf fehlgeschlagen". Liegt das daran, dass auf der VM keine Live-Zugangsdaten @@ -478,7 +548,7 @@ ist keine Umsetzung, sondern eine Lesehaltung: beim Sichten nicht fragen „warum ist das fehlgeschlagen", sondern „wie hat sich die Software dabei verhalten". -### G9 — Auswertung der Statuswechsel 🧊 +### [G] #98 — Auswertung der Statuswechsel **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` @@ -489,12 +559,12 @@ 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. -### G4 — Werkbank-Token auf der Test-VM ⚙️ +### [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 -E4 — dasselbe Token traegt beides. +#84 — dasselbe Token traegt beides. --- @@ -506,22 +576,8 @@ 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.** -| | Vorhaben | Stand | -|---|---|---| -| I1 | Migration 016: `epic`, `roadmap_rank`, `target_build`, `defer_trigger`, `doc_anchor`, Status `deferred`, Tabelle `roadmap_epics` | ✅ 2.7.0 | -| I2 | `Modules/Bugtracker/Roadmap.php` — Sicht, Reihenfolge, Erzeugung, Abgleich | ✅ 2.7.0 | -| I3 | `/api/bugtracker/v1/roadmap.php` — list, epics, next, active, render, check, set, move, reorder, epic | ✅ 2.7.0 | -| I4 | WebUI-Modul **Roadmap**: Übersicht, Liste, Aufnahmeliste, Themenbereiche | ✅ 2.7.0 | -| I5 | `cli/roadmap.php --write/--check/--list` | ✅ 2.7.0 | -| I6 | `dc_roadmap`, `dc_roadmap_set` im MCP-Adapter | ✅ 2.7.0 | -| I7 | Übernahmeleitfaden für die anderen Projekte | ✅ 2.7.0 — [ROADMAP_MIGRATION.md](./ROADMAP_MIGRATION.md) | -| I8 | **Migration 016 auf dem Server ausführen** | ⚙️ | -| I9 | **Diese Datei selbst übernehmen** — Einträge A1–H als Items anlegen, Kennungen auf Item-IDs umstellen | 📋 | -| I10 | Die anderen Projekte übernehmen (myapp, polytrader, predictalytics) | 📋 | -| I11 | `--check` in `scripts/deploy.py` einhängen | 📋 | -| I12 | Auswertung: wie lange steht ein Vorhaben in welcher Lage? | 🧊 | -### I1–I7 — der gebaute Teil ✅ +### [I] #99 ff. — Migration, Modul, Endpunkt, Oberfläche, CLI und Leitfaden Kein neues Modul und keine Roadmap-Tabelle: Ein Roadmap-Eintrag **ist** ein `bugtracker_item`. Es fehlten nur vier Angaben, die ein Backlog nicht braucht @@ -544,20 +600,20 @@ 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. -### I9 — diese Datei übernehmen 📋 +### [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 I8 (Migration), dann I9, dann I11 (Abgleich im Deploy). -I10 kommt zuletzt — die anderen Projekte bekommen ein Verfahren, das hier +**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. -### I12 — Verweildauern 🧊 +### [I] #110 — Auswertung der Verweildauern **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 @@ -570,36 +626,27 @@ die Antwort bereits; eine Auswertung daneben lohnt erst, wenn jemand sie liest. 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 | -|---|---| -| ❌ **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 | -| ❌ 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 | -| ❌ Quelltext mit dem Client ausliefern, auch verschlüsselt | Der Client muss entschlüsseln können, also kann es der Kunde auch | -| ❌ Deploy-Key oder Repo-Token im Client | Dekompiliert in Minuten | -| ❌ Obfuskation als Schutz einplanen | Verzögert, verhindert nicht | -| ❌ 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 | -| ❌ `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 | -| ❌ 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 | -| ❌ 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 | -| ❌ Lokale Befund-Oberfläche in den Anwendungen | Zweite Wahrheit, die nur den eigenen Ausschnitt kennt. Der Client hält keine Historie vor | -| ❌ `enforceProject()` pauschal für `dev_agent` öffnen | Hätte auch `publish` und `setup` in fremden Projekten erlaubt. Die Ausnahme wird je Aufrufstelle gesetzt | -| ❌ Feste Schwellwerte je Kennzahl als Hauptweg | Finden nur, woran vorher jemand gedacht hat. Die tragenden Detektoren vergleichen gegen eigenen Verlauf, Vorversion oder Flotte | +| | 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 | --- ## Pflege dieser Datei -**Bis I9 abgeschlossen ist**, gilt weiterhin von Hand: - -- Ein Vorhaben umgesetzt? Zeile auf ✅ setzen und die Fassung dazuschreiben. -- Etwas verworfen? Nach H, **mit Begründung**. Die Begründung ist der Wert des - Eintrags, nicht die Ablehnung. -- Etwas zurückgestellt? Nach 🧊, **mit Auslöser**. Ohne Auslöser ist es nicht - zurückgestellt, sondern vergessen. -- Ausführliche Herleitungen gehören ins Archiv, nicht hierher. Diese Datei soll - lesbar bleiben. - -**Danach** gilt die Arbeitsteilung aus Abschnitt I: +Die Umstellung ist vollzogen (#107, 29.08.2026). Es gilt die Arbeitsteilung +aus Abschnitt I: | | Wahrheit | Die andere Seite hält | |---|---|---| @@ -616,3 +663,9 @@ falls doch, dann nur mit einem Argument, das hier noch nicht steht. 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. +- 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 + Rückkanal-Absatz eine ausgedachte Beispielnummer — und die gab es wirklich, in + `predictalytics`. Der Abgleich hat es gemeldet; ein Mensch hätte es nie + bemerkt. Für Beispiele deshalb „das Item" schreiben, nicht „Item Nummer x". diff --git a/docs/ROADMAP_MIGRATION.md b/docs/ROADMAP_MIGRATION.md index 29fe39e..d167794 100644 --- a/docs/ROADMAP_MIGRATION.md +++ b/docs/ROADMAP_MIGRATION.md @@ -91,6 +91,37 @@ Oder im WebUI unter **Roadmap → Themenbereiche**. Halte die Zahl klein. Fünf bis zehn Bereiche sind übersichtlich, zwanzig sind eine zweite Ablage, die selbst wieder sortiert werden will. +### Abkürzung: Schritte 2–4 als Skript + +Für eine Datei im Format dieses Projekts — Bereichstabellen mit +`| A4 | Titel | 📋 |`, Auslöser als `**Auslöser:** …` — nimmt +[scripts/roadmap_import.py](../scripts/roadmap_import.py) die drei Schritte ab: + +```bash +python scripts/roadmap_import.py --lesen +``` + +Zeigt, was es gefunden hat, ohne etwas zu senden. **Immer zuerst.** Dabei fällt +auf, was die Datei falsch behauptet — bei der Übernahme hier waren drei +Einträge längst erledigt und einer doppelt vergeben. + +```bash +python scripts/roadmap_import.py --uebernehmen +``` + +`DC_TOKEN` muss dabei ein Werkbank-Token des Projekts sein. Das Skript legt die +Themenbereiche mit an, ist über `client_ref` wiederholbar und wartet bei +Drosselung, statt abzubrechen: `/report` lässt 60 Meldungen je Minute zu, und +eine Roadmap mit mehr Einträgen liegt darüber. + +Die Prioritätsreihenfolge liest es aus dem Abschnitt „Als Nächstes" der Datei — +solange die Datei die Quelle ist, soll auch die Priorität von dort kommen und +nicht aus einer zweiten Liste daneben. + +Für eine Datei in anderem Format ist das Skript die Vorlage, nicht das +Werkzeug: Die Auswertung steht in `auswerten()` und ist auf etwa achtzig Zeilen +angepasst. + ### 3. Vorhaben anlegen Jedes Vorhaben wird ein Item. Mit MCP-Adapter: diff --git a/public/index.php b/public/index.php index 24d72a5..2320842 100644 --- a/public/index.php +++ b/public/index.php @@ -2043,8 +2043,15 @@ $roadmapCandidates = $bugRepo->getItems([ 'limit' => 60, ])['items']; -/** Baut eine URL mit den aktuellen Roadmap-Filtern. */ -function rmUrl(array $overrides = []): string +/** + * Baut eine URL mit den aktuellen Roadmap-Filtern. + * + * Das Sprungziel ist einstellbar, weil ein gesetzter Filter nur dort etwas + * bewirkt, wo die gefilterte Liste auch steht. Ein Verweis auf einen + * Themenbereich, der auf der Uebersicht landet, setzt den Filter zwar - man + * sieht davon aber nichts und haelt den Link fuer kaputt. + */ +function rmUrl(array $overrides = [], string $anker = '#tab-roadmap'): string { $params = [ 'rm_project' => $_GET['rm_project'] ?? null, @@ -2058,7 +2065,7 @@ function rmUrl(array $overrides = []): string $params = array_merge($params, $overrides); $params = array_filter($params, static fn($v): bool => $v !== null && $v !== '' && $v !== 'all'); - return 'index.php' . ($params !== [] ? '?' . http_build_query($params) : '') . '#tab-roadmap'; + return 'index.php' . ($params !== [] ? '?' . http_build_query($params) : '') . $anker; } /** Zeichen und Klartext zu einem Status, fuer Abzeichen in der Liste. */ @@ -2250,6 +2257,30 @@ $csrfField = Csrf::field(); .modal-backdrop.active { display: flex; } .modal-card { background: #14171d; border: 1px solid rgba(255,255,255,0.12); border-radius: 18px; padding: 1.75rem; max-width: 600px; width: 95%; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 50px rgba(0,0,0,0.8); } + /* + Verweise im Inhalt. + + Es gab dafuer bisher keine Regel - Links in Karten und Tabellen + erschienen also im Browservorgabeblau (#0000EE) auf beinahe + schwarzem Grund und waren schlicht nicht lesbar. Aufgefallen ist es + an den Bereichsnamen der Roadmap, betrifft aber jeden Verweis im + Inhaltsbereich. + + Bewusst eng gefasst: .btn und .nav-link sind ebenfalls und + bringen ihre eigene Gestaltung mit. Eine Regel auf blankes "a" + haette sie ueberschrieben. + */ + .card a:not(.btn), table a:not(.btn) { + color: var(--primary); + text-decoration: none; + border-bottom: 1px solid rgba(91,157,255,0.35); + transition: color .15s, border-color .15s; + } + .card a:not(.btn):hover, table a:not(.btn):hover { + color: #9cc4ff; + border-bottom-color: #9cc4ff; + } + .tab-content, .subtab-content { display: none; } .tab-content.active, .subtab-content.active { display: block; } @@ -4023,7 +4054,8 @@ chmod +x update-agent - + diff --git a/scripts/roadmap_import.py b/scripts/roadmap_import.py new file mode 100644 index 0000000..2962c10 --- /dev/null +++ b/scripts/roadmap_import.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Eine handgepflegte ROADMAP.md ins Deploymentcenter uebernehmen. + +Einmalig je Projekt. Das Verfahren steht in docs/ROADMAP_MIGRATION.md; dieses +Skript nimmt davon die Schritte 3 und 4 ab - Items anlegen und einplanen. + + python scripts/roadmap_import.py --lesen + Nur auswerten und zeigen, was gefunden wurde. Kein Netzverkehr. + + python scripts/roadmap_import.py --probe + Gegen die Testinstanz laufen lassen. Braucht ein Werkbank-Token, das + das Skript sich ueber testctl selbst ausstellt. + + python scripts/roadmap_import.py --uebernehmen + Gegen die Produktion. DC_TOKEN muss ein Werkbank-Token des Projekts + sein (owner_type = dev_agent, Scope bugtracker:manage). + +Wiederholbar: Jedes Item traegt ein client_ref aus Projekt und alter Kennung +("roadmap-deploymentcenter-a4"). Ein zweiter Lauf legt deshalb nichts doppelt +an, sondern findet dieselben Items wieder und schreibt nur die Roadmap-Felder +neu. Bei sechzig Eintraegen laeuft irgendetwas immer zweimal. +""" + +import argparse +import json +import os +import re +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +PROJEKT_DIR = Path(__file__).resolve().parent.parent +ROADMAP = PROJEKT_DIR / 'docs' / 'ROADMAP.md' +SLUG = 'deploymentcenter' +ANKER_DATEI = 'docs/ROADMAP.md' + +# Zeichen -> (Status, ob als Vorhaben zaehlend) +MARKER = { + '✅': 'resolved', + '🔨': 'in_progress', + '📋': 'open', + '🧊': 'deferred', + '❌': 'rejected', + '⚙️': 'open', # Betriebsaufgabe: kein Code, aber uebernehmbar +} + +# Reihenfolge der Bereiche in roadmap_epics (Migration 016) +BEREICHE = { + 'A': 'Supervisor und Befunde', + 'B': 'Oberflaeche', + 'C': 'Projektlandschaft', + 'D': 'Werkzeuganbindung (MCP)', + 'E': 'Betrieb', + 'F': 'Quelltext-Zugriff', + 'G': 'Testumgebung', + 'H': 'Verworfen', + 'I': 'Roadmap im Deploymentcenter', +} + + +# --------------------------------------------------------------------------- +# Auswerten +# --------------------------------------------------------------------------- + +def entferne_auszeichnung(text): + """Markdown-Auszeichnung raus - ein Item-Titel ist kein Fliesstext.""" + text = re.sub(r'~~(.+?)~~', r'\1', text) + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) + text = re.sub(r'`([^`]+)`', r'\1', text) + text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) + return re.sub(r'\s+', ' ', text).strip() + + +def lies_ausloeser(quelle, kennung): + """ + Holt den Ausloeser eines zurueckgestellten Vorhabens aus seinem Absatz. + + Die Datei schreibt ihn durchgaengig als "**Ausloeser:** ...". Fehlt er, + ist der Eintrag nach der eigenen Regel gar nicht zurueckgestellt, sondern + vergessen - dann meldet das Skript das, statt etwas zu erfinden. + """ + abschnitt = re.search( + r'^###\s+%s\s+[—-][^\n]*\n(.*?)(?=^###|\Z)' % re.escape(kennung), + quelle, re.M | re.S) + if not abschnitt: + return None + m = re.search(r'\*\*Ausl[öo]ser:\*\*\s*(.+?)(?:\n\n|\Z)', abschnitt.group(1), re.S) + if not m: + return None + return entferne_auszeichnung(m.group(1))[:400] + + +def lies_fassung(stand): + """Aus "✅ 2.6.1 — als Arbeitsschlange" wird "2.6.1".""" + m = re.search(r'(\d+\.\d+\.\d+)', stand) + return m.group(1) if m else None + + +def anker(kennung, titel): + """ + Verweis auf die Stelle, an der die Begruendung steht. + + Nach dem Umbau der Datei traegt die Ueberschrift die Item-Nummer; bis + dahin zeigt der Anker auf die alte Kennung. Das ist Absicht: Der Anker + soll auch waehrend der Umstellung auf etwas zeigen, das es gibt. + """ + stueck = re.sub(r'[^a-z0-9]+', '-', entferne_auszeichnung(titel).lower()).strip('-') + return '%s#%s-%s' % (ANKER_DATEI, kennung.lower(), stueck[:60]) + + +def lies_reihenfolge(quelle): + """ + Die Prioritaet aus dem Abschnitt "Als Naechstes". + + Sie steht dort als nummerierte Liste mit den Kennungen in eckigen Klammern + ("1. **[B1] Befunde in der Oberflaeche**"). Sie hier auszulesen statt im + Skript zu hinterlegen ist der Punkt: Solange die Datei die Quelle ist, + soll auch die Prioritaet aus ihr kommen und nicht aus einer zweiten Liste, + die daneben veraltet. + + Danach ergibt sich dieselbe Reihenfolge aus dem Rang im Deploymentcenter - + und der Abschnitt in der Datei entfaellt. + + @return Liste der Kennungen in Prioritaetsreihenfolge + """ + m = re.search(r'^## Als N[äa]chstes\b(.*?)(?=^---|\Z)', quelle, re.M | re.S) + if not m: + return [] + return re.findall(r'^\d+\.\s*\*\*\[([A-Z]\d+)\]', m.group(1), re.M) + + +def auswerten(): + quelle = ROADMAP.read_text(encoding='utf-8') + eintraege = [] + gesehen = {} + + # --- Die nummerierten Vorhaben aus den Bereichstabellen ----------------- + for m in re.finditer(r'^\| *([A-Z])(\d+) *\| *(.+?) *\| *(.+?) *\|\s*$', quelle, re.M): + bereich, nummer, titel, stand = m.groups() + kennung = bereich + nummer + + status = next((v for k, v in MARKER.items() if k in stand), 'open') + betrieb = '⚙️' in stand + + # Doppelt vergebene Kennung: Genau der Fehler, den Item-IDs abschaffen. + # Nicht stillschweigend zusammenfuehren - beide sind eigene Vorhaben. + if kennung in gesehen: + gesehen[kennung] += 1 + kennung_eindeutig = '%s-%d' % (kennung, gesehen[kennung]) + else: + gesehen[kennung] = 1 + kennung_eindeutig = kennung + + eintraege.append({ + 'kennung': kennung_eindeutig, + 'epic': bereich, + 'titel': entferne_auszeichnung(titel), + 'status': status, + 'betrieb': betrieb, + 'fassung': lies_fassung(stand), + 'ausloeser': lies_ausloeser(quelle, kennung) if status == 'deferred' else None, + 'stand_roh': stand, + }) + + # --- Abschnitt H: Verworfenes ------------------------------------------ + # + # Ohne Kennung, zwei Spalten (Vorschlag | Warum nicht). Sie kommen mit ins + # DC, damit dc_roadmap die Frage "wurde das schon abgelehnt?" beantworten + # kann - sonst muesste ein Agent dafuer die Datei lesen, und genau das + # tut er nicht zuverlaessig. Die Begruendung bleibt in der Datei. + h = re.search(r'^## H — Verworfen\b(.*?)(?=^## |\Z)', quelle, re.M | re.S) + if h: + nr = 0 + for zeile in re.finditer(r'^\| *(❌ *)?(.+?) *\| *(.+?) *\|\s*$', h.group(1), re.M): + vorschlag = entferne_auszeichnung(zeile.group(2)) + grund = entferne_auszeichnung(zeile.group(3)) + if not vorschlag or vorschlag.startswith('---') or vorschlag == 'Vorschlag': + continue + nr += 1 + eintraege.append({ + 'kennung': 'H%d' % nr, + 'epic': 'H', + 'titel': vorschlag, + 'status': 'rejected', + 'betrieb': False, + 'fassung': None, + 'ausloeser': None, + 'begruendung': grund, + 'stand_roh': '❌', + }) + + return eintraege + + +# --------------------------------------------------------------------------- +# Uebertragen +# --------------------------------------------------------------------------- + +class Fehler(Exception): + pass + + +def ruf(basis, token, methode, pfad, query=None, body=None, versuche=6): + """ + Ein Aufruf, mit Geduld bei der Drosselung. + + /api/bugtracker/v1/report laesst 60 Meldungen je Minute zu. Eine Roadmap + mit einundsechzig Eintraegen liegt genau darueber - der erste Probelauf + brach beim letzten Eintrag ab. Andere Projekte werden groessere Dateien + haben. + + Behandelt wird das so, wie es die Agentenanleitung von jedem Client + verlangt: warten und das Intervall verdoppeln, nicht sofort wiederholen. + Abbrechen und den Menschen bitten, es spaeter noch einmal zu versuchen, + waere hier besonders unfreundlich - der Lauf ist zwar wiederholbar, aber + er dauert. + """ + url = basis + pfad + if query: + url += ('&' if '?' in pfad else '?') + urllib.parse.urlencode( + {k: v for k, v in query.items() if v is not None}) + + daten = json.dumps(body).encode('utf-8') if body is not None else None + warte = 5 + + for versuch in range(1, versuche + 1): + req = urllib.request.Request(url, data=daten, method=methode) + req.add_header('Authorization', 'Bearer ' + token) + req.add_header('Content-Type', 'application/json') + req.add_header('Accept', 'application/json') + + try: + with urllib.request.urlopen(req, timeout=60) as r: + return json.loads(r.read().decode('utf-8') or '{}') + except urllib.error.HTTPError as e: + roh = e.read().decode('utf-8', 'replace') + try: + d = json.loads(roh) + code = d.get('error', {}).get('code', str(e.code)) + text = d.get('error', {}).get('message', roh)[:200] + except ValueError: + code, text = 'http_%d' % e.code, roh[:200] + + if code == 'rate_limited' and versuch < versuche: + print(' gedrosselt - %d s warten (Versuch %d von %d)' + % (warte, versuch, versuche)) + time.sleep(warte) + warte = min(warte * 2, 90) + continue + + raise Fehler('%s: %s' % (code, text)) + + raise Fehler('rate_limited: auch nach %d Versuchen gedrosselt.' % versuche) + + +def bereiche_anlegen(basis, token, eintraege, trocken=False): + """ + Sorgt dafuer, dass jeder benutzte Themenbereich auch angelegt ist. + + Ohne diesen Schritt entstehen Waisen: Items mit einem Bereich, den + roadmap_epics nicht kennt. Die Uebersicht zeigt sie dann als Warnung an, + weil ein Tippfehler sonst lautlos einen weiteren Bereich erzeugt - aber + hier ist es kein Tippfehler, sondern ein fehlender Schritt. + + Beim Deploymentcenter faellt das nur bei H auf, weil Migration 016 die + uebrigen Bereiche mitbringt. Fuer jedes andere Projekt fehlen sie alle. + ON DUPLICATE KEY im Endpunkt macht den Aufruf wiederholbar. + """ + benutzt = [] + for e in eintraege: + if e['epic'] not in benutzt: + benutzt.append(e['epic']) + + # Nur anlegen, was fehlt. + # + # Der Endpunkt schreibt mit ON DUPLICATE KEY, setzt also auch Titel, + # Kurzbeschreibung und Sortierung neu. Ein Bereich, den jemand von Hand + # nachgeschaerft hat - oder den Migration 016 mit einer Beschreibung + # mitbrachte - verloere sie beim zweiten Lauf wieder. Ein Uebernahmeskript + # darf anlegen; kuratierte Angaben ueberschreiben darf es nicht. + vorhanden = set() + if not trocken: + antwort = ruf(basis, token, 'GET', '/api/bugtracker/v1/roadmap.php', + query={'action': 'epics', 'project_slug': SLUG}) + vorhanden = {e['epic'] for e in antwort.get('epics', [])} + + hoechste = 0 + for i, kuerzel in enumerate(benutzt): + if kuerzel in vorhanden: + print(' Bereich [%s] besteht bereits - unveraendert' % kuerzel) + continue + + titel = BEREICHE.get(kuerzel, 'Bereich %s' % kuerzel) + if trocken: + print(' wuerde Bereich anlegen: [%s] %s' % (kuerzel, titel)) + continue + + hoechste += 1 + ruf(basis, token, 'POST', '/api/bugtracker/v1/roadmap.php', + query={'action': 'epic'}, + body={'project_slug': SLUG, 'epic': kuerzel, 'title': titel, + 'sort_order': (len(vorhanden) + hoechste) * 10}) + print(' Bereich [%s] %s - neu angelegt' % (kuerzel, titel)) + + +def uebertragen(basis, token, eintraege, trocken=False): + angelegt = aktualisiert = 0 + rang = {} + + bereiche_anlegen(basis, token, eintraege, trocken) + print() + + # Prioritaet aus dem Abschnitt "Als Naechstes" der Datei. + # + # Der Rang dient zweierlei: innerhalb eines Bereichs ordnet er, ueber alle + # Bereiche hinweg priorisiert er (nextUp sortiert global danach). Wer + # ausdruecklich benannt ist, bekommt deshalb einen einstelligen Rang und + # steht damit vor allem, was nur in Dateireihenfolge eingereiht wurde. + prioritaet = lies_reihenfolge(ROADMAP.read_text(encoding='utf-8')) + if prioritaet: + print('Prioritaet aus "Als Naechstes": %s' % ', '.join(prioritaet)) + print() + + for e in eintraege: + rang[e['epic']] = rang.get(e['epic'], 0) + 10 + client_ref = 'roadmap-%s-%s' % (SLUG, e['kennung'].lower()) + + beschreibung = ( + 'Uebernommen aus %s, vormals Kennung %s.\n\n' + 'Die Begruendung steht weiterhin in der Datei - hier steht nur der Zustand.' + % (ANKER_DATEI, e['kennung']) + ) + if e.get('begruendung'): + beschreibung = 'Verworfen: %s\n\n%s' % (e['begruendung'], beschreibung) + if e['betrieb']: + beschreibung = 'Betriebsaufgabe - kein Code.\n\n' + beschreibung + + if trocken: + print(' wuerde anlegen: [%s] %-6s %s' % (e['epic'], e['kennung'], e['titel'][:60])) + continue + + # 1. Item anlegen bzw. wiederfinden. client_ref macht das idempotent. + antwort = ruf(basis, token, 'POST', '/api/bugtracker/v1/report', body={ + 'project_slug': SLUG, + 'type': 'feature_request', + 'title': e['titel'][:250], + 'description': beschreibung, + 'severity': 'medium', + 'environment': 'development', + 'client_ref': client_ref, + }) + item_id = antwort.get('item_id') or antwort.get('id') + if not item_id: + raise Fehler('Keine Item-ID in der Antwort: %s' % json.dumps(antwort)[:200]) + + neu = bool(antwort.get('is_new', True)) + angelegt += 1 if neu else 0 + aktualisiert += 0 if neu else 1 + + # 2. Einplanen. Status und Ausloeser muessen zusammen ankommen - + # getrennt gesetzt lehnt der Server das Zurueckstellen ab. + # Ausdruecklich priorisierte Eintraege nach vorn - aber nur, solange + # sie ueberhaupt noch offen sind. Ein erledigtes Vorhaben auf Rang 1 + # stuende dauerhaft an der Spitze einer Liste, die "was ist als + # Naechstes dran" beantworten soll. + if e['kennung'] in prioritaet and e['status'] in ('open', 'planned', 'in_progress'): + eigener_rang = prioritaet.index(e['kennung']) + 1 + else: + eigener_rang = rang[e['epic']] + + felder = { + 'project_slug': SLUG, + 'epic': e['epic'], + 'roadmap_rank': eigener_rang, + 'doc_anchor': anker(e['kennung'], e['titel']), + 'status': e['status'], + } + if e['status'] == 'deferred': + if not e['ausloeser']: + raise Fehler( + '%s ist zurueckgestellt, aber in der Datei steht kein ' + '"**Ausloeser:**" - nach der eigenen Regel ist das kein ' + 'zurueckgestelltes, sondern ein vergessenes Vorhaben.' % e['kennung']) + felder['defer_trigger'] = e['ausloeser'] + if e['fassung']: + felder['target_build'] = e['fassung'] + + ruf(basis, token, 'POST', '/api/bugtracker/v1/roadmap.php', + query={'action': 'set', 'id': item_id}, body=felder) + + # 3. Erledigte sauber abschliessen, damit resolved_in_build stimmt. + if e['status'] == 'resolved' and e['fassung']: + ruf(basis, token, 'POST', '/api/bugtracker/v1/manage', + query={'action': 'resolve', 'id': item_id}, + body={'resolved_in_build': e['fassung'], + 'resolution_notes': 'Bei der Roadmap-Uebernahme als erledigt uebertragen.'}) + + print(' %s #%-5s [%s] %-10s %s' % ( + 'neu ' if neu else 'akt.', item_id, e['epic'], e['status'], e['titel'][:52])) + + return angelegt, aktualisiert + + +# --------------------------------------------------------------------------- + +def main(): + p = argparse.ArgumentParser(description='ROADMAP.md ins Deploymentcenter uebernehmen') + p.add_argument('--lesen', action='store_true', help='nur auswerten, nichts senden') + p.add_argument('--probe', action='store_true', help='gegen die Testinstanz') + p.add_argument('--uebernehmen', action='store_true', help='gegen die Produktion') + p.add_argument('--trocken', action='store_true', help='zeigen, was gesendet wuerde') + args = p.parse_args() + + eintraege = auswerten() + + zaehler = {} + for e in eintraege: + zaehler[e['status']] = zaehler.get(e['status'], 0) + 1 + + print('%d Eintraege aus %s' % (len(eintraege), ANKER_DATEI)) + print(' ' + ', '.join('%s: %d' % kv for kv in sorted(zaehler.items()))) + print() + + if args.lesen: + bereich = None + for e in eintraege: + if e['epic'] != bereich: + bereich = e['epic'] + print('[%s] %s' % (bereich, BEREICHE.get(bereich, '?'))) + zusatz = '' + if e['fassung']: + zusatz += ' -> %s' % e['fassung'] + if e['ausloeser']: + zusatz += ' Ausloeser: %s' % e['ausloeser'][:50] + print(' %-6s %-10s %-52s%s' % (e['kennung'], e['status'], e['titel'][:52], zusatz)) + return 0 + + if args.probe: + sys.path.insert(0, str(PROJEKT_DIR / 'tests' / 'e2e')) + import harness + cfg = harness.lade_config(None) + client = harness.Client(cfg['base_url'], + cfg['basic_auth']['user'], cfg['basic_auth']['pass']) + r = client.post('/testctl.php', + body={'action': 'workbench_token', 'project_slug': SLUG, + 'name': 'roadmap-import'}, + token=cfg['keys']['shared_key']) + if not r.ok: + print('Werkbank-Token nicht erhalten: %s' % r.kurz(300)) + return 2 + basis, token = cfg['base_url'], r.feld('raw_token') + print('Testinstanz %s, Token frisch ausgestellt.' % basis) + + elif args.uebernehmen: + token = os.environ.get('DC_TOKEN', '') + if not token: + print('DC_TOKEN fehlt. Erwartet wird ein Werkbank-Token des Projekts') + print('"%s" (owner_type = dev_agent, Scope bugtracker:manage).' % SLUG) + return 2 + basis = os.environ.get('DC_BASE_URL', 'https://dc.mhdf.de') + print('Produktion %s' % basis) + + else: + p.print_help() + return 2 + + try: + neu, alt = uebertragen(basis, token, eintraege, trocken=args.trocken) + except Fehler as e: + print('\nAbbruch: %s' % e) + return 1 + + if not args.trocken: + print('\n%d angelegt, %d aktualisiert.' % (neu, alt)) + print('Naechster Schritt: php cli/roadmap.php --write --project=%s' % SLUG) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/Modules/Bugtracker/Roadmap.php b/src/Modules/Bugtracker/Roadmap.php index b621fa3..721bb9d 100644 --- a/src/Modules/Bugtracker/Roadmap.php +++ b/src/Modules/Bugtracker/Roadmap.php @@ -618,17 +618,54 @@ final class Roadmap */ public static function spliceBlock(string $content, string $block): string { - $start = strpos($content, self::BLOCK_START); - $end = strpos($content, self::BLOCK_END); + [$start, $end] = self::findeBlock($content); - if ($start === false || $end === false || $end < $start) { + if ($start === null || $end === null) { $separator = rtrim($content) === '' ? '' : "\n\n"; return rtrim($content) . $separator . $block . "\n"; } return substr($content, 0, $start) . $block - . substr($content, $end + strlen(self::BLOCK_END)); + . substr($content, $end); + } + + /** + * Findet den erzeugten Block - und zwar den echten. + * + * Die erste Fassung nahm strpos() auf BLOCK_START, also die erste + * Fundstelle im Text. Das ging so lange gut, bis eine Datei den Marker + * ERWAEHNTE: Die Einleitung dieser Roadmap erklaert, was zwischen den + * ``-Markern steht - und genau dort setzte der Splice + * an. Ergebnis: Der halbe Kopf der Datei wurde vom Block ueberschrieben. + * + * Bitter daran ist, dass es ausgerechnet die Datei trifft, die das + * Verfahren beschreibt. Jede gute Roadmap-Datei erwaehnt den Marker. + * + * Der echte Block traegt Angaben hinter dem Marker (stand=..., items=...) + * und steht am Zeilenanfang. Eine Erwaehnung im Fliesstext tut beides + * nicht. Danach wird gesucht - und zur Sicherheit auch beim Ende: das + * naechste BLOCK_END NACH dem Anfang, nicht das erste im Text. + * + * @return array{0:?int,1:?int} Anfang und Ende (hinter dem Endmarker) + */ + 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', + $content, $treffer, PREG_OFFSET_CAPTURE)) { + return [null, null]; + } + + $start = (int)$treffer[0][1]; + $end = strpos($content, self::BLOCK_END, $start); + + if ($end === false) { + return [null, null]; + } + + return [$start, $end + strlen(self::BLOCK_END)]; } // ================================================================== @@ -670,11 +707,14 @@ final class Roadmap // Zustandsblock ausklammern - die dortigen IDs stammen aus derselben // Abfrage und wuerden jede Pruefung trivial bestehen lassen. + // Ueber findeBlock(), nicht ueber strpos: Eine Erwaehnung des Markers + // im Fliesstext haette sonst den Anfang bestimmt, und ausgeklammert + // waere ein Stueck Prosa gewesen statt des Blocks - also genau die + // Absaetze, die geprueft werden sollen. $prose = $content; - $start = strpos($content, self::BLOCK_START); - $end = strpos($content, self::BLOCK_END); - if ($start !== false && $end !== false && $end > $start) { - $prose = substr($content, 0, $start) . substr($content, $end + strlen(self::BLOCK_END)); + [$start, $end] = self::findeBlock($content); + if ($start !== null && $end !== null) { + $prose = substr($content, 0, $start) . substr($content, $end); } $stale = self::blockAge($content); diff --git a/tests/e2e/harness.py b/tests/e2e/harness.py index 57d5821..86c6d3d 100644 --- a/tests/e2e/harness.py +++ b/tests/e2e/harness.py @@ -48,21 +48,48 @@ DEV_CONFIG = PROJECT_DIR / 'scripts' / 'deploy_dev_config.json' _ECHTES_GETADDRINFO = socket.getaddrinfo -def erreichbar(host, port, familie, timeout=3.0): +def erreichbar(host, port, familie, timeout=4.0): + """ + Prueft, ob ueber diese Adressfamilie wirklich gearbeitet werden kann. + + Ein blosses connect() genuegt dafuer nicht - und das war der Fehler der + ersten Fassung. Auf dieser Strecke gelingt der TCP-Verbindungsaufbau ueber + IPv6 zeitweise, waehrend danach nichts mehr fliesst. Die Pruefung meldete + dann "erreichbar", der Lauf blieb auf IPv6, und jede einzelne Anfrage lief + 21 Sekunden in einen Timeout: 129 Testfaelle statt in elf Sekunden in + ueber einer Stunde. Schlimmer noch, es war launisch - derselbe Lauf ging + mal so und mal so aus, was die Ursache lange verdeckt hat. + """ try: infos = _ECHTES_GETADDRINFO(host, port, familie, socket.SOCK_STREAM) except socket.gaierror: return False + + kontext = ssl.create_default_context() + for info in infos: s = socket.socket(info[0], socket.SOCK_STREAM) s.settimeout(timeout) try: s.connect(info[4]) - return True + + # Bis hierher kam die erste Fassung. Der Rest ist der Unterschied + # zwischen "eine Verbindung steht" und "es kommt etwas zurueck": + # TLS-Handschlag und eine echte Antwort. + with kontext.wrap_socket(s, server_hostname=host) as tls: + tls.settimeout(timeout) + tls.sendall( + b'HEAD /api/health HTTP/1.1\r\nHost: ' + host.encode() + + b'\r\nConnection: close\r\nUser-Agent: dc-e2e\r\n\r\n') + return tls.recv(16).startswith(b'HTTP/') except OSError: continue finally: - s.close() + try: + s.close() + except OSError: + pass + return False diff --git a/tests/e2e/suite_bugtracker.py b/tests/e2e/suite_bugtracker.py index f344400..d855428 100644 --- a/tests/e2e/suite_bugtracker.py +++ b/tests/e2e/suite_bugtracker.py @@ -516,6 +516,47 @@ def _(ctx): erwarte_erfolg(r) +@test('roadmap', 'Abgleich uebersieht eine Markererwaehnung im Fliesstext') +def _(ctx): + """ + BEFUND (29.08.2026): spliceBlock() und check() suchten den erzeugten Block + mit strpos() auf den Startmarker - also die erste Fundstelle im Text. + + Das ging gut, bis eine Datei den Marker ERWAEHNTE. Die Einleitung einer + ordentlichen Roadmap tut genau das: sie erklaert, was zwischen den + dc:roadmap-Markern steht. Der Splice setzte dort an und ueberschrieb den + halben Dateikopf; der Abgleich klammerte Prosa aus statt des Blocks. + + Bitter daran ist, dass es ausgerechnet die Dateien trifft, die das + Verfahren beschreiben - also die guten. + + Geprueft wird mit einer Datei, die den Marker erst erwaehnt und dann + wirklich fuehrt: Ein Verweis VOR der Erwaehnung muss in der Pruefung + ankommen, ein Verweis INNERHALB des Blocks nicht. + """ + inhalt = ( + '# Roadmap\n\n' + 'Zwischen den ``-Markern steht eine erzeugte Kopie.\n\n' + 'Siehe #%d fuer den Rest.\n\n' + '\n' + '| #999999 | erzeugt, wird nicht geprueft |\n' + '\n' + ) % (ctx['freies_item'], PROJEKT) + + r = ctx.client.post('/api/bugtracker/v1/roadmap.php', + query={'action': 'check'}, + body={'project_slug': PROJEKT, 'content': inhalt}) + erwarte_erfolg(r) + + codes = [f['code'] for f in (r.feld('findings', default=[]) or [])] + erwarte('unknown_item' not in codes, + 'Der erzeugte Block wurde mitgeprueft - die Markererwaehnung in der ' + 'Einleitung hat den Blockanfang bestimmt. Befunde: %s' % codes) + erwarte(r.feld('checked') >= 1, + 'Der Verweis vor dem Block wurde nicht geprueft (checked=%s) - es ' + 'wurde zu viel ausgeklammert.' % r.feld('checked')) + + @test('roadmap', 'Roadmap-Schreibzugriff verlangt Verwaltungsrecht') def _(ctx): r = ctx.client.post('/api/bugtracker/v1/roadmap.php',