Convert NavigationToggleSuppressionTrait into a collaborator (#1327) - #1335
Merged
Conversation
Base automatically changed from
refactor-1327-navigation-style-projector
to
trunk
August 29, 2026 01:21
NavigationToggleSuppressionTrait was a 781-line single-consumer mixin, so its methods shared one mutable object scope with the rest of HtmlTransformer. It is now NavigationToggleSuppressor behind a 7-operation NavigationToggleSuppressionContext, with no $this access to the transformer. elementContains moves to DomHelpersTrait rather than into the suppressor. It is a pure DOM ancestry helper with no $this dependency, and FormDispatchTrait consumes it too, so the shared DOM mixin is its correct home. The suppressor reaches it through the context. The three projection maps keyed by DOM node path move into the collaborator as its own state. They were per-transformer rather than per-transform as trait properties, and that lifetime is preserved unchanged here; the resulting cross-document leak is reported separately in #1334 rather than fixed inside a pure-extraction slice. Trait deleted, no shim. The object scope drops from 16,994 to 16,245 lines across the class and its remaining single-consumer mixins. Behavior preservation: serialized_blocks SHA-256 plus block, diagnostic, fallback, asset and coverage fingerprints across all 383 fixture documents with stylesheets attached -- 0 differing, 0 throwing, against a fingerprint verified to be discriminating (383 distinct hashes over 22 MB of markup). composer test exit 0.
chubes4
force-pushed
the
refactor-1327-navigation-toggle-suppressor
branch
from
August 29, 2026 01:26
dc155be to
8ccf39d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice 8 for #242, workstream 1. Part of #1327. Follows #1333.
NavigationToggleSuppressionTraitwas a 781-line single-consumer mixin. It is nowNavigationToggleSuppressorbehind a 7-operationNavigationToggleSuppressionContext, with no$thisaccess to the transformer.The measurement in #1327 was wrong, in a useful direction
#1327 recorded 10 external dependencies for this trait. Three of those —
projectedNavigationTargetsByControlPath,projectedNavigationSuppressedPaths,implicitDialogNavigationControlPaths— are properties the trait declares itself. The counting method matched$this->xagainst method names only, so trait-owned properties were miscounted as reaching outward.The true figure is 7, which makes this a cheaper slice than advertised rather than a more expensive one. The ranking in #1327 is unaffected — this was already second — but the same correction should be applied before estimating the remaining traits.
elementContainsmoved toDomHelpersTrait, not into the collaboratorIt is a nine-line DOM ancestry check with no
$thisdependency, andFormDispatchTraitconsumes it as well. Moving it into a class namedNavigationToggleSuppressorwould have left form dispatch reaching into navigation suppression for a generic helper.DomHelpersTraitis the legitimate shared mixin here — four consumers, three of them standalone classes — so that is where it goes. The suppressor reaches it through the context, and form dispatch is unaffected.The three projection maps, and a defect they exposed
The maps are keyed by
DOMElement::getNodePath()and were declared on the trait, meaning they lived on the transformer instance rather than the per-transform session, and were never reset. They move into the collaborator with that lifetime deliberately preserved — the collaborator is constructed once per transformer, so semantics are unchanged.That lifetime is a real defect: node paths collide across unrelated documents, so on a reused
HtmlTransformera document's output depends on what was transformed before it. Reproduced on corpus fixtures at 2,357 bytes of lost markup, withCorpusDiagnosticsRunnerandStaticStyleParityRunnerboth on the reachable path. Filed as #1334 rather than fixed here, per the epic's rule against smuggling logic changes into an extraction slice. It is easier to fix afterwards, with the state in one collaborator instead of a mixin sharing the transformer's scope.Behavior preservation
serialized_blocksSHA-256 plus block, diagnostic, fallback, asset-count, asset-key and coverage fingerprints, captured on this branch's base and recaptured after:composer testexit 0.The distinct-hash count is quoted deliberately. The harness used for #1333 read a camelCase key that
TransformerResult::toArray()does not emit, so every document hashed the empty string and the markup comparison was vacuous — corrected and re-verified in a comment on that PR. The harness now fails hard on a missing key and records markup length, and 383 distinct hashes over 22 MB is the check that the fingerprint can actually discriminate. A zero-diff result is only worth as much as the variance in the signal behind it.Impact
HtmlTransformer.php749 lines and 37 methods leave the shared mutable scope for 32 lines of wiring.
Cumulative across slices 7 and 8: 18,253 → 16,245, a 2,008-line reduction in the transformer's object scope.
Remaining single-consumer mixins:
FormDispatchTrait(1,865 / 30 deps),SvgMaterializationTrait(983 / 20),ElementConversionTrait(594 / 89 — not a collaborator candidate, see #1327). Their dependency counts should be re-measured with the property correction above and theself::/static::constant check from #1333 before the next slice is scoped.Verification notes
No
self::/static::constants in this trait — checked up front, since that gap is what broke #1333 mid-flight. No test reflected on any of the 37 moved methods.AI assistance disclosure: implemented and drafted by Claude Sonnet 4.6 running in Claude Code, operated by @chubes4. The AI re-measured the trait's dependency surface and corrected the earlier count, performed the extraction, relocated
elementContains, discovered and separately reported the state-lifetime defect in #1334, captured and compared the corpus fingerprints, and wrote this description. Reviewed by a human before opening.