diff --git a/docs/superpowers/plans/2026-11-06-kompatibilitaetsschicht.md b/docs/superpowers/plans/2026-11-06-kompatibilitaetsschicht.md new file mode 100644 index 0000000..367d970 --- /dev/null +++ b/docs/superpowers/plans/2026-11-06-kompatibilitaetsschicht.md @@ -0,0 +1,79 @@ +# Kompatibilitätsschicht für Fremd-Plugins (Phase 0–2) + +Stand 2026-11-06, Branch `feat/compatibility-layer`. Gate 1: voller Architektur-Entwurf (Benutzerentscheidung), weil Pods künftig +komplett über den Kern laufen soll und WP Super Cache direkt folgt. + +**Regeln für jeden Task:** zuerst ein fehlschlagender Test, dann die kleinste Änderung; nach jedem Task `composer test` grün und +`composer lint` ohne neue Befunde (zwei Warnungen bestehen schon: `wp-lit-mcp.php:6`, `RegisteredMetaSourceTest.php:78`). +`DiviBoundaryTest` und `CoreIndependenceTest` laufen immer mit. Ein roter Divi-Test heißt anhalten. Verschiebe-Tasks sind +mechanisch (Namespace + `use`), keine Verhaltensänderung. Keine Commits durch Umsetzungs-Agents (Gate 2 je Commit-Block). + +## Entscheidungen + +- Kern `src/Compatibility/` (Namespace `LIT\MCP\Compatibility`), importiert kein Divi (`CoreIndependenceTest`). +- `Capability` (`id()`, `label()`, `isWrite()`); `DiviFeature implements Capability`, `id()` = `value`. +- Kein eigenes `RequiresCapabilities`: Tools deklarieren Fähigkeits-IDs über das vorhandene `DeclaresRequirements`. +- Tier: `ReportOnly` (nie sperren), `LockOnFailure` (hartes Problem sperrt, sonst frei; `signatures:false`), `ReleasePerVersion` + (nur Divi; Matrix kommt per Closure aus `ReleaseGate::status()->matrix`). ReleaseStore, Resolver-Workflow, WriteProbe, CLI, + FeatureSets bleiben Divi-eigen. +- Gates: `CapabilityGate` überspringt IDs von `ReleasePerVersion`-Profilen, prüft IDs anderer Profile über `Compatibility::lockOf` + und sperrt IDs, die kein Profil kennt. `DiviFeatureGate` ignoriert Nicht-Divi-IDs wieder. Umstellung atomar in 1.13. +- Pods-IDs mit Präfix `pods-`, WPSC mit `wpsc-`; Divi-IDs bleiben unpräfixiert. Eindeutigkeitstest über alle Profile. +- Pods und WPSC werden `Integration`en ohne Abilities, die `ProvidesCompatibility` implementieren; leere Gruppen dürfen weder im + Tools-Tab noch in den Server-Anweisungen erscheinen. +- Admin: ein gemeinsamer Reiter „Compatibility“ (Slug `compatibility`); Divi-Freigabe wird Sektion darin + (`lit_mcp_compat_sections`). Kein Redirect vom alten Slug `divi`, `DiviTab` wird nicht umbenannt. +- WPSC: Hook `lit_mcp_changed` in `ChangeLog::record` (nur `result = ok`, eigenes try/catch); Listener nutzt `wp_cache_post_edit` + bzw. `wp_cache_clear_cache(blog)`; kein `lit/cache-clear`. + +## Phase 0 – Sicherheitsfixes (erledigt: 8f3a4e7, 234fd1e, 691231f) + +## Phase 1 – Kern, Pods, Reiter + +| # | Ziel und Dateien | Zuerst rot | Akzeptanz | Modell | +|---|---|---|---|---| +| 1.1 | `src/Compatibility/Capability.php`; `DiviFeature implements Capability` + `id()` | `DiviFeature::PageRead instanceof Capability`, `id()==='page-read'` | Divi-Tests grün | haiku | +| 1.2 | `ReleaseState`, `SourceFingerprint`, `LineDiff` nach `src/Compatibility`; `'Compatibility'` in `CORE_PATHS` | `CoreIndependenceTest::testEveryScannedPathExists`; verschobene Tests unter neuem Namespace | kein `Integrations\Divi`-Import im Kern | haiku | +| 1.3 | `Requirement` (`features(): list`), `Call` (+`accepts()` aus `ManifestCheck::accepts`, `signatureKey`), `ConstantRequirement`, `Mirror`, `SignatureFingerprint` verschieben; `ManifestCheck::accepts` delegiert | `CallTest` am neuen Ort; `ManifestCheckTest` Regression | Divi-Suite grün | sonnet | +| 1.4 | `Problem`, `FeatureStatus` verschieben (Capability); `Problem::fromArray(mixed, Closure(string): ?Capability)`; `ReleaseResolver::cached` übergibt `DiviFeature::tryFrom(...)`; Transient-Format bleibt | `ProblemTest` unbekannte ID → null; `ReleaseResolverTest` | Transient-Rundlauf unverändert | sonnet | +| 1.5 | `CompatibilityMatrix` generisch (`list`), `uniform`/`allIncompatible` mit Capabilities, `fromProblems(caps, problems, ?source, automatic)` aus `ReleaseResolver` (~89-129); `SOURCE_*` auf die Matrix, Aliase in `ReleaseStatus`; Aufrufer `ReleaseGate`, `ReleaseStatus`, `ReleaseResolver`, `DiviCheckCommand`, `tests/Support` | `CompatibilityMatrixTest` mit Fake-Capability `pods-x`; `fromProblems` gegen bisherigen Resolver-Fall | `ReleaseResolverTest`, `ReleaseStatusTest` unverändert grün | sonnet | +| 1.6 | `DiviAreas` (PAGES, LIBRARY, THEME_BUILDER, DESIGN, CATALOG) aus `Manifest` | `DiviAreasTest`; `ManifestTest` | reine Umbenennung | haiku | +| 1.7 | `Manifest` (Capabilities), `GlobalRequirement` (`isset($GLOBALS[name])`, hart), `ManifestCheck` nach `src/Compatibility`, ohne Divi-Defaults, Option `signatures`; `Divi\Release\DiviManifestCheck::create()`; Aufrufer `ReleaseResolver`, `CompatibilityCheck`, `DiviCompatibility`, Tests mit `new ManifestCheck()` | `ManifestCheckTest` mit `signatures:false` ohne reference-missing; `GlobalRequirement` fehlt = hartes Problem; `DiviManifestCheckTest` | Kern ohne Divi; Divi-Release-Tests unverändert | sonnet | +| 1.8 | `Tier`, `Profile` (id, label, tier, capabilities, manifests, ?matrix-Closure, ?version-Closure, ?minVersion, aktiv-Prüfung), `Compatibility` (matrix memoisiert, `lockOf`, `assert`, `usable`, `problems`, `capability(id)`, `active`) | `CompatibilityTest` mit Fake-Profilen je Tier, Memoisierung, unbekannte ID → null, minVersion sperrt alles | rein bis auf `defined()`/`get_file_data` | sonnet | +| 1.9 | `CapabilityLocked` (RuntimeException implements ToolRefusal, `compat_incompatible`, 423, details features/problems, Fabrik für unbekannte IDs) | `CapabilityLockedTest` | analog `DiviLockedTest` | sonnet | +| 1.10 | `CapabilityGate implements AbilityGuard` (Vorlage `DiviFeatureGate`), Log `refused` nur bei Schreibtools; noch nicht verdrahtet | `CapabilityGateTest`: Pods-ID gesperrt/frei, Divi-ID ignoriert, unbekannt gesperrt | annotate/outputSchema reichen durch | sonnet | +| 1.11 | `ProvidesCompatibility`, `IntegrationRegistry::compatibilities()` (auch nicht verfügbare), `DiviProfile::profile()`, `DiviIntegration implements ProvidesCompatibility` | `IntegrationRegistryTest`, `DiviIntegrationTest` | Divi neutral | sonnet | +| 1.12 | `PodsFeature` (`pods-schema-read`, `pods-field-read`, `pods-field-write`, `pods-field-permissions`), `PodsCompatibility::profile()` (LockOnFailure, PODS_VERSION, min 2.8, Calls mit Argumentzahlen gegen Pods 3.3.9.2), `PodsIntegration` ohne Abilities | `PodsCompatibilityTest`; Registry-Test: leere Integration nicht in `grouped()`/`labels()`/Instructions | keine leere Gruppe sichtbar | sonnet | +| 1.13 | `CompatibilityComponent` (3 Zustände, Zeile je Fähigkeit); `EnvironmentProbe` ohne Pods (TEC bleibt); `Plugin`: Guards `[DiviFeatureGate, CapabilityGate]`, Komponenten per Closure aus der Registry; 0.3-Umzug (`unknownIds` entfällt, Test zieht in `CapabilityGateTest`); ID-Eindeutigkeit | `EnvironmentProbeTest`; Zwei-Gate-Test über `AbilityRegistrar::argsFor`; `PluginTest` | Reihenfolge Divi, Pods, TEC | sonnet | +| 1.14 | `PodsFieldSource` + Schema-Wahl über `usable(PodsFeature::…)`; `isAvailable()` entfällt; Feldrechte über `usable(FieldPermissions)`, sonst fail-closed; 0.2-Bedingung = `active && ! usable(SchemaRead)` | `PodsFieldSourceTest` je Fähigkeit; `PluginTest` | alle Pods-Tests grün | sonnet | +| 1.15 | `CompatibilityTab` + `CompatibilitySection` (Filter `lit_mcp_compat_sections`, lazy); Zustand (Wort + Icon) und Probleme je Fähigkeit für Nicht-`ReleasePerVersion`-Profile | `CompatibilityTabTest`: drei Zustände, Sektion eingebettet, escaped | `CoreIndependenceTest` grün | sonnet | +| 1.16 | `DiviTab implements CompatibilitySection` (`title()` „Divi“); Slug/Redirects/`requested()`/Notice auf `compatibility`; `DiviIntegration` hängt über `lit_mcp_compat_sections` ein | `DiviTabTest`, `DiviIntegrationTest` | Release/Withdraw/Orphans/Render unverändert | sonnet | +| 1.17 | Links/Texte: `DiviCheckRoute`, `ReleaseStatus:24`, `docs/guides/overview.md:96`, `divi-release-smoke.sh`, Docblock `DiviCheckCommand` | `DiviCheckRouteTest` `tab=compatibility`; `ReleaseStatusTest` „→ Compatibility“ | DDEV-Smoke grün | haiku | +| 1.18 | Reiter in `Plugin::boot` registrieren (nach `LogTab`); Pods-Smoke erweitern | `PluginTest` Tab-Liste | DDEV: `compatibility ok`, Pods ohne Probleme; hohe minVersion → Pods-Felder schreibgeschützt | sonnet | + +## Phase 2 – WP Super Cache 3.1.5 + +| # | Ziel und Dateien | Zuerst rot | Akzeptanz | Modell | +|---|---|---|---|---| +| 2.1 | Hook `lit_mcp_changed(ability, objectType, objectId, logId)` in `ChangeLog::record`, nur bei `ok`, auch bei fehlgeschlagenem Insert; werfender Listener → `Log::failure` | `ChangeLogTest` | bestehende Fälle grün | sonnet | +| 2.2 | `SuperCacheFeature` (`wpsc-purge-post`, `wpsc-purge-all`), `SuperCacheCompatibility::profile()` (LockOnFailure, `WPCACHEHOME`, Version per `get_file_data`, `GlobalRequirement('cache_path')`), `SuperCacheIntegration` ohne Abilities | `SuperCacheCompatibilityTest` | Eindeutigkeitstest umfasst `wpsc-` | sonnet | +| 2.3 | `SuperCacheListener` an `lit_mcp_changed`: nur bei `usable()` (sonst Log-Zeile) und `$GLOBALS['cache_enabled']`; öffentlicher Posttyp außer attachment → `wp_cache_post_edit($id)`; sonst `wp_cache_clear_cache(is_multisite() ? get_current_blog_id() : 0)`; eigenes try/catch | `SuperCacheListenerTest` mit injizierten Closures | lint sauber | sonnet | +| 2.4 | Verdrahtung + Absatz in `docs/guides/overview.md` | `PluginTest` | `lit-site-status` listet WPSC | haiku | +| 2.5 | `tests/Integration/super-cache-smoke.sh` (Feld-Update leert Beitrag, Einstellung leert alles; `et_pb_layout` öffentlich?) | Smoke rot ohne 2.3 | DDEV grün | sonnet | + +## Commit-Schnitt + +4 `refactor(compat): Capability und DiviFeature` (1.1) · 5 `refactor(compat): reine Release-Klassen nach src/Compatibility` +(1.2–1.4) · 6 `refactor(compat): CompatibilityMatrix generisch mit fromProblems` (1.5) · 7 `refactor(compat): Manifest und +ManifestCheck generisch, DiviAreas, DiviManifestCheck` (1.6–1.7) · 8 `feat(compat): Tier, Profile, Compatibility, +CapabilityLocked, CapabilityGate` (1.8–1.10) · 9 `feat(compat): Profile aus Integrationen, Divi- und Pods-Profil` (1.11–1.12) · +10 `feat(compat): Gates, Status und Pods über den Kern verdrahten` (1.13–1.14) · 11 `feat(admin): Reiter „Compatibility“ mit +Divi als Abschnitt` (1.15–1.18) · 12 `feat(changelog): Hook lit_mcp_changed` (2.1) · 13 `feat(cache): WP Super Cache über den +Kern anbinden` (2.2–2.4) · 14 `test(cache): Integrations-Smoke` (2.5) + +## Risiken + +Vergessene `use`-Zeilen in Verschiebe-Tasks (voller Testlauf je Task); Lücke bei unbekannten IDs nur, wenn `CapabilityGate` +fehlt (PluginTest prüft beide Guards); leere Integrationen verändern Registry-Ausgaben (1.12 prüft zuerst); `signatures:false` +prüft nur Existenz und Argumentzahl; `wp_cache_post_edit` ist keine dokumentierte API (Wegfall sperrt nur den Purge, sichtbar +im Reiter und in `lit-site-status`). diff --git a/src/Compatibility/Capability.php b/src/Compatibility/Capability.php new file mode 100644 index 0000000..a5bbd89 --- /dev/null +++ b/src/Compatibility/Capability.php @@ -0,0 +1,23 @@ +value; + } + /** Whether the feature changes data (the seven *Write cases and FontUpload). */ public function isWrite(): bool { diff --git a/tests/Unit/Compatibility/CapabilityTest.php b/tests/Unit/Compatibility/CapabilityTest.php new file mode 100644 index 0000000..c00e3ed --- /dev/null +++ b/tests/Unit/Compatibility/CapabilityTest.php @@ -0,0 +1,37 @@ +id()); + self::assertSame('tb-write', DiviFeature::ThemeBuilderWrite->id()); + } + + public function testTheIdsAreUniqueAcrossTheDiviFeatures(): void + { + $ids = array_map(static fn (Capability $capability): string => $capability->id(), DiviFeature::cases()); + + self::assertCount(count(DiviFeature::cases()), array_unique($ids)); + } + + public function testWriteAndLabelStayAsTheyWere(): void + { + self::assertFalse(DiviFeature::PageRead->isWrite()); + self::assertTrue(DiviFeature::PageWrite->isWrite()); + self::assertSame('Seiten lesen', DiviFeature::PageRead->label()); + } +}