Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.
ROADMAP IM DEPLOYMENTCENTER (2.7.0)
Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.
Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.
- Migration 016: epic, roadmap_rank, target_build, defer_trigger,
doc_anchor, Status "deferred", Tabelle roadmap_epics.
KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
- Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
- Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
die Kennung nach einer Loeschung neu vergeben wurde.
- Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
Repository, damit sie auf allen drei Schreibwegen gleich greift.
- Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
dc_roadmap und dc_roadmap_set im MCP-Adapter.
- cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
--check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
- docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.
TESTINSTANZ UND TESTTOR
- dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
Basic-Auth davor, Rocket.Chat aus.
- 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
- scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
die Testinstanz und laesst dann den Testlauf laufen. Erst bei
Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
- tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
nicht versehentlich auf dc.mhdf.de landen.
- Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.
SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)
- Migration 013: projektuebergreifend melden (dev_agent, evidence,
triage_external, affects).
- Migration 014: supervisor_findings, Katalog, fuenf serverseitige
Detektoren im Tick.
- Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
- DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
- MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
von Fremddaten gegen Prompt-Injektion.
BEHOBEN
- observe und status im Supervisor legten den Zustand des Befundes als
"status" in die Antwort. Http::ok() merged ['status' => 'success']
darunter, also gewann der Befundzustand - eine angenommene Meldung kam
als status="open" an. Heisst jetzt finding_status. War Item #59.
Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
311 lines
9.9 KiB
PHP
311 lines
9.9 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Roadmap — Zustandsblock erzeugen und Prosa abgleichen.
|
|
*
|
|
* DAS PROBLEM, DAS DIESES SKRIPT LOEST
|
|
*
|
|
* 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:
|
|
*
|
|
* --write Der Zustandsteil der Datei wird ERZEUGT, nicht gepflegt.
|
|
* Zwischen den Markern kann nichts driften, weil dort niemand
|
|
* tippt. Ausserhalb der Marker wird nichts angefasst.
|
|
*
|
|
* --check Was von Hand geschrieben ist - die Prosa - wird gegen die
|
|
* Items geprueft. Verweist ein Absatz auf ein Item, das es
|
|
* nicht gibt? Gehoert es einem anderen Projekt? Fehlt zu einer
|
|
* zurueckgestellten Entscheidung die Begruendung?
|
|
*
|
|
* Aufrufe:
|
|
* php cli/roadmap.php --check --project=deploymentcenter
|
|
* php cli/roadmap.php --write --project=deploymentcenter
|
|
* php cli/roadmap.php --list --project=deploymentcenter
|
|
* php cli/roadmap.php --check --project=polytrader --file=../PolyTrader/ROADMAP.md
|
|
* php cli/roadmap.php --help
|
|
*
|
|
* Ohne --file wird docs/ROADMAP.md im Deploymentcenter-Verzeichnis genommen.
|
|
*
|
|
* Rueckgabewerte:
|
|
* 0 in Ordnung (bei --check: keine Fehler, Warnungen erlaubt)
|
|
* 1 Fehler im Abgleich oder Abbruch
|
|
* 2 nichts zu tun (Projekt ohne Roadmap-Eintraege)
|
|
*
|
|
* Gedacht fuer den Aufruf von Hand und aus scripts/deploy.py heraus. Das
|
|
* Projekt hat keine CI-Strecke; ein Skript, das man ausfuehren kann, ist
|
|
* deshalb die richtige Groesse - eine Pipeline waere ein eigenes Vorhaben.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Deploymentcenter\Core\Db;
|
|
use Deploymentcenter\Modules\Bugtracker\Roadmap;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Nur Kommandozeile. Wie bei tick.php: wirkt unabhaengig davon, ob die
|
|
// Webroot-Sperre greift.
|
|
// ---------------------------------------------------------------------------
|
|
if (PHP_SAPI !== 'cli') {
|
|
http_response_code(403);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
exit("Dieses Skript laeuft ausschliesslich auf der Kommandozeile.\n");
|
|
}
|
|
|
|
require_once __DIR__ . '/../src/bootstrap.php';
|
|
|
|
set_exception_handler(static function (\Throwable $e): void {
|
|
fwrite(STDERR, sprintf(
|
|
"[FEHLER] %s: %s\n %s:%d\n",
|
|
get_class($e),
|
|
$e->getMessage(),
|
|
$e->getFile(),
|
|
$e->getLine()
|
|
));
|
|
exit(1);
|
|
});
|
|
|
|
$options = parseArguments($argv);
|
|
|
|
if ($options['help']) {
|
|
printHelp();
|
|
exit(0);
|
|
}
|
|
|
|
$project = $options['project'];
|
|
if ($project === null) {
|
|
fwrite(STDERR, "Es fehlt --project=<slug>. Eine Roadmap gehoert zu genau einem Projekt.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$file = $options['file'] ?? (DC_ROOT . '/docs/ROADMAP.md');
|
|
|
|
$db = Db::init();
|
|
$roadmap = new Roadmap($db);
|
|
|
|
$mode = $options['mode'];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// --list: nur ansehen, nichts anfassen
|
|
// ---------------------------------------------------------------------------
|
|
if ($mode === 'list') {
|
|
$stats = $roadmap->stats($project);
|
|
|
|
if ($stats['total'] === 0) {
|
|
fwrite(STDOUT, sprintf("Projekt \"%s\" hat keine Roadmap-Eintraege.\n", $project));
|
|
exit(2);
|
|
}
|
|
|
|
fwrite(STDOUT, sprintf(
|
|
"%s — %d Vorhaben: %d offen, %d in Arbeit, %d erledigt, %d zurueckgestellt, %d verworfen\n\n",
|
|
$project,
|
|
$stats['total'],
|
|
$stats['pending'],
|
|
$stats['working'],
|
|
$stats['done'],
|
|
$stats['deferred'],
|
|
$stats['rejected']
|
|
));
|
|
|
|
foreach ($roadmap->epics($project) as $epic) {
|
|
fwrite(STDOUT, sprintf(
|
|
" [%s] %-40s %d Eintrag/Eintraege\n",
|
|
(string)$epic['epic'],
|
|
(string)$epic['title'],
|
|
(int)$epic['total']
|
|
));
|
|
}
|
|
|
|
fwrite(STDOUT, "\nAls Naechstes:\n");
|
|
foreach ($roadmap->nextUp($project, 5) as $i => $item) {
|
|
fwrite(STDOUT, sprintf(
|
|
" %d. [%s] #%d %s%s\n",
|
|
$i + 1,
|
|
(string)$item['epic'],
|
|
(int)$item['id'],
|
|
(string)$item['title'],
|
|
$item['claimed_by'] !== null ? ' (in Arbeit: ' . $item['claimed_by'] . ')' : ''
|
|
));
|
|
}
|
|
|
|
exit(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// --write: Zustandsblock erzeugen und einsetzen
|
|
// ---------------------------------------------------------------------------
|
|
if ($mode === 'write') {
|
|
$block = $roadmap->renderBlock($project);
|
|
|
|
if ($options['stdout']) {
|
|
fwrite(STDOUT, $block . "\n");
|
|
exit(0);
|
|
}
|
|
|
|
$existing = is_file($file) ? (string)file_get_contents($file) : '';
|
|
$updated = Roadmap::spliceBlock($existing, $block);
|
|
|
|
if ($existing === $updated) {
|
|
fwrite(STDOUT, sprintf("%s ist bereits auf dem Stand.\n", $file));
|
|
exit(0);
|
|
}
|
|
|
|
// Erst schreiben, dann melden. Ein fehlgeschlagenes file_put_contents
|
|
// duerfte nicht als Erfolg durchgehen - sonst haelt der naechste Lauf die
|
|
// Datei fuer aktuell.
|
|
if (file_put_contents($file, $updated) === false) {
|
|
fwrite(STDERR, sprintf("Konnte %s nicht schreiben.\n", $file));
|
|
exit(1);
|
|
}
|
|
|
|
$wasNew = strpos($existing, Roadmap::BLOCK_START) === false;
|
|
|
|
fwrite(STDOUT, sprintf(
|
|
"%s %s.\n",
|
|
$file,
|
|
$wasNew ? 'ergaenzt - der erzeugte Block wurde angehaengt' : 'aktualisiert'
|
|
));
|
|
|
|
if ($wasNew && $existing !== '') {
|
|
fwrite(STDOUT,
|
|
"Hinweis: Der Block steht am Dateiende. Verschiebe ihn samt Marker an die\n"
|
|
. " gewuenschte Stelle - kuenftige Laeufe finden ihn dort wieder.\n"
|
|
);
|
|
}
|
|
|
|
exit(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// --check: Prosa gegen die Items
|
|
// ---------------------------------------------------------------------------
|
|
$result = $roadmap->check($project, $file);
|
|
|
|
if ($options['json']) {
|
|
fwrite(STDOUT, json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n");
|
|
exit($result['ok'] ? 0 : 1);
|
|
}
|
|
|
|
fwrite(STDOUT, sprintf("Abgleich %s gegen Projekt \"%s\"\n", $file, $project));
|
|
fwrite(STDOUT, sprintf("%d Item-Verweis(e) in der Prosa geprueft.\n", $result['checked']));
|
|
|
|
// Ein veralteter Block ist kein Fehler - er ist eine Kopie, und Kopien altern.
|
|
// Gemeldet wird er trotzdem, damit niemand eine Woche alte Liste fuer den
|
|
// Stand von heute haelt.
|
|
if ($result['stale'] !== null) {
|
|
$age = time() - (int)strtotime($result['stale']);
|
|
if ($age > 86400) {
|
|
fwrite(STDOUT, sprintf(
|
|
"\n[Hinweis] Der erzeugte Block ist vom %s, also %d Tag(e) alt.\n"
|
|
. " `--write` bringt ihn auf den Stand. Der Live-Stand steht im DC.\n",
|
|
$result['stale'],
|
|
(int)floor($age / 86400)
|
|
));
|
|
}
|
|
} else {
|
|
fwrite(STDOUT,
|
|
"\n[Hinweis] Die Datei enthaelt keinen erzeugten Block. `--write` legt ihn an;\n"
|
|
. " bis dahin steht der Zustand nur im Deploymentcenter.\n"
|
|
);
|
|
}
|
|
|
|
if ($result['findings'] === []) {
|
|
fwrite(STDOUT, "\nKeine Abweichungen.\n");
|
|
exit(0);
|
|
}
|
|
|
|
$errors = 0;
|
|
$warnings = 0;
|
|
|
|
fwrite(STDOUT, "\n");
|
|
foreach ($result['findings'] as $finding) {
|
|
$isError = $finding['level'] === 'error';
|
|
$isError ? $errors++ : $warnings++;
|
|
|
|
fwrite($isError ? STDERR : STDOUT, sprintf(
|
|
" %s [%s] %s\n",
|
|
$isError ? 'FEHLER ' : 'Warnung',
|
|
$finding['code'],
|
|
$finding['message']
|
|
));
|
|
}
|
|
|
|
fwrite(STDOUT, sprintf("\n%d Fehler, %d Warnung(en).\n", $errors, $warnings));
|
|
|
|
exit($errors > 0 ? 1 : 0);
|
|
|
|
// ===========================================================================
|
|
// Hilfsfunktionen
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* @param list<string> $argv
|
|
* @return array{mode:string,project:?string,file:?string,json:bool,stdout:bool,help:bool}
|
|
*/
|
|
function parseArguments(array $argv): array
|
|
{
|
|
$options = [
|
|
'mode' => 'check',
|
|
'project' => null,
|
|
'file' => null,
|
|
'json' => false,
|
|
'stdout' => false,
|
|
'help' => false,
|
|
];
|
|
|
|
foreach (array_slice($argv, 1) as $arg) {
|
|
if ($arg === '--help' || $arg === '-h') {
|
|
$options['help'] = true;
|
|
} elseif ($arg === '--check') {
|
|
$options['mode'] = 'check';
|
|
} elseif ($arg === '--write') {
|
|
$options['mode'] = 'write';
|
|
} elseif ($arg === '--list') {
|
|
$options['mode'] = 'list';
|
|
} elseif ($arg === '--json') {
|
|
$options['json'] = true;
|
|
} elseif ($arg === '--stdout') {
|
|
$options['stdout'] = true;
|
|
} elseif (str_starts_with($arg, '--project=')) {
|
|
$options['project'] = substr($arg, 10);
|
|
} elseif (str_starts_with($arg, '--file=')) {
|
|
$options['file'] = substr($arg, 7);
|
|
} else {
|
|
fwrite(STDERR, sprintf("Unbekannte Option \"%s\". --help zeigt die Aufrufe.\n", $arg));
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
function printHelp(): void
|
|
{
|
|
fwrite(STDOUT, <<<'TEXT'
|
|
Roadmap — Zustandsblock erzeugen und Prosa abgleichen.
|
|
|
|
php cli/roadmap.php --check --project=<slug> [--file=<pfad>] [--json]
|
|
php cli/roadmap.php --write --project=<slug> [--file=<pfad>] [--stdout]
|
|
php cli/roadmap.php --list --project=<slug>
|
|
|
|
Was wo gepflegt wird:
|
|
|
|
Zustand Status, Reihenfolge, Zielfassung, Bearbeiter
|
|
-> Wahrheit ist das Deploymentcenter. Die Datei zeigt eine
|
|
erzeugte Kopie zwischen den Markern.
|
|
|
|
Begruendung Warum, verworfen, Ausloeser, Herleitung
|
|
-> Wahrheit ist die Datei, ausserhalb der Marker. Das Item
|
|
haelt nur den Verweis darauf (doc_anchor).
|
|
|
|
Kein Feld hat zwei Pfleger. Deshalb kann der Zustandsteil nicht driften -
|
|
und deshalb prueft --check nur die Prosa.
|
|
|
|
Rueckgabewerte: 0 in Ordnung, 1 Fehler, 2 nichts zu tun.
|
|
|
|
TEXT);
|
|
}
|