Skip to content

Convert StyleResolutionTrait into a StyleResolver collaborator (#242) - #1318

Merged
chubes4 merged 1 commit into
trunkfrom
refactor/242-style-resolution
Aug 28, 2026
Merged

Convert StyleResolutionTrait into a StyleResolver collaborator (#242)#1318
chubes4 merged 1 commit into
trunkfrom
refactor/242-style-resolution

Conversation

@chubes4

@chubes4 chubes4 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Slice 6 for #242, workstream 1. Follows #1310, #1312, #1315, #1316, #1317.

This is the big one. StyleResolutionTrait was 2,822 lines — the largest single-consumer trait mixed into HtmlTransformer, and the single largest contributor to that class's object scope.

The surprise

I expected this to be the hardest remaining trait. Measured, it was the easiest:

trait lines external deps
StyleResolutionTrait 2,822 15
FormDispatchTrait 1,865 30

The biggest trait had half the external coupling of one two-thirds its size. Size and coupling were inversely related here, which is worth knowing before picking the next one.

What changed

StyleResolutionTraitStyleResolver, behind a 15-operation StyleResolutionContext. Per-transform state (author styles, source styles, layout geometry, presentation cache, transformation evidence) resolves through closures, because the resolver is built once with the transformer but must see the state of the transform currently running.

StyleAttributeMapper and HighValueStyleBoundaryPolicy stay self-owned lazy singletons — they were already the resolver's own, not the transformer's.

292 call sites migrated, trait deleted, no shims:

call sites
HtmlTransformer 226
SvgMaterializationTrait 18
ElementConversionTrait 18
NavigationStyleProjectionTrait 13
FormDispatchTrait 13
NavigationToggleSuppressionTrait 2
DomHelpersTrait 2

A trap this surfaced

DomHelpersTrait is shared by four classes, only one of which is the transformer. Its canonicalizeLinkUrls() invalidated a selector-match cache that only the transformer owns — and it is reached transitively from innerHtml(), innerHtmlPreservingWhitespace(), and outerHtml(), which all four consumers call.

A naive migration would have left the shared trait referencing a collaborator that SubtreeClassifier, FallbackEmitter, and SemanticParityReporter do not have. It would have worked in the corpus run and blown up on whichever path first canonicalized a link from one of those three.

Fixed with an onSourceMarkupMutated() hook on the trait that defaults to a no-op; HtmlTransformer overrides it to invalidate the cache. The shared trait no longer knows about a collaborator three of its consumers lack.

Behavior preservation

385 fixtures — DIFFERING: 0 — threw: 0

The corpus check earned its keep twice here: it caught a namespace collision on HtmlTransformerAnalysisCache (resolved into ...\HtmlToBlocks\Style\ instead of ...\HtmlToBlocks\) as 385/385 TypeErrors the moment it was introduced.

composer test exit 0.

Test updates

Three unit tests reflected on style methods via HtmlTransformer. They now reach StyleResolver through the transformer's collaborator. presentationCacheKey stayed private and is reached by reflection on the resolver rather than widening its API for a test.

Impact

trunk after
HtmlTransformer.php 12,684 12,721
trait lines on the transformer 8,956 6,146
effective object scope 21,640 18,867

−2,773 lines off the object scope in one slice — more than the previous five slices combined (−386).

Cumulative since the audit: effective object scope 22,026 → 18,867 (−14%), three traits eliminated, convertElement() 441 → 301.

Scope

Five single-consumer traits remain, 5,542 lines: FormDispatchTrait (1,865), NavigationStyleProjectionTrait (1,319), SvgMaterializationTrait (983), NavigationToggleSuppressionTrait (781), ElementConversionTrait (594).

Given what this slice showed about size vs coupling, the next one should be chosen by measuring external dependencies rather than line count.


AI assistance disclosure: implemented and drafted by Claude Sonnet 4.6 running in Claude Code, operated by @chubes4. The AI measured trait coupling to select the target, captured the pre-change corpus baseline, converted the trait, migrated all 292 call sites, identified and fixed the shared-trait hazard in DomHelpersTrait, and verified byte-identical serializedBlocks and fallback counts across 385 fixtures. Reviewed by a human before opening.

At 2,822 lines this was the largest single-consumer trait mixed into
HtmlTransformer and the largest single contributor to that class's object
scope. Despite its size it was the most self-contained of the remaining
traits: 15 transformer operations and one cache, fewer external
dependencies than traits a third its size.

All 292 call sites across the transformer and six sibling traits were
migrated, so the trait is deleted rather than hollowed out.

DomHelpersTrait gains an onSourceMarkupMutated() hook. Link
canonicalization there invalidates a selector-match cache that only the
transformer owns, but the trait is shared by four classes; the hook
defaults to a no-op and HtmlTransformer overrides it.
@chubes4
chubes4 merged commit 1d968a2 into trunk Aug 28, 2026
10 checks passed
@chubes4
chubes4 deleted the refactor/242-style-resolution branch August 28, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant