refactor: ValueCleaner is the named choke point (H1)

The choke point moves out of AttrWriter into ValueCleaner (catalog, the
composition's VariableRef, sanitizer): clean() (was AttrWriter::cleanValue),
kindOf(), guardPath() and the private cleanNode/kind/unknownKind/tightens with
VALUE_KEY, URL_KEY, URL_SEGMENT, STRICTNESS, BUILDER_ONLY (its docblock now sits
on it; STRUCTURE stays in AttrWriter). Code of the pipeline moved unchanged.
- ValueCleaner's docblock is the one canonical description: steps 0-8 with their
  rulings in code order, invariants I1-I3; the step comments in cleanNode carry
  the same numbers. AttrWriter/ShortformWriter/Composer/TextReplacer docblocks
  point there and keep only their own rules.
- ValueCleaner::textReplacementFor($module) holds the D10 rule that was a
  closure in DiviReplaceTextAbility::replace (text/html only, clean() by kind).
- AttrWriter, ShortformWriter and Composer take the ValueCleaner; DiviComposition
  builds one per composition (cleaner()).
Tests: the eight AttrWriterTest methods that only exercised cleanValue/kindOf
moved verbatim to ValueCleanerTest (clean/kindOf, ComposeFixture::cleaner());
mixed tests call the cleaner in place; +1 test for textReplacementFor. Every
provider row and per-test assertion count unchanged.

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 18:11:28 +02:00
co-authored by Claude Opus 5.5
parent 203d14a2f9
commit 62ce99e558
13 changed files with 552 additions and 447 deletions
+4 -3
View File
@@ -19,7 +19,7 @@ use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
* Pure PHP.
*
* Divi saves bypass kses (D7): every client string reaches the attributes only through
* ShortformWriter / AttrWriter::cleanValue() (the single choke point) – short-form fields,
* ShortformWriter / AttrWriter / ValueCleaner::clean() (the single choke point) – short-form fields,
* full-form attrs, admin labels, background_color (ShortformWriter::color(), ruling S13) and
* preset ids. A value that fails there is an error, never stored raw (S13). What Composer
* writes itself is not client text: the builder version (checked in the constructor), block
@@ -69,6 +69,7 @@ final class Composer
private StructureRules $rules,
private ShortformWriter $shortform,
private AttrWriter $attrWriter,
private ValueCleaner $cleaner,
private DesignSystem $design,
private string $builderVersion
) {
@@ -502,7 +503,7 @@ final class Composer
}
try {
$clean = $this->attrWriter->cleanValue($name, self::LABEL_PATH, $label, null, 'text');
$clean = $this->cleaner->clean($name, self::LABEL_PATH, $label, null, 'text');
} catch (InvalidArgumentException $e) {
$this->errors[$path] = $e->getMessage();
@@ -552,7 +553,7 @@ final class Composer
// Design-system data, but still through the choke point: an id text() would change is refused.
try {
$clean = $this->attrWriter->cleanValue($name, 'modulePreset', $id, null, 'text');
$clean = $this->cleaner->clean($name, 'modulePreset', $id, null, 'text');
} catch (InvalidArgumentException $e) {
$clean = null;
}