feat: write results warn about sections built like a library section (spec §5.5)
SimilarSections compares inserted sections (not global instances) with at most 200 editable library section entries by structure fingerprint, read lazily once per request, only while library IDs may be named (P13). lit/library-insert is not asked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryIntegration;
|
||||
use LIT\MCP\Integrations\Divi\Library\SimilarSections;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility;
|
||||
@@ -194,7 +195,8 @@ final class DiviIntegration implements Integration
|
||||
$this->design,
|
||||
new DiviComposition($this->modules, $this->design),
|
||||
new GlobalEdits($this->access, new GlobalUsages(null, $this->access->roles())),
|
||||
new ThemeBuilderEdits($this->themeBuilderAccess, fields: new DynamicFields($this->schema))
|
||||
new ThemeBuilderEdits($this->themeBuilderAccess, fields: new DynamicFields($this->schema)),
|
||||
new SimilarSections($this->access)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Fingerprint;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviChange;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The quality warning "similar section" (spec §5.5, D6, E11): an inserted section built like a section entry of
|
||||
* the Divi Library (Fingerprint::structure) – compared with library entries only (small; at most MAX_ITEMS, the
|
||||
* ones the user may edit), read only when a write inserts a section and then once per request (kept in memory; no
|
||||
* index or meta, F31). Global instances are already linked to the library and never warn. Only while library IDs
|
||||
* may be named (ruling P13). The warning names the entry's title and ID, never the page's texts. Not final so the
|
||||
* writer can be tested with a mock.
|
||||
*/
|
||||
class SimilarSections
|
||||
{
|
||||
public const MAX_ITEMS = 200;
|
||||
|
||||
/** @var array<string, array{id: int, title: string}>|null Structure fingerprint => entry. */
|
||||
private ?array $index = null;
|
||||
|
||||
public function __construct(private LibraryAccess $access, private Parser $parser = new Parser())
|
||||
{
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function warnings(DiviChange $change): array
|
||||
{
|
||||
if (! $this->access->revealsIds()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$warnings = [];
|
||||
foreach ($change->entries() as $entry) {
|
||||
$block = $entry['block'];
|
||||
if (
|
||||
$entry['action'] !== 'inserted'
|
||||
|| ! $block instanceof Block
|
||||
|| $block->name() !== 'divi/section'
|
||||
|| $block->isOpaque()
|
||||
|| $block->isGlobal()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$match = $this->index()[Fingerprint::structure($block)] ?? null;
|
||||
if ($match !== null) {
|
||||
$warnings[] = sprintf(
|
||||
'Ein gleich aufgebauter Abschnitt liegt in der Divi-Bibliothek: „%s“ (%d) – lit-library-insert '
|
||||
. 'erwägen; weitere Vorkommen mit lit/library-find-similar.',
|
||||
$match['title'],
|
||||
$match['id']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($warnings));
|
||||
}
|
||||
|
||||
/** @return array<string, array{id: int, title: string}> */
|
||||
private function index(): array
|
||||
{
|
||||
if ($this->index !== null) {
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
$this->index = [];
|
||||
foreach ($this->access->store()->find(['layout_type' => 'section'], self::MAX_ITEMS - 1, 0) as $item) {
|
||||
if (! current_user_can('edit_post', (int) $item->ID)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$roots = LibraryContent::roots(LibraryContent::load($this->parser, (string) $item->post_content));
|
||||
} catch (MalformedMarkup) {
|
||||
continue;
|
||||
}
|
||||
if (count($roots) === 1 && $roots[0]->name() === 'divi/section') {
|
||||
$this->index[Fingerprint::structure($roots[0])] ??= [
|
||||
'id' => (int) $item->ID,
|
||||
'title' => (string) $item->post_title,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->index;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
|
||||
use LIT\MCP\Integrations\Divi\Library\SimilarSections;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
|
||||
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
|
||||
use WP_Error;
|
||||
@@ -27,13 +28,17 @@ defined('ABSPATH') || exit;
|
||||
* then lit_mcp_after_save, change log and the ToolResult with changed nodes, affected outline,
|
||||
* quality warnings (spec §5.5), revision_id, the new rev and tree_rev. commit() is the entry point
|
||||
* of the tools: library entries additionally pass GlobalEdits (shape, dry run/confirm, backup), Theme
|
||||
* Builder layouts ThemeBuilderEdits (live layouts: dry run/confirm, backup; ruling Q2/Q9).
|
||||
* Builder layouts ThemeBuilderEdits (live layouts: dry run/confirm, backup; ruling Q2/Q9). SimilarSections adds
|
||||
* the "similar section" warning (spec §5.5, E11) – not for lit/library-insert, whose sections come from the library.
|
||||
*/
|
||||
final class DiviWriter
|
||||
{
|
||||
public const OUTLINE_TRUNCATED = 'Die Übersicht der geänderten Abschnitte ist gekürzt; vollständig mit '
|
||||
. 'lit-divi-outline lesen.';
|
||||
|
||||
/** Inserts library entries: no advice to use the library (SimilarSections). */
|
||||
private const FROM_LIBRARY = 'lit/library-insert';
|
||||
|
||||
public function __construct(
|
||||
private DiviPageLoader $loader,
|
||||
private DiviSaver $saver,
|
||||
@@ -41,7 +46,8 @@ final class DiviWriter
|
||||
private DesignSystemReader $design,
|
||||
private DiviComposition $composition,
|
||||
private ?GlobalEdits $globals = null,
|
||||
private ?ThemeBuilderEdits $themeBuilder = null
|
||||
private ?ThemeBuilderEdits $themeBuilder = null,
|
||||
private ?SimilarSections $similar = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -106,7 +112,8 @@ final class DiviWriter
|
||||
|
||||
$index = NodeIndex::build($page->document);
|
||||
$quality = $this->composition->warnings()->collect($index, $change->touched());
|
||||
$warnings = array_merge($change->warnings(), $quality);
|
||||
$similar = $ability === self::FROM_LIBRARY ? [] : ($this->similar?->warnings($change) ?? []);
|
||||
$warnings = array_merge($change->warnings(), $quality, $similar);
|
||||
|
||||
try {
|
||||
$saved = $this->saver->save($post, $content, $scope);
|
||||
|
||||
Reference in New Issue
Block a user