diff --git a/composer.json b/composer.json
index 3f2507aae9b94c71a3a83b6e4a02f4df5f493dd5..2253ad888a1310bf1f4640ae4b6e396523230afd 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,6 @@
     "ext-json": "*",
     "drupal/core": "^9.7 || ^10",
     "php-tuf/composer-stager": "^1.2",
-    "composer/composer": "^2.2.12 || ^2.3.5",
     "composer-runtime-api": "^2.1"
   },
   "scripts": {
diff --git a/package_manager/core_packages.yml b/package_manager/core_packages.yml
index d1b29edfd196e694ef64739a2f6818a3d42d87ea..d3b8151a19a45e71c40395c7f9033d9d04efdb41 100644
--- a/package_manager/core_packages.yml
+++ b/package_manager/core_packages.yml
@@ -1,4 +1,4 @@
-# This file exists so that \Drupal\package_manager\ComposerUtility can discern
+# This file exists so that \Drupal\package_manager\InstalledPackagesList knows
 # which installed packages are considered part of Drupal core. There's no way
 # to tell by package type alone, since these packages have varying types, but
 # are all part of Drupal core's repository. This file is for internal use and
diff --git a/package_manager/package_manager.api.php b/package_manager/package_manager.api.php
index 533231663c1b100cc4cef883929abc5578d2bd55..c50ac7917eab238300a27dc2f90e58a26650bab7 100644
--- a/package_manager/package_manager.api.php
+++ b/package_manager/package_manager.api.php
@@ -144,14 +144,6 @@
  *   Destroys the stage directory, releases ownership, and dispatches pre- and
  *   post-destroy events.
  *
- * - \Drupal\package_manager\Stage::getActiveComposer()
- *   \Drupal\package_manager\Stage::getStageComposer()
- *   These methods initialize an instance of Composer's API in the active
- *   directory and stage directory, respectively, and return an object which
- *   can be used by event subscribers to inspect the directory and get relevant
- *   information from Composer's API, such as what packages are installed and
- *   where.
- *
  * If problems occur during any point of the stage life cycle, a
  * \Drupal\package_manager\Exception\StageException is thrown. If problems were
  * detected during one of the "pre" operations, a subclass of that is thrown:
@@ -207,6 +199,9 @@
  * To be able to do enforce those constraints, these event subscribers need to
  * know where to look: \Drupal\package_manager\PathLocator informs them where
  * the project root, the vendor directory, et cetera are.
+ * If the constraints involve Composer aspects (such as installed packages,
+ * required configuration …), then \Drupal\package_manager\ComposerInspector is
+ * available as a service.
  * Whenever a problem is encountered, an event subscriber should generate one or
  * more messages (with a summary if there's multiple) to explain it to the user
  * and call \Drupal\package_manager\Event\PreOperationStageEvent::addError() or
diff --git a/package_manager/src/ComposerInspector.php b/package_manager/src/ComposerInspector.php
index 35ff2339554c01bccffa549d53bf6a05363eb8dc..c0a086194900db977163be9ed10c818bac673e44 100644
--- a/package_manager/src/ComposerInspector.php
+++ b/package_manager/src/ComposerInspector.php
@@ -16,10 +16,11 @@ use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
 /**
  * Defines a class to get information from Composer.
  *
- * @internal
- *   This is an internal part of Package Manager and may be changed or removed
- *   at any time without warning. External code should not interact with this
- *   class.
+ * This is a PHP wrapper to facilitate interacting with composer and:
+ * - list installed packages: getInstalledPackagesList() (`composer show`)
+ * - validate composer state & project: validate() (`composer validate`)
+ * - read project & package configuration: getConfig() (`composer config`)
+ * - read root package info: getRootPackageInfo() (`composer show --self`)
  */
 class ComposerInspector {
 
diff --git a/package_manager/src/ComposerUtility.php b/package_manager/src/ComposerUtility.php
deleted file mode 100644
index d85ab5ecb526d22ce2289d8ed79b0310d6b9b6c6..0000000000000000000000000000000000000000
--- a/package_manager/src/ComposerUtility.php
+++ /dev/null
@@ -1,373 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\package_manager;
-
-use Composer\Composer;
-use Composer\Factory;
-use Composer\IO\NullIO;
-use Composer\Package\Loader\ValidatingArrayLoader;
-use Composer\Package\PackageInterface;
-use Composer\Package\Version\VersionParser;
-use Composer\PartialComposer;
-use Composer\Semver\Comparator;
-use Drupal\Component\Serialization\Yaml;
-
-/**
- * Defines a utility object to get information from Composer's API.
- */
-class ComposerUtility {
-
-  /**
-   * The Composer instance.
-   *
-   * @var \Composer\Composer
-   */
-  protected $composer;
-
-  /**
-   * The statically cached names of the Drupal core packages.
-   *
-   * @var string[]
-   */
-  private static $corePackages;
-
-  /**
-   * Whether to raise a deprecation error when the constructor is called.
-   *
-   * @var bool
-   */
-  private static $triggerConstructorDeprecation = TRUE;
-
-  /**
-   * Constructs a new ComposerUtility object.
-   *
-   * @param \Composer\Composer|\Composer\PartialComposer $composer
-   *   The Composer instance.
-   */
-  public function __construct(object $composer) {
-    // @todo Remove this in https://www.drupal.org/project/automatic_updates/issues/3321474.
-    if (self::$triggerConstructorDeprecation) {
-      @trigger_error(__METHOD__ . '() is deprecated in automatic_updates:8.x-2.5 and will be removed in automatic_updates:3.0.0. Use ' . __CLASS__ . '::createForDirectory() instead. See https://www.drupal.org/node/3314137.', E_USER_DEPRECATED);
-    }
-    self::$triggerConstructorDeprecation = TRUE;
-
-    // @todo Remove this check when either:
-    //   - PHP 8 or later is required, in which case the $composer type hint
-    //     should be Composer|PartialComposer.
-    //   - Composer 2.3 or later is required, in which case the $composer type
-    //     hint should be changed to PartialComposer.
-    // @todo Update in https://www.drupal.org/project/automatic_updates/issues/3321474
-    // @todo Update in https://www.drupal.org/project/automatic_updates/issues/3321476
-    if (!$composer instanceof Composer && !$composer instanceof PartialComposer) {
-      throw new \InvalidArgumentException('The $composer argument must be an instance of ' . Composer::class . ' or ' . PartialComposer::class);
-    }
-    $this->composer = $composer;
-  }
-
-  /**
-   * Returns the underlying Composer instance.
-   *
-   * @return \Composer\Composer|\Composer\PartialComposer
-   *   The Composer instance.
-   */
-  public function getComposer(): object {
-    return $this->composer;
-  }
-
-  /**
-   * Creates an instance of this class using the files in a given directory.
-   *
-   * @param string $dir
-   *   The directory that contains composer.json and composer.lock.
-   *
-   * @return \Drupal\package_manager\ComposerUtility
-   *   The utility object.
-   *
-   * @throws \InvalidArgumentException
-   *   When $dir does not contain a composer.json file.
-   */
-  public static function createForDirectory(string $dir): self {
-    $io = new NullIO();
-    $configuration = $dir . DIRECTORY_SEPARATOR . 'composer.json';
-
-    // The Composer factory requires that either the HOME or COMPOSER_HOME
-    // environment variables be set, so momentarily set the COMPOSER_HOME
-    // variable to the directory we're trying to create a Composer instance for.
-    // We have to do this because the Composer factory doesn't give us a way to
-    // pass the home directory in.
-    // @see \Composer\Factory::getHomeDir()
-    $home = getenv('COMPOSER_HOME');
-    // Disable the automatic generation of .htaccess files in the Composer home
-    // directory, since we are temporarily overriding that directory.
-    // @see \Composer\Factory::createConfig()
-    // @see https://getcomposer.org/doc/06-config.md#htaccess-protect
-    $htaccess = getenv('COMPOSER_HTACCESS_PROTECT');
-
-    $factory = new Factory();
-    putenv("COMPOSER_HOME=$dir");
-    putenv("COMPOSER_HTACCESS_PROTECT=false");
-    // Initialize the Composer API with plugins disabled and only the root
-    // package loaded (i.e., nothing from the global Composer project will be
-    // considered or loaded). This allows us to inspect the project directory
-    // using Composer's API in a "hands-off" manner.
-    $composer = $factory->createComposer($io, $configuration, TRUE, $dir, FALSE);
-    putenv("COMPOSER_HOME=$home");
-    putenv("COMPOSER_HTACCESS_PROTECT=$htaccess");
-
-    self::$triggerConstructorDeprecation = FALSE;
-    return new static($composer);
-  }
-
-  /**
-   * Returns the canonical names of the supported core packages.
-   *
-   * @return string[]
-   *   The canonical list of supported core package names, as listed in
-   *   ../core_packages.json.
-   */
-  protected static function getCorePackageList(): array {
-    if (self::$corePackages === NULL) {
-      $file = __DIR__ . '/../core_packages.yml';
-      assert(file_exists($file), "$file does not exist.");
-
-      $core_packages = file_get_contents($file);
-      $core_packages = Yaml::decode($core_packages);
-
-      assert(is_array($core_packages), "$file did not contain a list of core packages.");
-      self::$corePackages = $core_packages;
-    }
-    return self::$corePackages;
-  }
-
-  /**
-   * Returns the installed core packages.
-   *
-   * All packages listed in ../core_packages.json are considered core packages.
-   *
-   * @return \Composer\Package\PackageInterface[]
-   *   The installed core packages.
-   */
-  public function getCorePackages(): array {
-    $core_packages = array_intersect_key(
-      $this->getInstalledPackages(),
-      array_flip(static::getCorePackageList())
-    );
-
-    // If drupal/core-recommended is present, it supersedes drupal/core, since
-    // drupal/core will always be one of its direct dependencies.
-    if (array_key_exists('drupal/core-recommended', $core_packages)) {
-      unset($core_packages['drupal/core']);
-    }
-    return $core_packages;
-  }
-
-  /**
-   * Returns information on all installed packages.
-   *
-   * @return \Composer\Package\PackageInterface[]
-   *   All installed packages, keyed by name.
-   */
-  public function getInstalledPackages(): array {
-    $installed_packages = $this->getComposer()
-      ->getRepositoryManager()
-      ->getLocalRepository()
-      ->getPackages();
-
-    $packages = [];
-    foreach ($installed_packages as $package) {
-      $key = $package->getName();
-      $packages[$key] = $package;
-    }
-    return $packages;
-  }
-
-  /**
-   * Returns the packages that are in the current project, but not in another.
-   *
-   * @param self $other
-   *   A Composer utility wrapper around a different directory.
-   *
-   * @return \Composer\Package\PackageInterface[]
-   *   The packages which are installed in the current project, but not in the
-   *   other one, keyed by name.
-   */
-  public function getPackagesNotIn(self $other): array {
-    return array_diff_key($this->getInstalledPackages(), $other->getInstalledPackages());
-  }
-
-  /**
-   * Returns the packages which have a different version in another project.
-   *
-   * This compares the current project with another one, and returns packages
-   * which are present in both, but in different versions.
-   *
-   * @param self $other
-   *   A Composer utility wrapper around a different directory.
-   *
-   * @return \Composer\Package\PackageInterface[]
-   *   The packages which are present in both the current project and the other
-   *   one, but in different versions, keyed by name.
-   */
-  public function getPackagesWithDifferentVersionsIn(self $other): array {
-    $theirs = $other->getInstalledPackages();
-
-    // Only compare packages that are both here and there.
-    $packages = array_intersect_key($this->getInstalledPackages(), $theirs);
-
-    $filter = function (PackageInterface $package, string $name) use ($theirs): bool {
-      return Comparator::notEqualTo($package->getVersion(), $theirs[$name]->getVersion());
-    };
-    return array_filter($packages, $filter, ARRAY_FILTER_USE_BOTH);
-  }
-
-  /**
-   * Returns installed package data from Composer's `installed.php`.
-   *
-   * @return array
-   *   The installed package data as represented in Composer's `installed.php`,
-   *   keyed by package name.
-   */
-  public function getInstalledPackagesData(): array {
-    $installed_php = implode(DIRECTORY_SEPARATOR, [
-      // Composer returns the absolute path to the vendor directory by default.
-      $this->getComposer()->getConfig()->get('vendor-dir'),
-      'composer',
-      'installed.php',
-    ]);
-    $data = include $installed_php;
-    return $data['versions'];
-  }
-
-  /**
-   * Returns the Drupal project name for a given Composer package.
-   *
-   * @param string $package_name
-   *   The name of the package.
-   *
-   * @return string|null
-   *   The name of the Drupal project installed by the package, or NULL if:
-   *   - The package is not installed.
-   *   - The package is not of a supported type (one of `drupal-module`,
-   *     `drupal-theme`, or `drupal-profile`).
-   *   - The package name does not begin with `drupal/`.
-   *   - The project name could not otherwise be determined.
-   */
-  public function getProjectForPackage(string $package_name): ?string {
-    $data = $this->getInstalledPackagesData();
-
-    if (array_key_exists($package_name, $data)) {
-      $package = $data[$package_name];
-
-      $supported_package_types = [
-        'drupal-module',
-        'drupal-theme',
-        'drupal-profile',
-      ];
-      // Only consider packages which are packaged by drupal.org and will be
-      // known to the core Update module.
-      if (str_starts_with($package_name, 'drupal/') && in_array($package['type'], $supported_package_types, TRUE)) {
-        return $this->scanForProjectName($package['install_path']);
-      }
-    }
-    return NULL;
-  }
-
-  /**
-   * Returns the package name for a given Drupal project.
-   *
-   * @param string $project_name
-   *   The name of the project.
-   *
-   * @return string|null
-   *   The name of the Composer package which installs the project, or NULL if
-   *   it could not be determined.
-   */
-  public function getPackageForProject(string $project_name): ?string {
-    $installed = $this->getInstalledPackagesData();
-
-    $installed = array_keys($installed);
-    foreach ($installed as $package_name) {
-      if ($this->getProjectForPackage($package_name) === $project_name) {
-        return $package_name;
-      }
-    }
-    return NULL;
-  }
-
-  /**
-   * Determines whether a Composer requirement string is valid.
-   *
-   * @param string $requirement
-   *   A requirement string, optionally with a version constraint, e.g.,
-   *   "vendor/package" or "vendor/package:1.2.3", or any combination of package
-   *   name and requirement string that Composer understands.
-   *
-   * @return bool
-   *   TRUE if the requirement string is valid, FALSE otherwise.
-   *
-   * @see https://getcomposer.org/doc/04-schema.md#name
-   * @see https://getcomposer.org/doc/articles/versions.md
-   *
-   * @internal
-   *   This method may be changed or removed at any time without warning and
-   *   should not be used by external code.
-   */
-  public static function isValidRequirement(string $requirement): bool {
-    $version_parser = new VersionParser();
-    $parts = $version_parser->parseNameVersionPairs([$requirement])[0];
-    $package_name = $parts['name'];
-    $version = $parts['version'] ?? NULL;
-
-    // Validate just the package name.
-    if (ValidatingArrayLoader::hasPackageNamingError($package_name)) {
-      return FALSE;
-    }
-
-    // Return early if there's no version constraint to validate.
-    if ($version === NULL) {
-      return TRUE;
-    }
-
-    // Validate the version constraint.
-    try {
-      $version_parser->parseConstraints($version);
-    }
-    catch (\UnexpectedValueException) {
-      return FALSE;
-    }
-
-    // All good.
-    return TRUE;
-  }
-
-  /**
-   * Scans a given path to determine the Drupal project name.
-   *
-   * The path will be scanned for `.info.yml` files containing a `project` key.
-   *
-   * @param string $path
-   *   The path to scan.
-   *
-   * @return string|null
-   *   The name of the project, as declared in the first found `.info.yml` which
-   *   contains a `project` key, or NULL if none was found.
-   */
-  private function scanForProjectName(string $path): ?string {
-    $iterator = new \RecursiveDirectoryIterator($path);
-    $iterator = new \RecursiveIteratorIterator($iterator);
-    $iterator = new \RegexIterator($iterator, '/.+\.info\.yml$/', \RecursiveRegexIterator::GET_MATCH);
-
-    foreach ($iterator as $match) {
-      $info = file_get_contents($match[0]);
-      $info = Yaml::decode($info);
-
-      if (is_string($info['project']) && !empty($info['project'])) {
-        return $info['project'];
-      }
-    }
-    return NULL;
-  }
-
-}
diff --git a/package_manager/src/InstalledPackagesList.php b/package_manager/src/InstalledPackagesList.php
index 70dd5adb559b38595c511ffb00bce6154f1318dd..aba5d654b03610958920e837b906c15ac41b0e01 100644
--- a/package_manager/src/InstalledPackagesList.php
+++ b/package_manager/src/InstalledPackagesList.php
@@ -98,7 +98,7 @@ final class InstalledPackagesList extends \ArrayObject {
   }
 
   /**
-   * Returns the package name for a given Drupal project.
+   * Returns the package for a given Drupal project name, if it is installed.
    *
    * Although it is common for the package name to match the project name (for
    * example, a project name of `token` is likely part of the `drupal/token`
diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php
index 8d1fb28afbf705f6ef50ed8bb28ad97782826f2f..401bc31f00afe56122ef7f030c7cb6f09a4ad6a5 100644
--- a/package_manager/src/Stage.php
+++ b/package_manager/src/Stage.php
@@ -562,28 +562,6 @@ class Stage implements LoggerAwareInterface {
     }
   }
 
-  /**
-   * Returns a Composer utility object for the active directory.
-   *
-   * @return \Drupal\package_manager\ComposerUtility
-   *   The Composer utility object.
-   */
-  public function getActiveComposer(): ComposerUtility {
-    $dir = $this->pathLocator->getProjectRoot();
-    return ComposerUtility::createForDirectory($dir);
-  }
-
-  /**
-   * Returns a Composer utility object for the stage directory.
-   *
-   * @return \Drupal\package_manager\ComposerUtility
-   *   The Composer utility object.
-   */
-  public function getStageComposer(): ComposerUtility {
-    $dir = $this->getStageDirectory();
-    return ComposerUtility::createForDirectory($dir);
-  }
-
   /**
    * Attempts to claim the stage.
    *
@@ -758,9 +736,7 @@ class Stage implements LoggerAwareInterface {
    *   to ::require().
    *
    * @throws \InvalidArgumentException
-   *   Thrown if any of the given package names are invalid.
-   *
-   * @see https://getcomposer.org/doc/articles/composer-platform-dependencies.md
+   *   Thrown if any of the given package names fail basic validation.
    */
   protected static function validateRequirements(array $requirements): void {
     $version_parser = new VersionParser();
diff --git a/package_manager/tests/fixtures/FixtureUtilityTraitTest/missing_installed_php/vendor/composer/installed.json b/package_manager/tests/fixtures/FixtureUtilityTraitTest/missing_installed_php/vendor/composer/installed.json
deleted file mode 100644
index 4c9f037cd98f293bec9cab46745bb776f0324a26..0000000000000000000000000000000000000000
--- a/package_manager/tests/fixtures/FixtureUtilityTraitTest/missing_installed_php/vendor/composer/installed.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    "packages": [
-        {
-            "name": "the-org/the-package",
-            "version": "9.8.0"
-        }
-    ],
-    "dev-package-names": []
-}
\ No newline at end of file
diff --git a/package_manager/tests/fixtures/FixtureUtilityTraitTest/missing_installed_php/vendor/composer/installed.php b/package_manager/tests/fixtures/FixtureUtilityTraitTest/missing_installed_php/vendor/composer/installed.php
deleted file mode 100644
index e6e509a25db6f5aad8a9962d484c09db0fa96190..0000000000000000000000000000000000000000
--- a/package_manager/tests/fixtures/FixtureUtilityTraitTest/missing_installed_php/vendor/composer/installed.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-/**
- * @file
- * Fixture for  \Drupal\Tests\package_manager\Kernel\FixtureUtilityTraitTest::testModifyPackage().
- */
-
-// Composer Utility needs the versions key to be present.
-return [
-  'versions' => [],
-];
diff --git a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
index 5e4d7f7719687318b62903a2f98d1cf2152c4086..a38e4771dedc80209a42cb0b878a61f3ae2353aa 100644
--- a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
+++ b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php
@@ -237,7 +237,7 @@ class FixtureManipulator {
       return $this;
     }
 
-    $output = $this->runComposerCommand(array_filter(['remove', $name, $is_dev_requirement ? '--dev' : NULL, '--no-update']));
+    $output = $this->runComposerCommand(array_filter(['remove', $name, $is_dev_requirement ? '--dev' : NULL]));
     // `composer remove` will not set exit code 1 whenever a non-required
     // package is being removed.
     // @see \Composer\Command\RemoveCommand
@@ -245,10 +245,6 @@ class FixtureManipulator {
       $output->stderr = str_replace("./composer.json has been updated\n", '', $output->stderr);
       throw new \LogicException($output->stderr);
     }
-
-    // Make sure that `installed.json` & `installed.php` are updated.
-    // @todo Remove this when ComposerUtility gets removed.
-    $this->runComposerCommand(['update', $name]);
     return $this;
   }
 
@@ -624,15 +620,6 @@ class FixtureManipulator {
    * Sets up the path repos at absolute paths.
    */
   public function setUpRepos(): void {
-    // Some of the test coverage for FixtureManipulator tests edge cases for
-    // making installed.php invalid, and those test fixtures do NOT have a
-    // composer.json because ComposerUtility didn't look at that!
-    // @todo Remove this early return when ComposerUtility gets removed along
-    // with that edge case test coverage.
-    // @see fixtures/FixtureUtilityTraitTest/missing_installed_php
-    if (!file_exists($this->dir . '/composer.json')) {
-      return;
-    }
     $fs = new SymfonyFileSystem();
     $path_repo_base = \Drupal::state()->get(self::PATH_REPO_STATE_KEY);
     if (empty($path_repo_base)) {
diff --git a/package_manager/tests/src/Build/TemplateProjectTestBase.php b/package_manager/tests/src/Build/TemplateProjectTestBase.php
index 597c097384708100972f3f40c8305dfb4ec5cf0a..4b6195b7097bc7f11fc4e0bc62254f036e557f9c 100644
--- a/package_manager/tests/src/Build/TemplateProjectTestBase.php
+++ b/package_manager/tests/src/Build/TemplateProjectTestBase.php
@@ -256,7 +256,7 @@ END;
     // If using the drupal/recommended-project template, we don't expect there
     // to be an .htaccess file at the project root. One would normally be
     // generated by Composer when Package Manager or other code creates a
-    // ComposerUtility object in the active directory, except that Package
+    // ComposerInspector object in the active directory, except that Package
     // Manager takes specific steps to prevent that. So, here we're just
     // confirming that, in fact, Composer's .htaccess protection was disabled.
     // We don't do this for the drupal/legacy-project template because its
diff --git a/package_manager/tests/src/Kernel/ComposerUtilityTest.php b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
deleted file mode 100644
index 9f7a1f9945e98f99f0ee215f622aff5c955fa812..0000000000000000000000000000000000000000
--- a/package_manager/tests/src/Kernel/ComposerUtilityTest.php
+++ /dev/null
@@ -1,249 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Tests\package_manager\Kernel;
-
-use Drupal\Component\FileSystem\FileSystem as DrupalFileSystem;
-use Drupal\Core\Serialization\Yaml;
-use Drupal\fixture_manipulator\FixtureManipulator;
-use Drupal\KernelTests\KernelTestBase;
-use Drupal\package_manager\ComposerUtility;
-use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait;
-use Drupal\Tests\package_manager\Traits\ComposerInstallersTrait;
-use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;
-use Symfony\Component\Filesystem\Filesystem;
-
-/**
- * @coversDefaultClass \Drupal\package_manager\ComposerUtility
- * @group package_manager
- * @internal
- */
-class ComposerUtilityTest extends KernelTestBase {
-
-  use AssertPreconditionsTrait;
-  use ComposerInstallersTrait;
-  use FixtureUtilityTrait;
-
-  /**
-   * The temporary root directory for testing.
-   *
-   * @var string
-   */
-  protected string $rootDir;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = ['package_manager', 'update'];
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp(): void {
-    parent::setUp();
-
-    $this->rootDir = DrupalFileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . 'composer_utility_testing_root' . $this->databasePrefix;
-    $fs = new Filesystem();
-    if (is_dir($this->rootDir)) {
-      $fs->remove($this->rootDir);
-    }
-    $fs->mkdir($this->rootDir);
-    $fixture = $this->rootDir . DIRECTORY_SEPARATOR . 'fixture' . DIRECTORY_SEPARATOR;
-    static::copyFixtureFilesTo(__DIR__ . '/../../fixtures/fake_site', $fixture);
-    $this->installComposerInstallers($fixture);
-    $projects_dir = 'web/projects';
-    $manipulator = new FixtureManipulator();
-    $manipulator->addPackage(
-        [
-          'name' => 'drupal/package_project_match',
-          'type' => 'drupal-module',
-        ],
-        FALSE,
-        TRUE
-      );
-    $installer_paths["$projects_dir/package_project_match"] = ['drupal/package_project_match'];
-
-    $manipulator->addPackage(
-        [
-          'name' => 'drupal/not_match_package',
-          'type' => 'drupal-module',
-        ],
-        FALSE,
-        TRUE,
-        // Create an info.yml file with a different project name from the
-        // package.
-        ['not_match_project.info.yml' => Yaml::encode(['project' => 'not_match_project'])],
-      );
-    $installer_paths["$projects_dir/not_match_project"] = ['drupal/not_match_package'];
-    $manipulator->addPackage(
-        [
-          'name' => 'drupal/not_match_path_project',
-          'type' => 'drupal-module',
-        ],
-        FALSE,
-        TRUE,
-        []
-      );
-    $installer_paths["$projects_dir/not_match_path_project"] = ['drupal/not_match_path_project'];
-    $manipulator->addPackage(
-        [
-          'name' => 'drupal/nested_no_match_package',
-          'type' => 'drupal-module',
-        ],
-        FALSE,
-        TRUE,
-        // A test info.yml file where the folder names and info.yml file names
-        // do not match the project or package. Only the project key in this
-        // file need to match.
-        ['any_sub_folder/any_yml_file.info.yml' => Yaml::encode(['project' => 'nested_no_match_project'])],
-      );
-    $installer_paths["$projects_dir/any_folder_name"] = ['drupal/nested_no_match_package'];
-    $manipulator->addPackage(
-        [
-          'name' => 'non_drupal/other_project',
-          'type' => 'drupal-module',
-        ],
-        FALSE,
-        TRUE
-      );
-    $installer_paths["$projects_dir/other_project"] = ['non_drupal/other_project'];
-    $manipulator->addPackage(
-        [
-          'name' => 'drupal/custom_module',
-          'type' => 'drupal-custom-module',
-        ],
-        FALSE,
-        TRUE
-      );
-    $installer_paths["$projects_dir/custom_module"] = ['drupal/custom_module'];
-
-    // Commit the changes to 'installer-paths' first so that all the packages
-    // will be installed at the correct paths.
-    $this->setInstallerPaths($installer_paths, $fixture);
-    $manipulator->commitChanges($fixture);
-  }
-
-  /**
-   * Tests that ComposerUtility::CreateForDirectory() validates the directory.
-   */
-  public function testCreateForDirectoryValidation(): void {
-    $dir = $this->rootDir;
-    $this->expectException(\InvalidArgumentException::class);
-    $this->expectExceptionMessage('Composer could not find the config file: ' . $dir . DIRECTORY_SEPARATOR . 'composer.json');
-
-    ComposerUtility::createForDirectory($dir);
-  }
-
-  /**
-   * Tests that ComposerUtility disables automatic creation of .htaccess files.
-   */
-  public function testHtaccessProtectionDisabled(): void {
-    $dir = $this->rootDir;
-    file_put_contents($dir . '/composer.json', '{}');
-
-    ComposerUtility::createForDirectory($dir);
-    $this->assertFileDoesNotExist($dir . '/.htaccess');
-  }
-
-  /**
-   * @covers ::getProjectForPackage
-   *
-   * @param string $package
-   *   The package name.
-   * @param string|null $expected_project
-   *   The expected project if any, otherwise NULL.
-   *
-   * @dataProvider providerGetProjectForPackage
-   */
-  public function testGetProjectForPackage(string $package, ?string $expected_project): void {
-    $dir = $this->rootDir . DIRECTORY_SEPARATOR . 'fixture';
-    $this->assertSame($expected_project, ComposerUtility::createForDirectory($dir)->getProjectForPackage($package));
-  }
-
-  /**
-   * Data provider for ::testGetProjectForPackage().
-   *
-   * @return mixed[][]
-   *   The test cases.
-   */
-  public function providerGetProjectForPackage(): array {
-    return [
-      'package and project match' => [
-        'drupal/package_project_match',
-        'package_project_match',
-      ],
-      'package and project do not match' => [
-        'drupal/not_match_package',
-        'not_match_project',
-      ],
-      'vendor is not drupal' => [
-        'non_drupal/other_project',
-        NULL,
-      ],
-      'missing package' => [
-        'drupal/missing',
-        NULL,
-      ],
-      'nested_no_match' => [
-        'drupal/nested_no_match_package',
-        'nested_no_match_project',
-      ],
-      'unsupported package type' => [
-        'drupal/custom_module',
-        NULL,
-      ],
-    ];
-  }
-
-  /**
-   * @covers ::getPackageForProject
-   *
-   * @param string $project
-   *   The project name.
-   * @param string|null $expected_package
-   *   The expected package if any, otherwise NULL.
-   *
-   * @dataProvider providerGetPackageForProject
-   */
-  public function testGetPackageForProject(string $project, ?string $expected_package): void {
-    $dir = $this->rootDir . DIRECTORY_SEPARATOR . 'fixture';
-    $this->assertSame($expected_package, ComposerUtility::createForDirectory($dir)->getPackageForProject($project));
-  }
-
-  /**
-   * Data provider for ::testGetPackageForProject().
-   *
-   * @return mixed[][]
-   *   The test cases.
-   */
-  public function providerGetPackageForProject(): array {
-    return [
-      'package and project match' => [
-        'package_project_match',
-        'drupal/package_project_match',
-      ],
-      'package and project do not match' => [
-        'not_match_project',
-        'drupal/not_match_package',
-      ],
-      'package and project match + wrong installed path' => [
-        'not_match_path_project',
-        NULL,
-      ],
-      'vendor is not drupal' => [
-        'other_project',
-        NULL,
-      ],
-      'missing package' => [
-        'missing',
-        NULL,
-      ],
-      'nested_no_match' => [
-        'nested_no_match_project',
-        'drupal/nested_no_match_package',
-      ],
-    ];
-  }
-
-}
diff --git a/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php b/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php
index 7d742a8d62e78d947670809fbcdf6a7d4fdf6ca6..f48efba8203a06fbe339b0f315f4b7fef4624f62 100644
--- a/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php
+++ b/package_manager/tests/src/Kernel/FakeSiteFixtureTest.php
@@ -5,7 +5,6 @@ declare(strict_types = 1);
 namespace Drupal\Tests\package_manager\Kernel;
 
 use Drupal\fixture_manipulator\ActiveFixtureManipulator;
-use Drupal\package_manager\ComposerUtility;
 use Symfony\Component\Process\Process;
 
 /**
@@ -28,18 +27,15 @@ class FakeSiteFixtureTest extends PackageManagerKernelTestBase {
   }
 
   /**
-   * Tests calls to ComposerUtility class methods.
+   * Tests calls to ComposerInspector class methods.
    */
-  public function testCallToComposerUtilityMethods(): void {
+  public function testCallToComposerInspectorMethods(): void {
     $active_dir = $this->container->get('package_manager.path_locator')->getProjectRoot();
-    $composer_utility = ComposerUtility::createForDirectory($active_dir);
-    // Although the fake-site fixture does not contain any Composer packages or
-    // Drupal projects that would be returned from these methods calling them
-    // and asserting that they return NULL proves there are not any missing
-    // metadata in the fixture files that would cause these methods to throw an
-    // exception.
-    $this->assertNull($composer_utility->getProjectForPackage('any_random_name'));
-    $this->assertNull($composer_utility->getPackageForProject('drupal/any_random_name'));
+    /** @var \Drupal\package_manager\ComposerInspector $inspector */
+    $inspector = $this->container->get('package_manager.composer_inspector');
+    $list = $inspector->getInstalledPackagesList($active_dir);
+    $this->assertNull($list->getPackageByDrupalProjectName('any_random_name'));
+    $this->assertFalse(isset($list['drupal/any_random_name']));
   }
 
   /**
diff --git a/package_manager/tests/src/Unit/ComposerUtilityTest.php b/package_manager/tests/src/Unit/ComposerUtilityTest.php
deleted file mode 100644
index 7a004680bd175f92633848b0f0e5815699f6496d..0000000000000000000000000000000000000000
--- a/package_manager/tests/src/Unit/ComposerUtilityTest.php
+++ /dev/null
@@ -1,207 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Tests\package_manager\Unit;
-
-use Composer\Package\PackageInterface;
-use Drupal\package_manager\ComposerUtility;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * @coversDefaultClass \Drupal\package_manager\ComposerUtility
- * @group package_manager
- * @internal
- */
-class ComposerUtilityTest extends UnitTestCase {
-
-  /**
-   * Data provider for ::testCorePackages().
-   *
-   * @return \string[][][]
-   *   The test cases.
-   */
-  public function providerCorePackages(): array {
-    return [
-      'core-recommended not installed' => [
-        ['drupal/core'],
-        ['drupal/core'],
-      ],
-      'core-recommended installed' => [
-        ['drupal/core', 'drupal/core-recommended'],
-        ['drupal/core-recommended'],
-      ],
-    ];
-  }
-
-  /**
-   * @covers ::getCorePackages
-   *
-   * @param string[] $installed_package_names
-   *   The names of the packages that are installed.
-   * @param string[] $expected_core_package_names
-   *   The expected core package names that should be returned by
-   *   ::getCorePackages().
-   *
-   * @dataProvider providerCorePackages
-   */
-  public function testCorePackages(array $installed_package_names, array $expected_core_package_names): void {
-    $versions = array_fill(0, count($installed_package_names), '1.0.0');
-    $installed_packages = array_combine($installed_package_names, $versions);
-
-    $core_packages = $this->mockUtilityWithPackages($installed_packages)
-      ->getCorePackages();
-    $this->assertSame($expected_core_package_names, array_keys($core_packages));
-  }
-
-  /**
-   * @covers ::isValidRequirement
-   *
-   * @param bool $expected_is_valid
-   *   Whether the given requirement string is valid.
-   * @param string $requirement
-   *   The requirement string to validate.
-   *
-   * @dataProvider providerIsValidRequirement
-   */
-  public function testIsValidRequirement(bool $expected_is_valid, string $requirement): void {
-    $this->assertSame($expected_is_valid, ComposerUtility::isValidRequirement($requirement));
-  }
-
-  /**
-   * Data provider for ::testIsValidRequirement().
-   *
-   * @return \string[][][]
-   *   The test cases.
-   */
-  public function providerIsValidRequirement(): array {
-    return [
-      // Valid requirements.
-      [TRUE, 'vendor/package'],
-      [TRUE, 'vendor/snake_case'],
-      [TRUE, 'vendor/kebab-case'],
-      [TRUE, 'vendor/with.dots'],
-      [TRUE, '1vendor2/3package4'],
-      [TRUE, 'vendor/package:1'],
-      [TRUE, 'vendor/package:1.2'],
-      [TRUE, 'vendor/package:1.2.3'],
-      [TRUE, 'vendor/package:1.x'],
-      [TRUE, 'vendor/package:^1'],
-      [TRUE, 'vendor/package:~1'],
-      [TRUE, 'vendor/package:>1'],
-      [TRUE, 'vendor/package:<1'],
-      [TRUE, 'vendor/package:>=1'],
-      [TRUE, 'vendor/package:>1 <2'],
-      [TRUE, 'vendor/package:1 || 2'],
-      [TRUE, 'vendor/package:>=1,<1.1.0'],
-      [TRUE, 'vendor/package:1a'],
-      [TRUE, 'vendor/package:*'],
-      [TRUE, 'vendor/package:dev-master'],
-      [TRUE, 'vendor/package:*@dev'],
-      [TRUE, 'vendor/package:@dev'],
-      [TRUE, 'vendor/package:master@dev'],
-      [TRUE, 'vendor/package:master@beta'],
-      [TRUE, 'php'],
-      [TRUE, 'php:8'],
-      [TRUE, 'php:8.0'],
-      [TRUE, 'php:^8.1'],
-      [TRUE, 'php:~8.1'],
-      [TRUE, 'php-64bit'],
-      [TRUE, 'composer'],
-      [TRUE, 'composer-plugin-api'],
-      [TRUE, 'composer-plugin-api:1'],
-      [TRUE, 'ext-json'],
-      [TRUE, 'ext-json:1'],
-      [TRUE, 'ext-pdo_mysql'],
-      [TRUE, 'ext-pdo_mysql:1'],
-      [TRUE, 'lib-curl'],
-      [TRUE, 'lib-curl:1'],
-      [TRUE, 'lib-curl-zlib'],
-      [TRUE, 'lib-curl-zlib:1'],
-
-      // Invalid requirements.
-      [FALSE, ''],
-      [FALSE, ' '],
-      [FALSE, '/'],
-      [FALSE, 'php8'],
-      [FALSE, 'package'],
-      [FALSE, 'vendor\package'],
-      [FALSE, 'vendor//package'],
-      [FALSE, 'vendor/package1 vendor/package2'],
-      [FALSE, 'vendor/package/extra'],
-      [FALSE, 'vendor/package:a'],
-      [FALSE, 'vendor/package:'],
-      [FALSE, 'vendor/package::'],
-      [FALSE, 'vendor/package::1'],
-      [FALSE, 'vendor/package:1:2'],
-      [FALSE, 'vendor/package:develop@dev@dev'],
-      [FALSE, 'vendor/package:develop@'],
-      [FALSE, 'vEnDor/pAcKaGe'],
-      [FALSE, '_vendor/package'],
-      [FALSE, '_vendor/_package'],
-      [FALSE, 'vendor_/package'],
-      [FALSE, '_vendor/package_'],
-      [FALSE, 'vendor/package-'],
-      [FALSE, 'php-'],
-      [FALSE, 'ext'],
-      [FALSE, 'lib'],
-    ];
-  }
-
-  /**
-   * @covers ::getPackagesNotIn
-   * @covers ::getPackagesWithDifferentVersionsIn
-   */
-  public function testPackageComparison(): void {
-    $active = $this->mockUtilityWithPackages([
-      'drupal/existing' => '1.0.0',
-      'drupal/updated' => '1.0.0',
-      'drupal/removed' => '1.0.0',
-    ]);
-    $staged = $this->mockUtilityWithPackages([
-      'drupal/existing' => '1.0.0',
-      'drupal/updated' => '1.1.0',
-      'drupal/added' => '1.0.0',
-    ]);
-
-    $added = $staged->getPackagesNotIn($active);
-    $this->assertSame(['drupal/added'], array_keys($added));
-
-    $removed = $active->getPackagesNotIn($staged);
-    $this->assertSame(['drupal/removed'], array_keys($removed));
-
-    $updated = $active->getPackagesWithDifferentVersionsIn($staged);
-    $this->assertSame(['drupal/updated'], array_keys($updated));
-  }
-
-  /**
-   * Mocks a ComposerUtility object to return a set of installed packages.
-   *
-   * @param string[]|null[] $installed_packages
-   *   The installed packages that the mocked object should return. The keys are
-   *   the package names and the values are either a version number or NULL to
-   *   not mock the corresponding package's getVersion() method.
-   *
-   * @return \Drupal\package_manager\ComposerUtility|\PHPUnit\Framework\MockObject\MockObject
-   *   The mocked object.
-   */
-  private function mockUtilityWithPackages(array $installed_packages) {
-    $mock = $this->getMockBuilder(ComposerUtility::class)
-      ->disableOriginalConstructor()
-      ->onlyMethods(['getInstalledPackages'])
-      ->getMock();
-
-    $packages = [];
-    foreach ($installed_packages as $name => $version) {
-      $package = $this->createMock(PackageInterface::class);
-      if (isset($version)) {
-        $package->method('getVersion')->willReturn($version);
-      }
-      $packages[$name] = $package;
-    }
-    $mock->method('getInstalledPackages')->willReturn($packages);
-
-    return $mock;
-  }
-
-}
diff --git a/package_manager/tests/src/Unit/InstalledPackagesDataTest.php b/package_manager/tests/src/Unit/InstalledPackagesDataTest.php
deleted file mode 100644
index a486184f27441693fb5a18ac8ee8dab4aa788e20..0000000000000000000000000000000000000000
--- a/package_manager/tests/src/Unit/InstalledPackagesDataTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Tests\package_manager\Unit;
-
-use Composer\Autoload\ClassLoader;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * Tests retrieval of package data from Composer's `installed.php`.
- *
- * ComposerUtility relies on the internal structure of `installed.php` for
- * certain operations. This test is intended as an early warning if the file's
- * internal structure changes in a way that would break our functionality.
- *
- * @todo Delete this test in https://drupal.org/i/3316368.
- *
- * @group package_manager
- * @internal
- */
-class InstalledPackagesDataTest extends UnitTestCase {
-
-  /**
-   * Tests that Composer's `installed.php` file looks how we expect.
-   */
-  public function testInstalledPackagesData(): void {
-    $loaders = ClassLoader::getRegisteredLoaders();
-    $installed_php = key($loaders) . '/composer/installed.php';
-    $this->assertFileIsReadable($installed_php);
-    $data = include $installed_php;
-
-    // There should be a `versions` array whose keys are package names.
-    $this->assertIsArray($data['versions']);
-    $this->assertMatchesRegularExpression('|^[a-z0-9\-_]+/[a-z0-9\-_]+$|', key($data['versions']));
-
-    // The values of `versions` should be arrays of package information that
-    // includes a non-empty `install_path` string and a non-empty `type` string.
-    $package = reset($data['versions']);
-    $this->assertIsArray($package);
-    $this->assertNotEmpty($package['install_path']);
-    $this->assertIsString($package['install_path']);
-    $this->assertNotEmpty($package['type']);
-    $this->assertIsString($package['type']);
-  }
-
-}
diff --git a/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php b/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
index b9fa93ee751622cc5c6ce2ec5e6ccaa28c05e9f6..c75800ac54d464709eba4c5996789225fab93948 100644
--- a/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/RequestedUpdateValidatorTest.php
@@ -31,7 +31,7 @@ class RequestedUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
     // requested version of '9.8.1'. This also does not update all packages that
     // are expected to be updated when updating Drupal core.
     // @see \Drupal\automatic_updates\Updater::begin()
-    // @see \Drupal\package_manager\ComposerUtility::getCorePackages()
+    // @see \Drupal\package_manager\InstalledPackagesList::getCorePackages()
     $this->getStageFixtureManipulator()->setVersion('drupal/core-recommended', '9.8.2');
     $this->setCoreVersion('9.8.0');
     $this->setReleaseMetadata([
@@ -64,7 +64,7 @@ class RequestedUpdateValidatorTest extends AutomaticUpdatesKernelTestBase {
     $this->getStageFixtureManipulator()
       ->removePackage('drupal/core')
       ->removePackage('drupal/core-recommended')
-      ->removePackage('drupal/core-dev');
+      ->removePackage('drupal/core-dev', TRUE);
 
     $this->setCoreVersion('9.8.0');
     $this->setReleaseMetadata([
diff --git a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
index 0e81882e9161f37d99087cb6bcb1d976a36d0b08..bf92f74c33b34121dfa08fe794b676eaeb68ef0c 100644
--- a/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
+++ b/tests/src/Kernel/StatusCheck/StagedProjectsValidatorTest.php
@@ -132,7 +132,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
         TRUE
       )
       ->removePackage('other/removed')
-      ->removePackage('other/dev-removed');
+      ->removePackage('other/dev-removed', TRUE);
 
     $messages = [
       t("custom module 'drupal/dev-test-module2' installed."),
@@ -204,7 +204,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
     // The validator shouldn't complain about these packages being removed,
     // since it only cares about Drupal modules and themes.
       ->removePackage('other/removed')
-      ->removePackage('other/dev-removed')
+      ->removePackage('other/dev-removed', TRUE)
       ->setCorePackageVersion('9.8.1');
 
     $messages = [
@@ -350,7 +350,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
       ->setVersion('other/changed', '1.3.2')
       ->setVersion('other/dev-changed', '1.3.2')
       ->removePackage('other/removed')
-      ->removePackage('other/dev-removed');
+      ->removePackage('other/dev-removed', TRUE);
 
     $updater = $this->container->get('automatic_updates.updater');
     $updater->begin(['drupal' => '9.8.1']);