From d1a0e77f2827e6072876c3f711093599252a1732 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 17:15:36 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Ger=C3=BCst=20Plan=20Etappe=204d=20?= =?UTF-8?q?=E2=80=93=20Theme-Optionen=20und=20Schriften=20(Rulings,=20Schn?= =?UTF-8?q?ittstellenvertrag)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- ...0-19-etappe-4d-theme-optionen-schriften.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-19-etappe-4d-theme-optionen-schriften.md diff --git a/docs/superpowers/plans/2026-10-19-etappe-4d-theme-optionen-schriften.md b/docs/superpowers/plans/2026-10-19-etappe-4d-theme-optionen-schriften.md new file mode 100644 index 0000000..3615a54 --- /dev/null +++ b/docs/superpowers/plans/2026-10-19-etappe-4d-theme-optionen-schriften.md @@ -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/` (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 | + +--- + +