# Änderungsanfragen

So gelangt die Arbeit an einer Session nach der Überprüfung in den Standard-Branch.

Canonical page: https://beeuni.ai/docs/de/work/change-requests

Eine Änderungsanfrage (CR) führt einen Git-Branch mit einem anderen zusammen. Beeuni erstellt eine CR vom Branch einer Sitzung (`head_ref`) zum Standardbranch des Projekts (`base_ref`, normalerweise `main`). Der CR-Eintrag enthält Metadaten; Zusammenführung, Diff und Konfliktprüfungen erfolgen als echte Git-Operationen im Repository des Projekts. Eine CR ist der einzige Weg, wie Sitzungsarbeit in den Standardbranch gelangt.

<a id="why-work-goes-through-a-cr"></a>

## Warum die Arbeit über eine CR läuft

Eine Sitzung läuft in einer Sandbox auf einem eigenen Branch, der nach der Sitzungs-ID benannt ist. Die Sandbox besteht nicht ewig, der Branch aber schon: Git ist der einzige dauerhafte Datensatz der Arbeit einer Sitzung. Jede neue Sitzung beginnt beim Standardbranch. Bis eine CR zusammengeführt wird, bleibt die Arbeit auf ihrem eigenen Branch, ungeprüft und für alle anderen Agenten, Trigger und Mitwirkenden unsichtbar. Das gilt für jede Änderung: Code, Agenten, Skills und das Manifest (`beeuni.yaml`) — ohne Ausnahme.

<a id="the-agent-mandate"></a>

## Vorgaben für Agenten

Ein Agent muss eine CR eröffnen, um Änderungen in den Standardbranch des Projekts zu übernehmen. Der Agent führt seine eigene CR nicht zusammen — die Zusammenführung ist Sache des Benutzers. Halte dich an diese Vorgaben:

1. Committe auf dem Sitzungsbranch (`$BEEUNI_BRANCH_NAME`). Erstelle kleine, funktionierende Commits. Schreibe den Verlauf nicht um und erzwinge keinen Push.
2. Pushe den Branch: `git push origin HEAD`.
3. Eröffne die CR: `beeuni cr open --title "..." --description "..."`. In einer Sitzungs-Sandbox haben `--head` und `--session` standardmäßig den Wert `$BEEUNI_BRANCH_NAME` und `$BEEUNI_SESSION_ID`. `--base` ist standardmäßig der Standardbranch des Projekts.
4. Teile dem Benutzer die CR-Nummer mit, damit er sie überprüfen kann.
5. Halte an. Führe die CR nicht selbst zusammen.

<a id="anti-patterns"></a>

### Antimuster

- **Force-Push auf den Standardbranch.** Das verletzt die Review-Vereinbarung, selbst wenn das Backend es zulässt.
- **„Der Branch ist da, hol ihn dir selbst.“** Sobald die Sandbox beendet wird, ist der Sitzungsbranch weg, sofern er nicht vorher durch eine CR zusammengeführt wurde.
- **Die Änderung als Datei, eingefügten Text oder Archiv senden.** Das CR-System löst dieses Problem bereits.

<a id="data-model"></a>

## Datenmodell

CRs befinden sich in der Tabelle `change_requests`.

| Spalte | Typ | Hinweise |
| --- | --- | --- |
| `cr_id` | uuid | Primärschlüssel. Die Kennung der REST-API. |
| `project_id` | uuid | Das Projekt, zu dem die CR gehört. |
| `number` | integer | Anzeigenummer pro Projekt (`#1`, `#2`…). Innerhalb eines Projekts eindeutig. Wird nie wiederverwendet. |
| `title` | text | Erforderlich. |
| `description` | text | Standardmäßig eine leere Zeichenfolge. |
| `base_ref` | text | Der Branch, in den zusammengeführt wird. Normalerweise `main`. |
| `head_ref` | text | Der Branch, aus dem zusammengeführt wird. In einer Sitzung ist dies die Sitzungs-ID (eine UUID). |
| `status` | enum | `open`, `merged` oder `closed`. |
| `head_commit_sha` | text | Bei offenen CRs bei jedem Lesezugriff mit dem aktuellen `head_ref`-Tip aktualisiert. Bei zusammengeführten CRs zum Zeitpunkt der Zusammenführung erfasst. |
| `base_commit_sha` | text | Gleiche Regel, für `base_ref`. |
| `origin_session_id` | text | Die Sitzung, die die CR eröffnet hat. Wird auf null gesetzt, wenn diese Sitzung gelöscht wird. |
| `created_by` | uuid | Der Benutzer, der die CR erstellt hat. |
| `merged_at` / `merged_by` | timestamp / uuid | Wann und von wem die CR zusammengeführt wurde. |
| `merge_commit_sha` | text | Der Merge-Commit. Bei einem Fast-Forward entspricht er `head_commit_sha`. |
| `closed_at` / `closed_by` | timestamp / uuid | Wann und von wem die CR ohne Zusammenführung geschlossen wurde. |
| `metadata` | jsonb | Enthält `requested_changes`, eine Liste von `{text, by, at}`-Einträgen, die von `POST /:crId/request-changes` hinzugefügt wurden. CRs haben keine separate Kommentartabelle. |
| `created_at` / `updated_at` | timestamp | Wird bei der Erstellung gesetzt. Bei jeder Statusänderung oder SHA-Aktualisierung aktualisiert. |

Ein eindeutiger Index auf `(project_id, number)` ermöglicht es, eine CR über ihre Kurznummer statt über ihre UUID zu referenzieren.

<a id="lifecycle"></a>

## Lebenszyklus

```
open ──(merge)──▶ merged      (terminal)
open ──(close)──▶ closed ──(reopen)──▶ open
```

- `open` ist der Anfangsstatus.
- `closed` ist umkehrbar. `POST /:crId/reopen` setzt den Status auf `open` zurück.
- `merged` ist endgültig. Eine zusammengeführte CR kann nicht wieder geöffnet oder geschlossen werden. Eröffne stattdessen eine neue CR auf Basis des zusammengeführten Zustands.

Beeuni lehnt die Erstellung einer CR ab, wenn der Branch keine Commits enthält, die dem Standardbranch voraus sind. Das bedeutet meist, dass der Agent lokal committet, aber nie gepusht hat. Pushe die Commits und erstelle die CR erneut.

<a id="sha-refresh"></a>

## SHA-Aktualisierung

Bei einer offenen CR aktualisiert Beeuni `head_commit_sha` und `base_commit_sha` bei jedem `GET` anhand der aktuellen Branches. Ist das Repository nicht erreichbar oder fehlt ein Branch, überspringt Beeuni die Aktualisierung und liefert die zuletzt bekannten Metadaten der CR. Bei einer zusammengeführten CR bleiben die zum Zeitpunkt der Zusammenführung erfassten SHAs erhalten — Beeuni aktualisiert sie danach nie wieder.

<a id="merge-mechanics"></a>

## Zusammenführungsablauf

`POST /v1/projects/:projectId/change-requests/:crId/merge` führt die folgenden Schritte aus.

1. Beeuni liest das Manifest (`beeuni.yaml`) aus `head_ref` und validiert es anhand des Manifest-Schemas. Ein Branch ohne Manifest wird akzeptiert. Bei einem ungültigen Manifest wird `422` mit `code: "MANIFEST_INVALID"` zurückgegeben und die Zusammenführung abgebrochen.
2. Beeuni führt einen Fast-Forward von `base_ref` durch, wenn `head_ref` ihm strikt voraus ist.
3. Andernfalls erstellt Beeuni einen Merge-Commit. Die Standardnachricht lautet `Merge CR #<n>: <title>` und kann mit `message` im Anfrage-Body überschrieben werden. Autor des Commits ist `Beeuni <noreply@beeuni.com>`.
4. Bei einem Konflikt wird `409` mit der Konfliktliste zurückgegeben. Prüfe dieselbe Liste vor der Zusammenführung mit `GET /:crId/merge-preview`.
5. Bei Erfolg setzt Beeuni `status` auf `merged`, erfasst `merged_at`, `merged_by` und `merge_commit_sha` und invalidiert den Git-Cache des Projekts.

Der Versuch, eine CR zusammenzuführen, die nicht `open` ist, führt zu `409`.

<a id="merge-preview"></a>

### Vorschau der Zusammenführung

`GET /:crId/merge-preview` gibt Folgendes zurück:

| Feld | Typ | Bedeutung |
| --- | --- | --- |
| `base_sha` | string | Aktueller Tip von `base_ref`. |
| `head_sha` | string | Aktueller Tip von `head_ref`. |
| `merge_base` | string \| null | Gemeinsamer Vorfahr. Null, wenn die Verläufe nicht zusammenhängen. |
| `is_up_to_date` | boolean | `head_ref` ist vollständig in `base_ref` zusammengeführt. |
| `can_merge` | boolean | Keine Konflikte. |
| `can_fast_forward` | boolean | `head_ref` ist `base_ref` strikt voraus. |
| `conflicts` | string[] | Dateipfade, bei denen Konflikte auftreten würden. |

## REST-API

Alle Routen befinden sich unter `/v1/projects/:projectId/change-requests`.

| Methode | Pfad | Hinweise |
| --- | --- | --- |
| GET | `/` | `?status=open\|merged\|closed\|all`. Ohne Filter werden alle Status zurückgegeben. |
| POST | `/` | Body: `{title, description?, head_ref, base_ref?, session_id?}`. Gibt `201` zurück. |
| GET | `/:crId` | Gibt die CR zurück. Aktualisiert dabei auch die SHAs. |
| PATCH | `/:crId` | Bearbeitet `title` oder `description`. `409`, wenn nicht `open`. |
| GET | `/:crId/diff` | Vereinheitlichter Patch: Dateiliste, Ergänzungen und Löschungen. |
| GET | `/:crId/merge-preview` | Siehe „Vorschau der Zusammenführung“ oben. |
| POST | `/:crId/merge` | Body: `{message?}`. `422` bei einem ungültigen Manifest. `409` bei Konflikten oder wenn nicht `open`. |
| POST | `/:crId/close` | `409`, wenn bereits `merged`. |
| POST | `/:crId/reopen` | `409`, wenn nicht `closed`. |
| POST | `/:crId/request-changes` | Body: `{feedback}`. Fügt den Eintrag an `metadata.requested_changes` an und weckt den Agenten der ursprünglichen Sitzung. `409`, wenn nicht `open`. |

`POST /` lehnt eine `head_ref` ab, deren Branch keine Commits enthält, die `base_ref` voraus sind: `422 code: "CR_HEAD_NOT_AHEAD"`. Diesen Fehler sieht ein Agent, wenn er eine CR eröffnet, bevor er seinen Branch pusht.

<a id="authorization"></a>

### Autorisierung

Für Lesezugriffe ist Lesezugriff auf das Projekt erforderlich. Für Schreibzugriffe ist Schreibzugriff erforderlich. Jede Schreibaktion benötigt außerdem eine Berechtigung, die unten sowohl für ein vollständiges Token als auch für das eingeschränkte Token einer Sitzung angegeben ist.

| Aktion | Berechtigung |
| --- | --- |
| CR eröffnen | `project.gitops.push` |
| Änderungen anfordern | `project.review.act` |
| Zusammenführen | `project.gitops.merge` |

Für jede Aktion ist eine eigene Berechtigung erforderlich, sowohl bei vollständigen Tokens als auch bei eingeschränkten Sitzungstokens. Das Eröffnen und Zusammenführen sind getrennte Berechtigungen. Ein Token kann also Änderungsanfragen eröffnen, ohne sie zusammenführen zu dürfen — so wird die oben beschriebene Vorgabe für Agenten umgesetzt.

`project.cr.open` und `project.cr.merge` waren vor der Umstellung die Namen von `project.gitops.push` und `project.gitops.merge` — dieselbe Berechtigung unter einem zweiten Namen. Ein `beeuni.yaml`, das noch einen dieser Namen aufführt, funktioniert weiterhin (beim Auflösen wird die Berechtigung auf die aktuelle Schreibweise umgeschrieben). Verwende für neue Einträge jedoch den `gitops`-Namen.

Eine Sitzung kann niemals eine Änderungsanfrage zusammenführen, die sie selbst eröffnet hat, ungeachtet der erteilten Berechtigungen. Diese Regel ist strukturell und keine Berechtigung.
