feat(pipeline): redaktionelle Freigabe vor der Veroeffentlichung

Die Pipeline liess GPT Artikel umschreiben und legte sie direkt als
geplanten WordPress-Beitrag an - ohne dass ein Mensch sie gesehen hat.
Der KI-Hinweis auf dem Blog sagt aber redaktionelle Pruefung zu, und
genau daran haengt die Ausnahme in Art. 50 Abs. 4 KI-VO.

Neuer Status `pending_review` zwischen Rewrite und Publish: Die Pipeline
endet beim Rewrite, ohne WordPress-Beitrag und ohne Publish-Slot. Erst
die Freigabe im Portal stempelt Pruefer und Systemzeit, reserviert den
Slot und legt den Beitrag an.

- Migration: editorial_review_at/_by/_note, Status-CHECK erweitert
- Spalten-Migration laeuft nach den Tabellen-Neubauten erneut, sonst
  verwirft der aeltere no_image-Rebuild die frisch angelegten Spalten
- Jeder Weg nach `approved` stempelt (Button, Statuswechsel, API)
- Jeder maschinelle Rewrite loescht einen alten Stempel
- Telegram: Info mit Portal-Link statt Draft-Meldung, kein Freigabe-Button
- Altbestand bleibt unberuehrt und veroeffentlicht weiter
- EDITORIAL_REVIEW_REQUIRED=false stellt den alten Ablauf wieder her
- 14 neue Tests, docs/KI-VO.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Oliver 2026-08-24 10:35:49 +02:00
parent 682755c6a0
commit a233012887
No known key found for this signature in database
21 changed files with 1151 additions and 64 deletions

View file

@ -95,15 +95,16 @@ class TestArticleWorkflow(unittest.TestCase):
self.assertEqual(bad.status_code, 410)
@patch("backend.app.main.rewrite_article_text")
def test_rewrite_run_sets_publish_status(self, mock_rewrite) -> None:
def test_rewrite_run_sends_article_to_editorial_queue(self, mock_rewrite) -> None:
"""A rewrite no longer reaches `publish` by itself — a person has to sign off."""
mock_rewrite.return_value = "<h2>Neu</h2><p>Umschreibung</p>"
article_id = self._create_article()
self.client.post(f"/api/articles/{article_id}/transition", json={"target_status": "rewrite"})
r = self.client.post(f"/api/articles/{article_id}/rewrite-run")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()["status"], "publish")
self.assertEqual(r.json()["status"], "freigabe")
final = self.client.get(f"/api/articles/{article_id}")
self.assertEqual(final.json()["item"]["status_ui"], "publish")
self.assertEqual(final.json()["item"]["status_ui"], "freigabe")
if __name__ == "__main__":