Sub of #18304 (Workspace decomposition). Graduated from D#18150; the vehicle classification is Option G — a vehicle per responsibility.
The Problem
Workspace.mjs owns maximize as stateful presentation nobody can switch off. At dev@131c782ca8 the cluster is six class-state fields — dockMaximizeMotion (:442), dockMaximizeResizeObserved (:450), dockMaximizeResizeWired (:458), dockMaximizeRestore (:483), dockMaximizeTransition (:508), dockMaximizePlay (:518) — plus maximizedNodeId, a ResizeObserver registered per maximize (:3269–3292), an escape handler (onDockMaximizeEscape :2789), rect-style derivation (:2854), and the FLIP capture/play pair. By member span: 321 code / 198 doc lines, self-contained.
It is the cleanest plugin candidate in the file: it owns its own state, it has a real teardown obligation, and it is optional by nature — a consumer that never maximizes still pays for the observer wiring and cannot decline it without subclassing the host.
The Fix
Extract to src/dashboard/dock/plugin/Maximize.mjs as a Neo.plugin.Base subclass — a new plugin/ folder on existing in-package precedent (src/tab/plugin/Overflow.mjs; five modules in src/plugin/).
component/Base.mjs:1322 already destroys owned plugins, so the teardown obligation is discharged by engine code rather than by a new convention. plugins_ is a per-instance config (component/Base.mjs:216), which is exactly the declinability Neo.mixin.* cannot offer — the falsifier that removed Option A.
Workspace retains only the existing enableDockMaximizeAction install switch; maximize behavior and state move to the plugin without forwarding wrappers. The Ownership test's question 4 still rejects owner.foo() proxies.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
dock/plugin/Maximize.mjs |
D#18150 Option G; tab/plugin/Overflow.mjs precedent |
owns the six dockMaximize* fields, the ResizeObserver, escape handling and the FLIP lifecycle |
plugin absent ⇒ no maximize affordance, zero registered instances |
class docblock |
member-span census, 321/198 lines |
Workspace maximize surface |
the Ownership test |
deleted, not forwarded |
none — zero-backcompat operator ruling |
— |
method + config counts fall in the same PR |
enableDockMaximizeAction |
current config surface |
selects whether the plugin is instantiated |
false ⇒ plugin never created |
config docblock |
declined-plugin arm below |
dockHeaderAction event (broadened) |
the existing re-emit |
fired for every action the engine set does not own — host actions AND collaborators' — {action, dockNodeId, tabContainer}; maximize is the plugin's by name |
a throwing listener propagates like any listener |
the dispatch docblock |
DockLayoutAdapter.spec intent arms; DockMaximizePlugin.spec |
beforeDockZoneDocumentChange event (new) |
this leaf |
synchronous, subscription order, before dockModel changes; {descriptor, document, source} |
a throwing listener aborts the commit; a collaborator writes its own state and never throws |
the comment at the fire |
DockMaximizePlugin.spec "a committed operation is announced to collaborators before it applies" |
Workspace#getDockCollaboratorOptions() (new) |
this leaf |
every plugin's getDockProjectionOptions() merged in plugins order, spread before the consumer's getDockProjectionOptions() |
two collaborators claiming one key throw at projection naming both; the consumer hook overrides a collaborator key |
its docblock |
the collision arm |
awaited plugin.syncDockProjection() (new) |
this leaf |
awaited in plugins order inside refreshDockWorkspace, before the FLIP play |
a rejection is reported (console.warn + dockCollaboratorSyncFailed {component, error, plugin}) and skipped; the refresh settles |
the loop comment |
the reported-rejection arm |
owner surface read by plugin.Maximize |
this leaf |
id, windowId, keys, plugins, dockModel, dockShellIndex, refreshPromise, getDockHost(), on/un/fire, addDomListeners/removeDomListeners, isDestroying/isDestroyed — nothing else |
— |
the plugin's class docblock |
the plugin-only destroy arm exercises the listener pair and plugins |
Acceptance Criteria
Out of Scope
- Any other cluster in
Workspace — one leaf per concern (#18304).
- Motion tokens / reduced-motion policy: unchanged, this moves the mechanism not the design.
- The no-regrowth ratchet (#18278).
Avoided Traps
- A forwarding shell.
Workspace#maximize() delegating to the plugin passes a file-size check and fails the Ownership test by construction — the exact "relocation, not refactoring" defect D#18150 named in its own original criteria.
- A mixin.
Neo.mixin.VdomLifecycle composes unconditionally at component/Abstract.mjs:80; a mixin cannot be declined per instance, which is what this concern requires.
- Leaving the observer registration behind "because the host owns the element". The observer is maximize state; it moves with the concern or the split is wrong.
Related
#18304 (epic) · D#18150 (source) · #18278 (ratchet) · src/tab/plugin/Overflow.mjs, src/plugin/* (precedent) · #18143 (the red-first case for owner-destroys-collaborator)
Live latest-open sweep: latest 20 open issues at 2026-09-04T18:12Z; no equivalent. A2A claim sweep: no claim on this surface. Own-assignment sweep: 12 open, none on the maximize cluster. Memory Core sweep: semantic recall is serving stale 2025 hits today (@neo-opus-ada, 17:22Z) — recorded as a null, not a clean sweep.
Decision Record impact: none — placement follows existing plugin precedent; ADR 0029 is untouched by this leaf.
Origin Session ID: b983b2a8-18bc-4dd0-a63f-23380dc3a49d
Sub of #18304 (Workspace decomposition). Graduated from D#18150; the vehicle classification is Option G — a vehicle per responsibility.
The Problem
Workspace.mjsowns maximize as stateful presentation nobody can switch off. Atdev@131c782ca8the cluster is six class-state fields —dockMaximizeMotion(:442),dockMaximizeResizeObserved(:450),dockMaximizeResizeWired(:458),dockMaximizeRestore(:483),dockMaximizeTransition(:508),dockMaximizePlay(:518) — plusmaximizedNodeId, aResizeObserverregistered per maximize (:3269–3292), an escape handler (onDockMaximizeEscape:2789), rect-style derivation (:2854), and the FLIP capture/play pair. By member span: 321 code / 198 doc lines, self-contained.It is the cleanest plugin candidate in the file: it owns its own state, it has a real teardown obligation, and it is optional by nature — a consumer that never maximizes still pays for the observer wiring and cannot decline it without subclassing the host.
The Fix
Extract to
src/dashboard/dock/plugin/Maximize.mjsas aNeo.plugin.Basesubclass — a newplugin/folder on existing in-package precedent (src/tab/plugin/Overflow.mjs; five modules insrc/plugin/).component/Base.mjs:1322already destroys owned plugins, so the teardown obligation is discharged by engine code rather than by a new convention.plugins_is a per-instance config (component/Base.mjs:216), which is exactly the declinabilityNeo.mixin.*cannot offer — the falsifier that removed Option A.Workspaceretains only the existingenableDockMaximizeActioninstall switch; maximize behavior and state move to the plugin without forwarding wrappers. The Ownership test's question 4 still rejectsowner.foo()proxies.Contract Ledger Matrix
dock/plugin/Maximize.mjstab/plugin/Overflow.mjsprecedentdockMaximize*fields, the ResizeObserver, escape handling and the FLIP lifecycleWorkspacemaximize surfaceenableDockMaximizeActionfalse⇒ plugin never createddockHeaderActionevent (broadened){action, dockNodeId, tabContainer};maximizeis the plugin's by nameDockLayoutAdapter.specintent arms;DockMaximizePlugin.specbeforeDockZoneDocumentChangeevent (new)dockModelchanges;{descriptor, document, source}DockMaximizePlugin.spec"a committed operation is announced to collaborators before it applies"Workspace#getDockCollaboratorOptions()(new)getDockProjectionOptions()merged inpluginsorder, spread before the consumer'sgetDockProjectionOptions()plugin.syncDockProjection()(new)pluginsorder insiderefreshDockWorkspace, before the FLIP playconsole.warn+dockCollaboratorSyncFailed{component, error, plugin}) and skipped; the refresh settlesplugin.Maximizeid,windowId,keys,plugins,dockModel,dockShellIndex,refreshPromise,getDockHost(),on/un/fire,addDomListeners/removeDomListeners,isDestroying/isDestroyed— nothing elsepluginsAcceptance Criteria
src/dashboard/dock/plugin/Maximize.mjsowns everydockMaximize*field,maximizedNodeId, the per-maximizeResizeObserver,onDockMaximizeEscape, the rect-style derivation and the FLIP capture/play pair.Workspace.mjsretains no maximize behavior or state beyond theenableDockMaximizeActioninstall switch — no forwarding wrappers. Its method count and config/state count both fall in this PR; the delta is recorded in the PR body.Maximizeinstances and noResizeObserverregistration — the guard D#18150 assigned to Option E.component/Base.mjs:1322; noResizeObserverand no pending transition survives. Red-first against a deliberately leaked observer.Out of Scope
Workspace— one leaf per concern (#18304).Avoided Traps
Workspace#maximize()delegating to the plugin passes a file-size check and fails the Ownership test by construction — the exact "relocation, not refactoring" defect D#18150 named in its own original criteria.Neo.mixin.VdomLifecyclecomposes unconditionally atcomponent/Abstract.mjs:80; a mixin cannot be declined per instance, which is what this concern requires.Related
#18304 (epic) · D#18150 (source) · #18278 (ratchet) ·
src/tab/plugin/Overflow.mjs,src/plugin/*(precedent) · #18143 (the red-first case for owner-destroys-collaborator)Live latest-open sweep: latest 20 open issues at 2026-09-04T18:12Z; no equivalent. A2A claim sweep: no claim on this surface. Own-assignment sweep: 12 open, none on the maximize cluster. Memory Core sweep: semantic recall is serving stale 2025 hits today (@neo-opus-ada, 17:22Z) — recorded as a null, not a clean sweep.
Decision Record impact:
none— placement follows existing plugin precedent; ADR 0029 is untouched by this leaf.Origin Session ID: b983b2a8-18bc-4dd0-a63f-23380dc3a49d