Skip to content

Convert NavigationToggleSuppressionTrait into a collaborator (#1327) - #1335

Merged
chubes4 merged 1 commit into
trunkfrom
refactor-1327-navigation-toggle-suppressor
Aug 29, 2026
Merged

Convert NavigationToggleSuppressionTrait into a collaborator (#1327)#1335
chubes4 merged 1 commit into
trunkfrom
refactor-1327-navigation-toggle-suppressor

Conversation

@chubes4

@chubes4 chubes4 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Slice 8 for #242, workstream 1. Part of #1327. Follows #1333.

Stacked on #1333. Base is refactor-1327-navigation-style-projector, so this diff shows only slice 8. Both slices rewrite HtmlTransformer heavily, and the epic asks for a quiet base rather than racing the same file. GitHub will retarget this to trunk once #1333 merges.

NavigationToggleSuppressionTrait was a 781-line single-consumer mixin. It is now NavigationToggleSuppressor behind a 7-operation NavigationToggleSuppressionContext, with no $this access 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->x against 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.

elementContains moved to DomHelpersTrait, not into the collaborator

It is a nine-line DOM ancestry check with no $this dependency, and FormDispatchTrait consumes it as well. Moving it into a class named NavigationToggleSuppressor would have left form dispatch reaching into navigation suppression for a generic helper.

DomHelpersTrait is 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 HtmlTransformer a document's output depends on what was transformed before it. Reproduced on corpus fixtures at 2,357 bytes of lost markup, with CorpusDiagnosticsRunner and StaticStyleParityRunner both 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_blocks SHA-256 plus block, diagnostic, fallback, asset-count, asset-key and coverage fingerprints, captured on this branch's base and recaptured after:

383 documents — DIFFERING: 0 — threw: 0
markup: 22,047,755 bytes across 383 distinct hashes

composer test exit 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

slice-7 base after
HtmlTransformer.php 12,771 12,803
Object scope (class + single-consumer mixins) 16,994 16,245

749 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 the self::/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.

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
chubes4 force-pushed the refactor-1327-navigation-toggle-suppressor branch from dc155be to 8ccf39d Compare August 29, 2026 01:26
@chubes4
chubes4 merged commit 1d3401e into trunk Aug 29, 2026
10 checks passed
@chubes4
chubes4 deleted the refactor-1327-navigation-toggle-suppressor branch August 29, 2026 01:34
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