@Nonnull
	public JvmTransformResult transformJvm(@Nonnull List<Class<? extends JvmClassTransformer>> transformerClasses,
	                                       @Nullable JvmClassTransformerPredicate predicate) throws TransformationException {
		// Build transformer visitation order
		TransformerQueue queue = buildQueue(cast(transformerClasses));

		// Map to hold transformation errors for each class:transformer
		Map<ClassPathNode, Map<Class<? extends JvmClassTransformer>, Throwable>> transformJvmFailures = new IdentityHashMap<>();

		// Map to hold transformers to the paths of classes they have modified.
		Map<Class<? extends JvmClassTransformer>, Collection<ClassPathNode>> transformerToModifiedClasses = new IdentityHashMap<>();

		// Build the transformer context and apply all transformations in order
		List<JvmClassTransformer> transformers = queue.getTransformers();
		WorkspaceResource resource = workspace.getPrimaryResource();
		ResourcePathNode resourcePath = PathNodes.resourcePath(workspace, resource);
		JvmTransformerContext context = new JvmTransformerContext(workspace, resource, transformers);
		for (JvmClassTransformer transformer : transformers) {
			try {
				transformer.setup(context, workspace);
			} catch (Throwable t) {
				// If setup fails, abort the transformation
				String message = "Transformer '" + transformer.name() + "' failed on setup";
				logger.error(message, t);
				throw new TransformationException(message, t);
			}
		}
		resource.jvmClassBundleStreamRecursive().forEach(bundle -> {
			BundlePathNode bundlePathNode = resourcePath.child(bundle);
			for (JvmClassTransformer transformer : transformers) {
				bundle.forEach(cls -> {
					// Skip if the class does not pass the predicate
					if (predicate != null && !predicate.shouldTransform(workspace, resource, bundle, cls))
						return;

					try {
						context.resetTransformerTracking();
						transformer.transform(context, workspace, resource, bundle, cls);
						if (context.didTransformerDoWork()) {
							// Transformer modified this class, record the interaction
							ClassPathNode path = bundlePathNode.child(cls.getPackageName()).child(cls);
							transformerToModifiedClasses.computeIfAbsent(transformer.getClass(),
									t -> Collections.newSetFromMap(new IdentityHashMap<>())).add(path);
						}
					} catch (Throwable t) {
						logger.error("Transformer '{}' failed on class '{}'", transformer.name(), cls.getName(), t);
						ClassPathNode path = bundlePathNode.child(cls.getPackageName()).child(cls);
						var transformerToThrowable = transformJvmFailures.computeIfAbsent(path, p -> new IdentityHashMap<>());
						transformerToThrowable.put(transformer.getClass(), t);
					}
				});
			}
		});

		// Update the workspace contents with the transformation results
		Map<ClassPathNode, JvmClassInfo> transformedJvmClasses = context.buildChangeMap(inheritanceGraph);
		return new JvmTransformResult() {
			@Nonnull
			@Override
			public Map<ClassPathNode, Map<Class<? extends JvmClassTransformer>, Throwable>> getTransformerFailures() {
				return transformJvmFailures;
			}

			@Nonnull
			@Override
			public Map<ClassPathNode, JvmClassInfo> getTransformedClasses() {
				return transformedJvmClasses;
			}

			@Nonnull
			@Override
			public Set<ClassPathNode> getClassesToRemove() {
				return context.getClassesToRemove().stream()
						.map(workspace::findJvmClass)
						.collect(Collectors.toSet());
			}

			@Nonnull
			@Override
			public IntermediateMappings getMappingsToApply() {
				return context.getMappings();
			}

			@Nonnull
			@Override
			public Map<Class<? extends JvmClassTransformer>, Collection<ClassPathNode>> getModifiedClassesPerTransformer() {
				return transformerToModifiedClasses;
			}

			@Override
			public void apply() {
				// Dump transformed classes into the workspace
				checkedForEach(transformedJvmClasses, (path, cls) -> {
					JvmClassBundle bundle = path.getValueOfType(JvmClassBundle.class);
					if (bundle != null)
						bundle.put(cls);
				}, (path, cls, t) -> logger.error("Exception thrown handling transform application", t));

				// Delete classes that are marked for removal
				for (ClassPathNode path : getClassesToRemove()) {
					JvmClassBundle bundle = path.getValueOfType(JvmClassBundle.class);
					if (bundle != null)
						bundle.remove(path.getValue().getName());
				}

				// Apply mappings if they exist
				IntermediateMappings mappings = context.getMappings();
				if (!mappings.isEmpty()) {
					MappingResults results = mappingApplier.applyToPrimaryResource(mappings);
					results.apply();
				}
			}
		};
	}
@param transformerClasses
		JVM class transformers to run.
@param predicate
		Filter to control which JVM classes are transformed.
		Can be {@code null} to transform all JVM classes.

@return Result container with details about the transformation, including any failures, the transformed classes,
and the option to apply the transformations to the workspace.

@throws TransformationException
		When transformation cannot be run for any reason.
recaf-core/src/main/java/software/coley/recaf/services/transform/TransformationApplier.java