# Etappe 4d – Designsystem: Theme-Optionen und Schriften – 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 liest und ändert die Design-Theme-Optionen von Divi (Schriften, Größen, Layout – `lit/divi-theme-options-get`, `lit/divi-theme-options-update`), lädt eigene Schriftdateien von einer URL hoch (`lit/divi-fonts-upload`), bekommt bei unbekannten Schriftnamen Vorschläge (auch bei Schrift-Variablen), und kann Snapshots des Bereichs `theme_options` wiederherstellen (`lit/divi-design-restore`) – mit Probelauf, `confirm`, Snapshot (Theme-Optionen) und Zurücklesen. **Architecture:** Die 4a-Pipeline `DesignChange` bleibt unverändert; neu sind die `DesignArea` `ThemeOptionsArea` mit reinen Klassen (`ThemeOptionSpec`, `ThemeOptionBook`, `ThemeOptionDiff`, `FontSuggest`) und drei kleinen Divi-Adaptern (`ThemeOptionsAdapter`, `FontCatalog`, `FontInstaller`) mit eigenen Fähigkeiten (`theme-option-read`, `theme-option-write`, `font-upload`). Divis Customizer-Standards und -Bereiche stehen nur als Literale in `et_divi_customizer_theme_settings()`; sie werden als Tabelle (`ThemeOptionSpec`) nachgebaut und über einen Mirror mit Fingerabdruck abgesichert. Der Font-Upload ist KEIN Pipeline-Bereich (keine Snapshots, Spec A2): eigener Probelauf/`confirm`-Ablauf, Download über einen aus `MediaImporter` herausgelösten, geteilten `RemoteFileFetcher`, Installation über Divis `FontsUtility::font_add()` im Prozess (`action: wp_handle_upload_rest`, wie Divis eigener KI-Pfad). **Tech Stack:** PHP ≥ 8.1, WordPress 7.x, Divi 5.15.1 (`et_update_option`/`et_delete_option`, Customizer-Sanitizer, `et_builder_get_fonts`/`_google_fonts`/`_websafe_fonts`/`_custom_fonts`, `FontsUtility::font_add/font_remove`, `HooksRegistration::check_filetype_and_ext_font`, `et_pb_is_allowed` – nur über die drei neuen Adapter), PHPUnit 9.6 + Brain Monkey + Mockery, phpcs (WPCS, PHPCompatibilityWP), DDEV + bash/jq-Smokes. **Spec:** `docs/superpowers/specs/2026-10-16-etappe-4-designsystem-design.md` (§3, §4, §5, §8, §10, §11, §13, §14 Schritt 7), Entscheidungen `docs/superpowers/notes/2026-09-28-etappe-4-entscheidungen-entwurf.md` (G3, G6, G10, A1, A2, A8, A9 – verbindlich, soweit unten nicht durch Rulings W1–W14 geändert), Datenformate `docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md` §4 und der Nachtrag 4d in Task 11, Recherche 5.15.1: `docs/superpowers/notes/2026-10-19-etappe-4d-divi-recherche.md` (Task 11 legt sie ab; bis dahin die Kopie `…/scratchpad/research-4d-divi.md` des Planers – die Task-Autoren haben sie gelesen), Plan 4c (`2026-10-19-etappe-4c-farben-variablen.md`) als Vorbild; der Code ist maßgeblich, wo er von den Plänen abweicht. **Voraussetzung:** Branch `etappe-4` (= `main`), Stand 2307027 (4a, 4b, 4c fertig), 3121 Unit-Tests grün (`composer test`), `composer lint` sauber. **Teil von Etappe 4:** 4a, 4b, 4c (fertig) → **4d (dieser Plan)** → 4e Schleifen-Inhalte, Guides, Audit, Version. ## 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 (`et_divi` byte-genau über serialize/base64, hochgeladene Schriften inkl. Dateien über `FontsUtility::font_remove`, Seiten, Variablen). - 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 Klassen aus `DiviAdapters::CLASSES` mit Manifest; jedes Divi-Symbol steht mit Fähigkeiten im Manifest; `DiviBoundaryTest` und `DiviFeatureAssignmentTest` bleiben grün (die angepasste Tabelle gehört in denselben Task wie die Manifest-Änderung); neue Signaturen: `fingerprints.json` über `wp lit-mcp divi-check --update-reference` in DDEV neu erzeugen (Befehl und Prüfung wie in Task 2 des Plans 4c). Alle übrigen neuen Klassen sind **reines PHP** ohne WordPress- und Divi-Aufrufe; Ausnahmen nennt der Task ausdrücklich. - **Pipeline (Spec §4):** Theme-Optionen laufen durch `DesignChange::run()` (Rechte → Eingabe prüfen, alle Fehler gesammelt als `validation_failed` mit Feldpfaden → Probelauf mit `diff`, `design_rev`, `warnings` → mit `confirm: true` Sperre, Snapshot `theme_options`, Schreiben, Zurücklesen + `verify`, Protokoll). Der Font-Upload nutzt die Pipeline nicht (kein Snapshot, A2), hat aber Probelauf + `confirm`, Rechte vorab, Rollback und Protokoll. - **Werte:** Jeder Wert einer Theme-Option wird vom Plugin geprüft (`ThemeOptionSpec::check`: Typ, Bereich, Auswahl) UND durch Divis eigenen Customizer-Sanitizer des Schlüssels geschickt (`ThemeOptionsAdapter::sanitize`); `false` oder ein vom Eingabewert abweichendes Ergebnis ist ein Validierungsfehler (Divi würde `false` stillschweigend speichern bzw. runden). Die fünf Hauptfarben, Button-Standards (`all_buttons_*`), Logo und SEO bleiben außen (G3, A8). - **Rechte:** Theme-Optionen schreiben und Restore: `edit_theme_options` **und** `et_pb_is_allowed('theme_customizer')` (Divis Customizer-Regel); Lesen (`theme-options-get`): wie `lit/divi-design-system` (`edit_posts`). Font-Upload: `upload_files` **und** `edit_theme_options` **und** `et_pb_is_allowed('custom_fonts_management')` (Divis eigene Regel `CustomFontController::store_permission()` plus die Plugin-Regel; Spec §10 nannte irrtümlich `use_visual_builder`). - **Exposition:** Neue Tools sind indirekt (über `mcp-adapter-execute-ability`), Gruppe `divi`, Annotations wie die Preset-Tools (`write`/`destructive`); Texte nennen indirekte Tools `lit/` (ToolNamingTest); `docs/guides/overview.md` „Direkt sichtbar:“ bleibt unverändert; Guides folgen gesammelt mit 4e. - **Sprache:** Meldungen, Hinweise, Beschreibungen auf Deutsch mit echten Umlauten; neutral „KI-Assistent“, „MCP-Client“. - **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. Adapter-Tests ersetzen Divi-Klassen per `Mockery::mock('alias:…')` mit `@runInSeparateProcess` (Vorbild `GlobalColorsAdapterTest`). Integration: `cd /home/janniclas/Projekte/WordPress && timeout 900 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/.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 11). - Commits: `git commit -m "" -m "Co-Authored-By: Claude "` mit dem Namen des ausführenden Modells; nur die Dateien des Tasks stagen. ## Entscheidungen für 4d (Rulings, verbindlich) - **W1 Drei Fähigkeiten:** `DiviFeature::ThemeOptionRead` (`theme-option-read`, „Theme-Optionen lesen“), `ThemeOptionWrite` (`theme-option-write`, „Theme-Optionen schreiben“, schreibend), `FontUpload` (`font-upload`, „Schriften hochladen“, schreibend). `ThemeOptionsArea::feature()` = `ThemeOptionWrite`. Der Mirror `customizer-typography` (Standards/Bereiche der 20 Settings) trägt `theme-option-read` und `-write`; die Schriftlisten-Aufrufe tragen `theme-option-write`, `font-upload` und `variable-write` (die Schrift-Variablen werden gegen dieselbe Liste geprüft; ein Bruch sperrt bewusst alle drei). - **W2 Whitelist (Spec §8.1, G3):** `heading_font`, `body_font`, `primary_nav_font`, `secondary_nav_font`, `heading_font_weight`, `body_font_weight`, `body_font_size`, `tablet_body_font_size`, `phone_body_font_size`, `body_header_size`, `tablet_header_font_size`, `phone_header_font_size`, `body_font_height`, `body_header_height`, `body_header_spacing`, `body_header_style`, `content_width`, `gutter_width`, `section_padding`, `row_padding`. Standards, Bereiche und Sanitizer-Namen je Schlüssel stehen in der Recherche (Tabelle A) und werden in `ThemeOptionSpec` als Literale nachgebaut; der Mirror auf `et_divi_customizer_theme_settings()` (`functions.php`) sichert sie. `shortcut:` der Mirror hasht die ganze Funktion und schlägt bei jeder Customizer-Änderung an; bei zu häufigem Anschlagen auf einen engeren Ausschnitt umstellen (im Code kommentieren). - **W3 Unset statt `'none'`:** Ein Schlüssel ist „nicht gesetzt“ = Divi-Standard. Der Eingabewert `null` setzt zurück (Divi: `et_delete_option`); `'none'` und `false` werden abgelehnt (Divi speichert die Standardschrift als `false`, ein gespeichertes `'none'` wäre in Divi 5 ein echter Schriftname). Tablet-/Phone-Größen erben, solange sie nicht gesetzt sind (effektiver Standard = der größere Wert); `get` nennt den effektiven Wert und `default`. - **W4 Bereiche prüft das Plugin:** Divis Sanitizer (`absint`, `intval`, `floatval`, Auswahl-Sanitizer → `false`) klemmen nicht; `ThemeOptionSpec` prüft min/max/step/choices der Customizer-Controls. `body_header_style` nimmt genau einen Wert (`italic`, `uppercase`, `underline`) oder `null` (Divis Sanitizer lehnt Kombinationen ab). Zahlen dürfen als Zahl oder Ziffern-String kommen; gespeichert wird der Typ, den Divis Sanitizer liefert (int/float/String). - **W5 Schriftnamen:** gültig ist der exakte Familienname (Groß-/Kleinschreibung) aus `array_merge(et_builder_get_custom_fonts(), et_builder_get_fonts())` (so prüft Divis Sanitizer). Unbekannt → `validation_failed` mit bis zu 5 ähnlichen Namen (`FontSuggest`, ohne Beachtung der Groß-/Kleinschreibung: Präfix, Teilstring, `levenshtein`). Die Liste wird je Request einmal geladen (`et_builder_get_google_fonts()` lädt 1,3 MB und kann einmal je Tag einen HTTP-Abruf auslösen). Gilt für `heading_font`, `body_font`, `primary_nav_font`, `secondary_nav_font` und Variablen vom Typ `fonts`. Schriftnamen in Preset-/Modul-Attributen werden in 4d NICHT geprüft (Folgepunkt für 4e, in der Notiz festgehalten). - **W6 Snapshot `theme_options`:** `raw` = genau die vorhandenen Whitelist-Schlüssel aus `get_option('et_divi')` mit Rohwert und Typ (`array`); ein fehlender Schlüssel fehlt im Array. `ThemeOptionBook` normalisiert (nur Whitelist, Schlüsselreihenfolge fest) und liefert `rev()`. Schreiben/Restore: Schlüssel im neuen Bestand → `et_update_option`, Whitelist-Schlüssel, die im neuen Bestand fehlen → `et_delete_option`; danach die Caches wie ein Customizer-Speichern (`ET_Core_PageResource::remove_static_resources('all', 'all')` und `et_pb_force_regenerate_templates()`). Zurücklesen über `get_option('et_divi')` (nicht über `GlobalData::get_customizer_fonts()`, das `static` cacht). Restore ist ein vollständiges Ersetzen der Whitelist (`mayRemove = true`). Lost-Update-Risiko von `et_update_option` (schreibt das ganze Array aus dem Request-Global) ist dokumentiert; `design_rev` und Sperre mindern es. - **W7 Keine Fundstellen:** Theme-Optionen haben keinen Seitenbezug; der Probelauf liefert kein `affected` und die Warnung „wirkt auf die gesamte Website“ (bei Schriften zusätzlich „wirkt auch auf den Builder“). `DesignPlan::usages` bleibt `null`. - **W8 Font-Upload-Weg:** `FontInstaller::install()` ruft `FontsUtility::font_add($files, $name, $settings, ['action' => 'wp_handle_upload_rest'])` mit Temp-Dateien (`wp_tempnam`, wie `AiAgentImageUtility::install_to_custom_fonts()`); jede Datei vorab durch `HooksRegistration::check_filetype_and_ext_font()`; nach `font_add` werden die gelieferten `font_file`-Schlüssel gegen die Eingabe verglichen (Divi meldet Teilerfolg als Erfolg) – Abweichung → `FontsUtility::font_remove($name)` und Fehler. Statt eines Nachbaus des `et_uploaded_fonts`-Eintrags (Spec §8.2) ein Mirror `font-install` auf `AiAgentImageUtility::install_to_custom_fonts` (Aufrufkonvention). Erlaubte Formate: `woff2`, `woff`, `ttf`, `otf` (kein `eot`); Gewichte `all` oder Liste aus `100…900` (Plugin prüft, Divi nicht); `generic_family` aus `serif|sans-serif|monospace|cursive|fantasy` (Standard `sans-serif`; Divi prüft nicht). - **W9 Font-Namen und -Dateien:** Name: 1–100 Zeichen, nur `A–Z a–z 0–9`, Leerzeichen, `_`, `-` (Divi entfernt andere Zeichen stillschweigend – das Plugin lehnt ab und nennt den bereinigten Namen), nicht in Googles, Web-Safe- oder bereits hochgeladenen Schriften (ohne Beachtung der Groß-/Kleinschreibung; strenger als Divi). Dateien: höchstens `min(Limits::uploadBytes(), 5 MB)` je Datei (A9), 1–4 Dateien (je Format höchstens eine), Magic Bytes (`FontSignature`: `wOF2`, `wOFF`, `00 01 00 00`/`true` = ttf, `OTTO` = otf) müssen zur Endung der Eingabe passen; URL-Regeln (nur öffentliche Hosts, Weiterleitungen geprüft, Zeitlimit) wie `lit/media-upload-from-url` über den geteilten `RemoteFileFetcher`. - **W10 Probelauf des Uploads:** ohne `confirm` lädt das Tool die Dateien wirklich herunter (nur Temp-Dateien, danach gelöscht), prüft alles und antwortet mit `Errors::confirmRequired` (Name, Dateien mit Format/Größe/SHA-256, Gewichte, Familie, „wirkt auf …“-Hinweis). Mit `confirm: true` lädt es erneut, prüft erneut und installiert; das Protokoll nennt die SHA-256-Werte. Kein Snapshot; Rollback bei Fehler über `font_remove`. - **W11 Restore:** `DiviDesignRestoreAbility` bekommt `ThemeOptionsRestorePlanner` (`area()` `theme_options`); die Meldung „Theme-Optionen folgen mit Etappe 4d“ entfällt (ein unbekannter Bereich bleibt `unsupported`); Rechte und Fähigkeiten wie beim Schreiben. - **W12 Schrift-Variablen:** `DiviVariablesSetAbility` prüft Variablen vom Typ `fonts` zusätzlich gegen `FontCatalog` (W5); `VariableValue` bleibt rein (die Listenprüfung liegt in der Ability, `VariableValue.php:12` „folgt mit 4d“ wird berichtigt). - **W13 Divi-Beschränkungen als Grenzen dokumentieren:** `et_get_option`-/`et_update_option`-Lost-Update, `body_header_style`-Einzelwert, fehlende Bereichsprüfung in Divi, `font_add`-Teilerfolg, `remove_static_resources` wirkungslos ohne angemeldeten Benutzer (der Prozess hat einen, Smokes laufen mit `--user`). - **W14 Rechte-Texte:** `DesignRights::themeOptions(DesignSystemWriter, ThemeOptionsAdapter)` und `DesignRights::fonts(FontCatalog)` entsprechen den Rechten oben; Fehlertexte nennen das fehlende Recht und die Divi-Rolle („Theme-Anpassung“/„Eigene Schriften verwalten“). ## Schnittstellenvertrag (Namen und Signaturen, die spätere Tasks voraussetzen) Alle Klassen unter `src/Integrations/Divi/Design/` (Namespace `LIT\MCP\Integrations\Divi\Design`), Tests unter `tests/Unit/Integrations/Divi/Design/` (Namespace `LIT\MCP\Tests\Unit\Integrations\Divi\Design`), sofern nicht anders genannt. Task-Autoren dürfen Details verfeinern, aber nur mit Eintrag unter `## ABWEICHUNGEN` am Ende ihres Abschnitts. ```php // Release/DiviFeature.php (Task 1) case ThemeOptionRead = 'theme-option-read'; // label 'Theme-Optionen lesen' case ThemeOptionWrite = 'theme-option-write'; // label 'Theme-Optionen schreiben', isWrite() = true case FontUpload = 'font-upload'; // label 'Schriften hochladen', isWrite() = true // ThemeOptionsAdapter.php (Task 2) – class (nicht final) implements DiviAdapter, Manifest::DESIGN public static function manifest(): Manifest; /** @return array vorhandene Whitelist-Schlüssel aus get_option('et_divi') */ public function read(): array; /** Divis Customizer-Sanitizer des Schlüssels; false = Divi lehnt ab. @throws InvalidArgumentException unbekannter Schlüssel */ public function sanitize(string $key, mixed $value): mixed; /** @param array $set @param list $unset @throws RuntimeException wenn das Zurücklesen nicht stimmt */ public function write(array $set, array $unset): array; // et_update_option / et_delete_option, dann Caches, liefert read() public function canCustomize(): bool; // et_pb_is_allowed('theme_customizer'), fail closed ohne Divi // FontCatalog.php (Task 2) – class (nicht final) implements DiviAdapter /** @return list exakte Familiennamen: hochgeladene + Google + Web-Safe, einmal je Request */ public function names(): array; /** @return list */ public function uploaded(): array; /** @return list Google + Web-Safe + hochgeladen (Kollisionsprüfung beim Upload) */ public function reserved(): array; public function has(string $name): bool; public function canManage(): bool; // et_pb_is_allowed('custom_fonts_management') // FontInstaller.php (Task 8) – class (nicht final) implements DiviAdapter /** @return list Formate, die Plugin UND Divi (mime_types_font) erlauben: woff2, woff, ttf, otf */ public function formats(): array; public function accepts(string $path, string $filename): bool; // HooksRegistration::check_filetype_and_ext_font /** @param array $files Endung => Temp-Dateipfad @return list installierte Endungen @throws RuntimeException */ public function install(string $name, array $files, string $weights, string $family): array; public function remove(string $name): void; // DesignRights.php (Task 2) – zusätzlich public static function themeOptions(DesignSystemWriter $writer, ThemeOptionsAdapter $options): ?WP_Error; public static function fonts(FontCatalog $fonts): ?WP_Error; // upload_files + edit_theme_options + custom_fonts_management // ThemeOptionSpec.php (Task 3) – reines PHP public const KEYS = [ /* die 20 Schlüssel in der Reihenfolge von W2 */ ]; /** @return array{type: 'int'|'float'|'font'|'weight'|'style', label: string, default: int|float|string|null, min?: int|float, max?: int|float, step?: int|float, choices?: list, inherits?: string} */ public static function definition(string $key): array; public static function isKey(string $key): bool; public static function check(string $key, mixed $value): ?string; // null = gültig (Typ/Bereich/Auswahl, ohne Schriftliste), sonst deutsche Meldung public static function normalize(string $key, mixed $value): int|float|string; // "15" → 15, "1.7" → 1.7 public static function effective(string $key, array $stored): int|float|string|null; // gespeicherter Wert, sonst vererbter/Standardwert (W3) // ThemeOptionBook.php (Task 3) – reines PHP, unveränderlich public static function fromDivi(array $raw): self; // nur Whitelist, feste Reihenfolge /** @return array */ public function data(): array; public function rev(): string; // Rev::of wie PresetBook/ColorBook public function has(string $key): bool; public function get(string $key): int|float|string|false|null; public function with(string $key, int|float|string|false $value): self; public function without(string $key): self; // ThemeOptionDiff.php (Task 3) – reines PHP /** @return list action: added|changed|reset */ public static function between(ThemeOptionBook $before, ThemeOptionBook $after): array; public static function summary(array $diff): string; // 'Theme-Optionen: 1 gesetzt, 2 geändert, 1 zurückgesetzt.' / '… keine Änderung.' // FontSuggest.php (Task 3) – reines PHP /** @param list $names @return list höchstens $limit ähnliche Namen */ public static function similar(string $name, array $names, int $limit = 5): array; // ThemeOptionsArea.php (Task 4) – implements DesignArea: name 'theme_options', label 'Theme-Optionen', feature ThemeOptionWrite public function __construct(ThemeOptionsAdapter $options); // DiviThemeOptionsGetAbility.php: __construct(ThemeOptionsAdapter, ?FontCatalog = null) – lit/divi-theme-options-get // DiviThemeOptionsUpdateAbility.php: __construct(DesignSystemWriter, ThemeOptionsAdapter, FontCatalog, DesignChange) – lit/divi-theme-options-update // RemoteFileFetcher.php (Task 7) – src/Integrations/Media/RemoteFileFetcher.php (Namespace LIT\MCP\Integrations\Media) – aus MediaImporter herausgelöst // FontSignature.php (Task 8) – reines PHP: public static function detect(string $header): ?string; // 'woff2'|'woff'|'ttf'|'otf' // FontNameRules.php (Task 8) – reines PHP: public static function check(string $name, array $reserved): ?string; // null = gültig // DiviFontsUploadAbility.php (Task 8): __construct(FontCatalog, FontInstaller, RemoteFileFetcher, ChangeLog, …) – lit/divi-fonts-upload // RestorePlanner (Task 6) – ThemeOptionsRestorePlanner implements RestorePlanner (area 'theme_options') ``` ## Dateistruktur **Neu (`src/Integrations/Divi/Design/`):** `ThemeOptionsAdapter`, `FontCatalog`, `FontInstaller`, `ThemeOptionSpec`, `ThemeOptionBook`, `ThemeOptionDiff`, `FontSuggest`, `ThemeOptionsArea`, `DiviThemeOptionsGetAbility`, `DiviThemeOptionsUpdateAbility`, `ThemeOptionsRestorePlanner`, `FontSignature`, `FontNameRules`, `DiviFontsUploadAbility`; **neu** `src/Integrations/Media/RemoteFileFetcher.php`. **Geändert:** `Release/DiviFeature.php`, `Release/DiviAdapters.php`, `Release/fingerprints.json`, `Design/DesignRights.php`, `Design/DiviDesignRestoreAbility.php`, `Design/DiviVariablesSetAbility.php`, `Design/VariableValue.php` (nur Kommentar), `src/Integrations/Media/MediaImporter.php`, `DiviIntegration.php`, Notizen/Spec (Task 11). **Neu (Tests):** je Klasse eine Testdatei; `tests/Integration/divi-theme-fonts-smoke.sh`; ggf. eine kleine Schrift-Fixture unter `tests/Fixtures/`. **Geändert (Tests):** `DiviFeatureTest`, `DiviFeatureAssignmentTest`, `DiviToolFeaturesTest`, `DiviIntegrationTest`, `DiviDesignRestoreAbilityTest`, `DesignRightsTest`, `DiviVariablesSetAbilityTest`, `MediaImporterTest` (und was `grep -rn "PresetWrite\|preset-write\|VariableWrite\|variable-write" tests` an Fähigkeitszählungen findet). ## Task-Übersicht | # | Task | Ergebnis | |---|---|---| | 1 | Fähigkeiten `ThemeOptionRead`/`ThemeOptionWrite`/`FontUpload` | Enum, Label, Tests (Commit gemeinsam mit Task 2) | | 2 | `ThemeOptionsAdapter`, `FontCatalog`, Manifest, Fingerabdrücke, `DesignRights` | Divi-Grenze für Lesen/Schreiben/Schriftlisten | | 3 | `ThemeOptionSpec`, `ThemeOptionBook`, `ThemeOptionDiff`, `FontSuggest` | reiner Bestand der Theme-Optionen | | 4 | `ThemeOptionsArea`, `lit/divi-theme-options-get`, `lit/divi-theme-options-update` | Theme-Optionen lesen/schreiben | | 5 | Schriftprüfung der Schrift-Variablen | `lit/divi-variables-set` prüft Typ `fonts` | | 6 | `ThemeOptionsRestorePlanner` | Restore für `theme_options` | | 7 | `RemoteFileFetcher` aus `MediaImporter` herauslösen | geteilter Download (Refactor, Verhalten gleich) | | 8 | `FontSignature`, `FontNameRules`, `FontInstaller`, `lit/divi-fonts-upload` | Schriften hochladen | | 9 | Verdrahtung | `DiviIntegration`, Tabellen der Fähigkeiten | | 10 | Smoke gegen echtes Divi | `divi-theme-fonts-smoke.sh` | | 11 | Notizen, Recherche ablegen, Spec, Gesamtverifikation | Formate-Nachtrag 4d, Spec §8/§10, Review, Memory | ---