Files
WP-LIT-MCP/docs/superpowers/plans/2026-10-19-etappe-4e-schleifen-guides-audit.md
T

16 KiB
Raw Blame History

Etappe 4e – Schleifen-Inhalte, Guides, Security-Audit, Version – Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Der KI-Assistent kann loop_*-Inhalte (z. B. Datum, Kategorien, Autor) in Modulen setzen, die in einem Divi-Loop-Element liegen oder selbst eines sind – mit Kontextprüfung aus dem Blockbaum, passenden Fehlermeldungen und der Kurzform {{loop:<option>}}; lit/divi-dynamic-sources zeigt die passenden Loop-Optionen je Knoten. Außerdem: Guides für alle Etappe-4-Werkzeuge, ein Security-Audit der ganzen Etappe mit Fix-Welle und der Versionssprung auf 0.7.0.

Architecture: Der Schleifen-Kontext wird im Blockbaum bestimmt (LoopContext: nächster Vorfahre-oder-selbst mit module.advanced.loop.desktop.value.enable === "on", Typ aus queryType, Standard post_types) und als reines Wertobjekt LoopScope an VariableRef/DynamicContentRules gegeben; dort erlaubt er nur loop_*-Optionen der passenden Art (Post/Term/Benutzer/Menü). Die Einspeisung liegt in den Abilities (Insert, Update, Move), nicht in den unteren Schichten: DiviComposition::inLoop() liefert eine Komposition mit Kontext. DynamicContentMap behält die loop_*-Optionen (heute verworfen) gruppiert nach Art; Divi-Aufrufe ändern sich nicht (DynamicContentOptions::get_options() liefert sie schon). Guides, Audit und Version sind Dokumentations-/Prozess-Tasks mit klaren Checklisten.

Tech Stack: PHP ≥ 8.1, WordPress 7.x, Divi 5.15.1 (nur über vorhandene Adapter), PHPUnit 9.6 + Brain Monkey + Mockery, phpcs, DDEV + bash/jq-Smokes.

Spec: docs/superpowers/specs/2026-10-16-etappe-4-designsystem-design.md (§9 Schleifen-Inhalte, §12 Audit, §14 Schritt 8–9, §1.3 Nr. 5–6, A3, A10, A11), Entscheidungen docs/superpowers/notes/2026-09-28-etappe-4-entscheidungen-entwurf.md (G11, G12), Recherchen 4e: docs/superpowers/notes/2026-10-19-etappe-4e-recherche-schleifen.md (Divi-Interna; Task 10 legt sie ab, bis dahin …/scratchpad/research-4e-loop-divi.md, …/research-4e-loop-plugin.md, …/research-4e-guides-audit-version.md – die Task-Autoren haben sie gelesen), Pläne 4c/4d als Vorbild; der Code ist maßgeblich, wo er von den Plänen abweicht.

Voraussetzung: Branch etappe-4 (= main), Stand 7678d22 (4a–4d fertig), 3368 Unit-Tests grün (composer test), composer lint sauber.

Teil von Etappe 4: 4a–4d (fertig) → 4e (dieser Plan) = Abschluss von Etappe 4.

Global Constraints

  • Dateien nur im Plugin-Verzeichnis /home/janniclas/Projekte/WordPress/wp-content/plugins/WP-LIT-MCP anlegen oder ändern. Integrationsskripte stellen im EXIT-Trap alles wieder her, was sie ändern (Seiten, Optionen).
  • PHP ≥ 8.1 ohne 8.2+-Features; WordPress ≥ 6.9; Divi 5.15.1. Namespace LIT\MCP\ → src/, PSR-4, eine Klasse pro Datei, jede Datei mit defined('ABSPATH') || exit;, declare(strict_types=1);, höchstens 120 Zeichen je Zeile. Nach jedem Task composer test und composer lint sauber (composer test = php -d memory_limit=512M vendor/bin/phpunit).
  • Divi-Grenze (Divi-Absicherung §3): Divi-Aufrufe nur in Adaptern aus DiviAdapters::CLASSES. 4e braucht keinen neuen Divi-Aufruf: DynamicContentMap ruft DynamicContentOptions::get_options() schon und behält jetzt nur mehr von dessen Ergebnis. Ergibt sich doch ein neuer Divi-Aufruf, Task anhalten und melden (NEEDS_CONTEXT). LoopScope, LoopContext und die Kontext-Teile der Regeln sind reines PHP (keine WordPress-/Divi-Aufrufe).
  • Sicherheitsgrenze Typabgleich: Ein loop_*-Wert ist nur erlaubt, wenn der Schleifen-Kontext des Knotens (Art) zur Art der Option passt. Das ist keine Komfortmeldung: In Divi liest loop_post_meta_key_* in einer Term-/Benutzer-/Menü-Schleife Post-Meta des Objekts mit derselben ID (und Benutzer-/Term-Meta in einer Post-Schleife umgekehrt) – ein Fehlabgleich würde fremde Daten ausgeben (Recherche 4e, Befund 1). Ohne Schleifen-Kontext ist jede loop_*-Option abgelehnt.
  • Rechte/Sperren unverändert: Alle Dynamic-Content-Einstellungen laufen weiter durch ValueCleaner/VariableRef/DynamicContentRules mit den Meta-Schlüssel-Regeln S29/S30 (auch für loop_*_meta_key_) und unfiltered_html-Regeln; die Release-Gate-Fähigkeiten der Tools ändern sich nicht (dynamic-sources ist schon überall deklariert, wo Inhalte entstehen).
  • Sprache: Meldungen, Hinweise, Guides, Notizen auf Deutsch mit echten Umlauten; neutral „KI-Assistent“, „MCP-Client“. Tool-Namen: direkte Tools lit-<name>, indirekte lit/<name> (ToolNamingTest).
  • Tests: Abilities über LIT\MCP\Tests\Unit\TestCase (Brain Monkey + Mockery, assertToolError); vorbelegte Stubs (wp_json_encode, is_wp_error, absint, wp_slash, wp_unslash, sanitize_text_field) nie mit Functions\expect() prüfen. Reine Klassen erben von PHPUnit\Framework\TestCase. TDD: Test zuerst, RED-Lauf, Code, GREEN; ein Test muss den Codepfad erreichen, den sein Name behauptet (keine vakuösen Assertions – Lehre aus 4c/4d). Echte Loop-Optionsnamen aus Divi 5.15.1 verwenden (die bisherigen Testdaten loop_post_meta_key u. ä. sind erfunden und werden in Task 2 korrigiert). Integration: cd /home/janniclas/Projekte/WordPress && timeout 900 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/<skript>.sh (läuft nur mit deaktivierten Plugins postlos und asynclistry; vorher Zustand mit ddev exec wp plugin list notieren und danach wiederherstellen).
  • Kein clean-code-review-Skill je Task (die globale CLAUDE.md-Regel zieht sonst die Antwort des Implementierers an sich); der Gesamtreview folgt am Ende (Task 10).
  • Commits: git commit -m "<Nachricht>" -m "Co-Authored-By: Claude <Modellname> <noreply@anthropic.com>" mit dem Namen des ausführenden Modells; nur die Dateien des Tasks stagen.

Entscheidungen für 4e (Rulings, verbindlich)

  • X1 Kontextregel (Divi 5.15.1): Der Schleifen-Kontext eines Knotens ist der des nächsten Vorfahren-oder-selbst, dessen attrs.module.advanced.loop.desktop.value.enable genau "on" ist (Tablet/Phone/Hover-Varianten und true zählen nicht – das liest auch Divi so). queryType fehlt → post_types. Verschachtelte Schleifen: die innerste gilt für alles darunter, egal welcher Art. Loop-Einstellungen, die nur über ein Preset kommen (LoopPresetAttrsMap), erkennt die Baum-Prüfung nicht – das ist eine dokumentierte Grenze (fail closed: ohne erkannte Schleife werden loop_* abgelehnt).
  • X2 Arten: post (post_types, current_page), term (post_taxonomies), user (user_roles), menu (menus), repeater (repeater_<acf-name>, nur falls die Recherche zeigt, dass es eigene Optionen gibt – sonst fällt repeater_* unter post oder wird abgelehnt; der Task-Autor entscheidet anhand der Recherchetabelle und dokumentiert es). Die Zuordnung Option → Art stammt aus der group der Divi-Option („Loop Terms“ → term, „Loop Users“ → user, alle übrigen loop_* → post, Menü nach Tabelle); die drei Meta-Schlüssel-Optionen (loop_post_meta_key_manual_custom_field, loop_user_…, loop_term_…) ordnet der Name zu, weil ihre Gruppennamen übersetzt sind. Gespeichert wird der Array-Schlüssel der Option (loop_term_name), nicht die interne id.
  • X3 Einspeisung in den Abilities: VariableRef und DynamicContentRules bekommen einen optionalen ?LoopScope (Standard LoopScope::none() → verhält sich wie heute, kein bestehender Aufruf bricht). DiviComposition::inLoop(LoopScope) liefert eine Komposition mit Kontext (Vorbild forPost()). Insert: Kontext der Zielposition (Eltern-Knoten, alle neuen Knoten eines Aufrufs teilen ihn, solange sie nicht selbst eine Schleife einschalten – ein neuer Knoten mit eigener loop-Einstellung im Aufruf schaltet den Kontext für seine Nachfahren um: der Task-Autor löst das über die neuen Attribute des Knotens, wenn nötig fail closed = Schleife zählt erst ab dem gespeicherten Zustand und der Fehler nennt das). Update: Kontext des bestehenden Knotens (Vorfahren aus dem Baum, plus eigene neue loop-Attribute desselben Aufrufs); ein Composer je Kontext (Ruling S31 „ein VariableRef je Komposition“ wird entsprechend umformuliert). Move: Kontext am neuen Ziel; ein Move, der vorhandene loop_*-Inhalte aus einer Schleife herausführt oder in eine Schleife anderer Art, erzeugt eine Warnung (Divi lässt das zu), keine Ablehnung (A11). lit-content-create und Theme-Builder-Anlage haben nie einen Schleifen-Kontext.
  • X4 Kurzform: {{loop:<option>}} (Option ohne Präfix, z. B. {{loop:post_date}} → loop_post_date) mit Einstellungen wie {{post:…}} (A10); die Tabelle LOOP in Placeholders ordnet den Kurznamen den Optionen zu; welche Optionen before/after tragen, kommt aus der DynamicContentMap.
  • X5 Fehlermeldungen (Spec §9): ohne Schleife: „Schleifen-Inhalt "%s" braucht eine Divi-Schleife in diesem Modul oder einem übergeordneten Element (Abschnitt, Zeile, Spalte, Gruppe).“; falsche Art: „… passt nicht zur Schleife dieses Elements (Typ: %s).“ (Typ deutsch: Beiträge, Terme, Benutzer, Menü). Die irreführende alte Meldung „nur innerhalb von Divi-Schleifen“ entfällt; Ruling S36 ist abgelöst.
  • X6 lit/divi-dynamic-sources: optional post_id + node (Pfad-ID): zusätzlich die zum Schleifen-Kontext dieses Knotens passenden loop_*-Optionen (Feldtypen wie die übrigen Optionen); lesen mit Leserecht auf die Seite (Muster lit-divi-node-get, DiviPageLoader); ohne Schleife im Kontext: Hinweis „kein Schleifen-Kontext“ statt Liste. Der Konstruktor bekommt den DiviPageLoader (Task 9 verdrahtet; die Tool-Fähigkeiten ändern sich nicht).
  • X7 Grenzen, dokumentiert statt gelöst: (a) module.advanced.loop ist als Attributpfad schreibbar – Schleifen ein-/ausschalten über attrs war schon vorher möglich und bleibt (A3 verbietet nur ein Anlegen/Konfigurieren-Werkzeug); (b) globale Module werden nicht betreten – loop_* in Bibliothekseinträgen hat keinen Schleifen-Kontext und wird abgelehnt; (c) StoredNodeGuard/Kopieren: kopierte Schleifen-Container mit loop_*-Kindern bleiben für Benutzer ohne unfiltered_html abgelehnt (fail closed), solange die Prüfung nur einen Kontext je Unterbaum kennt; (d) Presets, die eine Schleife einschalten, erkennt die Baumprüfung nicht (X1); (e) ACF-Wiederholungsfelder verlangen die Einstellung acf_type, die die Einstellungsregeln heute ablehnen – bleibt außen vor, wenn die Task-Autoren keinen sicheren Weg finden. Alle Punkte kommen in die Audit-Notiz (Task 8) und den Guide (Task 7).
  • X8 Guides (G11): keine Prompts, keine neue Guide-Datei ohne Not; Ablauf „Corporate Design übernehmen“ in workflows.md; divi-design-system.md bekommt Farben, Variablen, Theme-Optionen, Schriften, Restore je Bereich, Rechte; overview.md die vollständige Liste der indirekten Design-Tools und die neuen Fehlercodes (design_rev, rev_mismatch, conflict, in_use, font_*); divi-placeholders.md/divi-shortform.md {{loop:…}} und die Loop-Regeln. Die Inhalte folgen der Checkliste der Recherche (A) und dem echten Code der Tools.
  • X9 Audit (Task 8): Ein unabhängiger Audit (Opus, Skill anthropic-skills:security-audit wenn verfügbar, sonst eigenes Vorgehen) über den Umfang von Spec §12 (Preset-attrs/XSS, Font-Upload, Massen-Schreibrechte, Schleifen-Optionen/Meta-Schlüssel/Typabgleich, Snapshot-Restore) im Format der Audits 3a/3b (docs/superpowers/notes/2026-09-27-etappe-3a-security-audit.md); Befunde mit Schwere; kritische/hohe Befunde werden in einer Fix-Welle behoben, mittlere nach Urteil, der Rest steht als Restrisiko in der Notiz. Das DNS-Rebinding-Fenster zwischen allows() und cURL (W13) gehört in den Audit.
  • X10 Version: 0.6.1 → 0.7.0 (wp-lit-mcp.php:6, src/Plugin.php:48, readme.txt:7 plus Changelog-Abschnitt in readme.txt); kein Git-Tag, kein Update-Server, kein CHANGELOG; DiviReleases::TESTED bleibt (5.2.1, 5.14.0, 5.15.1). Spec/§9 nennt 5.14.0 – Task 10 berichtigt auf 5.15.1.

Schnittstellenvertrag (Namen und Signaturen, die spätere Tasks voraussetzen)

Klassen unter src/Integrations/Divi/Compose/ (Namespace LIT\MCP\Integrations\Divi\Compose), Tests unter tests/Unit/Integrations/Divi/Compose/, sofern nicht anders genannt. Task-Autoren dürfen Details verfeinern, aber nur mit Eintrag unter ## ABWEICHUNGEN am Ende ihres Abschnitts.

// LoopScope.php (Task 1) – reines PHP, unveränderlich
public const POST = 'post'; public const TERM = 'term'; public const USER = 'user'; public const MENU = 'menu';
public static function none(): self;
public static function ofQueryType(string $queryType): self;     // post_types|current_page → post, post_taxonomies → term, user_roles → user, menus → menu; unbekannt (repeater_… nach X2) → laut Task-Entscheidung
public function active(): bool;
public function kind(): ?string;                                  // null ohne Schleife
public function queryType(): ?string;
public function label(): string;                                  // deutsch für Meldungen: 'Beiträge' | 'Terme' | 'Benutzer' | 'Menü'

// LoopContext.php (Task 1) – reines PHP
/** @param array<string, mixed> $attrs Attribute eines Blocks */
public static function enabled(array $attrs): bool;               // desktop.value.enable === 'on' (X1)
public static function ownScope(array $attrs): ?LoopScope;        // Schleife an genau diesem Block, sonst null
/** @param list<array<string, mixed>> $attrChain Attribute von der Wurzel bis zum Knoten (inklusive) */
public static function fromChain(array $attrChain): LoopScope;    // innerste gewinnt
public static function of(NodeIndex $index, string $nodeId): LoopScope;     // Vorfahren-oder-selbst über die Pfad-ID (Namespace BlockTree\NodeIndex)

// DynamicContentMap (Pages/, Task 2) – loop_*-Optionen bleiben erhalten
/** @return array<string, array<string, mixed>> Option => Feldspezifikation, nur loop_*-Optionen der Art $kind */
public function loopFields(string $kind): array;                  // Name final nach Task 2; Task 3–5 nutzen genau diese Methode

// DynamicContentRules / VariableRef (Task 2) – optionaler Kontext
// VariableRef::__construct(DesignSystem $design, ?array $dynamicContent = null, ?LoopScope $loop = null)
// DynamicContentRules::__construct(DesignSystem $design, ?array $map, ?LoopScope $loop = null)

// DiviComposition (Pages/, Task 4)
public function inLoop(LoopScope $scope): self;                   // Klon mit Kontext, Vorbild forPost()

// Placeholders (Task 3): {{loop:<kurzname>}} → loop_<name>; Tabelle LOOP wie POST
// DiviDynamicSourcesAbility (Task 5): __construct(…, ?DiviPageLoader $loader = null); Eingabe post_id + node optional

Dateistruktur

Neu: Compose/LoopScope.php, Compose/LoopContext.php, tests/Integration/divi-loop-smoke.sh, ggf. Fixture unter tests/Fixtures/divi/ (echte Loop-Seite), Audit-Notiz docs/superpowers/notes/2026-10-19-etappe-4e-security-audit.md, Recherche-Notiz docs/superpowers/notes/2026-10-19-etappe-4e-recherche-schleifen.md.

Geändert: Compose/{DynamicContentRules,VariableRef,Placeholders}.php, Pages/{DynamicContentMap,DiviComposition,DiviInsertAbility,DiviUpdateAbility,DiviMoveAbility,DiviDynamicSourcesAbility,DiviWriter}.php (je nach Task), DiviIntegration.php (Loader für dynamic-sources), Guides (docs/guides/*.md), wp-lit-mcp.php, src/Plugin.php, readme.txt, Spec/Notizen, Tests der genannten Klassen (inkl. DynamicContentRulesTest, VariableRefTest, DiviFixtures).

Task-Übersicht

# Task Ergebnis
1 LoopScope, LoopContext reiner Schleifen-Kontext aus dem Baum
2 DynamicContentMap behält loop_*; DynamicContentRules/VariableRef mit Kontext; Meldungen; Fixtures korrigieren loop_* im passenden Kontext erlaubt, sonst Fehler
3 {{loop:…}} in Placeholders/Kurzform Kurzform
4 Kontext in Insert, Update, Move; Move-Warnung Werkzeuge nutzen den Kontext
5 lit/divi-dynamic-sources mit post_id + node Optionen je Knoten
6 Smoke gegen echtes Divi (Loop-Seite, Speichern, Rendern) divi-loop-smoke.sh
7 Guides Etappe-4-Werkzeuge und Loops dokumentiert
8 Security-Audit Etappe 4 + Fix-Welle Audit-Notiz, Fixes
9 Version 0.7.0, Spec-/Notiz-Nachträge, Verdrahtung Release-Stand
10 Gesamtverifikation, Gesamtreview, Memory Abschluss Etappe 4