docs: Gerüst Plan Etappe 4d – Theme-Optionen und Schriften (Rulings, Schnittstellenvertrag)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 17:15:36 +02:00
co-authored by Claude Sonnet 5.5
parent 2307027eae
commit d1a0e77f28
@@ -0,0 +1,156 @@
# 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/<name>` (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/<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 11).
- 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 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<string, int|float|string|false>`); 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<string, int|float|string|false> 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<string, int|float|string|false> $set @param list<string> $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<string> exakte Familiennamen: hochgeladene + Google + Web-Safe, einmal je Request */
public function names(): array;
/** @return list<string> */ public function uploaded(): array;
/** @return list<string> 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<string> 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<string, string> $files Endung => Temp-Dateipfad @return list<string> 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<string>, 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<string, int|float|string|false> */ 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<array{key: string, label: string, action: string, before: mixed, after: mixed}> 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<string> $names @return list<string> 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 |
---
<!-- TASKS-PLACEHOLDER -->