feat: Divi tree editor and quality warnings

TreeEditor (position/insert/move/remove/duplicate on the BlockTree) and
Warnings (post-write quality checks), plus S18's outline breadth limit
on the 2a Outline/DiviOutlineAbility.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 14:42:26 +02:00
co-authored by Claude Opus 5.5
parent 8688a02402
commit ea1fd607b2
8 changed files with 791 additions and 7 deletions
+46 -3
View File
@@ -7,21 +7,44 @@ namespace LIT\MCP\Integrations\Divi\BlockTree;
/**
* The page tree as rows and as compact text (lit-divi-outline, spec §5.3/§5.7; also the
* "outline" of write results). Preset IDs are shown with their names when known.
*
* Ruling S18: a page can have far more nodes at one level than are useful to show (e.g. a
* 200-item slider). rows() therefore caps the number of *direct* children shown per parent
* (breadth, not depth) at $limit; the parent whose children got cut is marked
* 'truncated' => true. wasTruncated() additionally reports whether ANY cap fired, including at
* the top level (the page root has no row of its own to mark).
*/
final class Outline
{
/** Sane default breadth cap per parent when no explicit limit is given. */
public const DEFAULT_LIMIT = 50;
private bool $truncated = false;
/** @param array<string, string> $presetNames Preset ID => name. */
public function __construct(private array $presetNames = [])
{
}
/** Whether the last rows() call cut off any children (at any level, including the top). */
public function wasTruncated(): bool
{
return $this->truncated;
}
/**
* @return list<array{id: string, type: string, label: string, excerpt: string, info: string,
* presets: list<string>, children: int, editable: bool, depth: int}>
* presets: list<string>, children: int, editable: bool, depth: int, truncated: bool}>
*/
public function rows(NodeIndex $index, ?string $rootId = null, ?int $maxDepth = null): array
public function rows(NodeIndex $index, ?string $rootId = null, ?int $maxDepth = null, ?int $limit = null): array
{
$limit = $limit ?? self::DEFAULT_LIMIT;
$this->truncated = false;
$rows = [];
$rowIndexById = [];
$counts = [];
$skipped = [];
$baseDepth = null;
foreach ($index->entries() as $entry) {
$id = $entry['id'];
@@ -29,12 +52,30 @@ final class Outline
continue;
}
$parentId = $index->parentId($entry['block']);
if ($parentId !== null && isset($skipped[$parentId])) {
$skipped[$id] = true;
continue;
}
$baseDepth ??= $entry['depth'];
$depth = $entry['depth'] - $baseDepth;
if ($maxDepth !== null && $depth > $maxDepth) {
continue;
}
$parentKey = $parentId ?? '';
$counts[$parentKey] = ($counts[$parentKey] ?? 0) + 1;
if ($counts[$parentKey] > $limit) {
$this->truncated = true;
$skipped[$id] = true;
if ($parentId !== null && isset($rowIndexById[$parentId])) {
$rows[$rowIndexById[$parentId]]['truncated'] = true;
}
continue;
}
$rowIndexById[$id] = count($rows);
$rows[] = $this->row($entry['block'], $id, $depth);
}
@@ -52,6 +93,7 @@ final class Outline
$line .= $row['presets'] !== [] ? ' {Preset: ' . implode(', ', $row['presets']) . '}' : '';
$line .= $row['excerpt'] !== '' ? ' – ' . $row['excerpt'] : '';
$line .= $row['editable'] ? '' : ' (nur lesbar)';
$line .= ($row['truncated'] ?? false) ? ' (weitere Kinder abgeschnitten)' : '';
$lines[] = $line;
}
@@ -60,7 +102,7 @@ final class Outline
/**
* @return array{id: string, type: string, label: string, excerpt: string, info: string,
* presets: list<string>, children: int, editable: bool, depth: int}
* presets: list<string>, children: int, editable: bool, depth: int, truncated: bool}
*/
private function row(Block $block, string $id, int $depth): array
{
@@ -79,6 +121,7 @@ final class Outline
'children' => $editable ? count($block->children()) : 0,
'editable' => $editable,
'depth' => $depth,
'truncated' => false,
];
}
}