---
name: verify-pom-refactor
description: Verify that a change to any pom.xml did not silently alter build behaviour. Use whenever a POM is restructured, a plugin or dependency is moved between sections, or a pluginManagement/dependencyManagement entry is pruned — Maven's model merging can change what executes with no visible symptom.
---

# Verifying a POM refactor

Maven's model merging is implicit enough that a POM change can alter behaviour with no visible symptom — that is the
common thread through the 4.0.1 obfuscation regression, the silent zero-test Failsafe run, and the parent POM
inlining. For any structural POM change, diff the **effective** POM rather than reading the source diff:

```bash
for m in . api build-tasks core jax-rs jsf maven-plugin obfuscate spi swing tests ui v1 v2-core v2-json v2-xml v4; do
  n=$(echo "$m" | sed 's|^\.$|ROOT|')
  for p in plain release; do
    [ "$p" = release ] && PF="-P sonatype-oss-release" || PF=""
    ./mvnw -o -q help:effective-pom -pl "$m" $PF -Doutput="$DIR/$n.$p.xml"
  done
done
```

Strip the `Generated by Maven Help Plugin on` line (it carries a timestamp), capture before and after, and diff. Two
things make the result readable:

- **A pure reordering is not a behaviour change** unless two plugins share a phase. Check with
  `diff <(sort before) <(sort after)`: if that is empty, only the order moved.
- **Inherited-but-inactive profiles are invisible.** `help:effective-pom` lists a project's *own* profiles and
  injects the *active* ones into `<build>`; profiles inherited from an ancestor are not listed at all, which is why
  module effective POMs have no `<profiles>` section. So a profile moving between POMs shows up as a large
  `<profiles>` addition on the declaring project and nothing anywhere else. Diff the `<build>` section separately —
  that is what actually executes.

The inlining commit was signed off on exactly this: all 32 module effective POMs content-identical, one adjacent
transposition (`truelicense-maven-plugin` ↔ `maven-antrun-plugin`, phases `process-classes` vs `prepare-package`) in
the release-profile ones, and the root's `<build>` unchanged. Profile declaration order in the root POM is arranged
to reproduce the old parent-then-child injection order for that reason, and for no other.

Then confirm against artifacts, not the log — `./mvnw -o clean install -P sonatype-oss-release -Dgpg.skip=true
-Dmaven.javadoc.skip=true`, followed by the `grep -rlaE '_clinit@|_string#'` check under *Build-time verification*
in `CLAUDE.md`.

## Pruning

For a **prune**, where the effective POM is *supposed* to change, invert the check: assert that the things which
must not move, didn't. Two comparisons cover it, and both caught nothing only because the prune was scoped from
evidence rather than from reading:

- **Resolved version of every plugin that executes a goal.** Extract the `<artifactId>`/`<version>` pairs from the
  `<build><plugins>` section of all 34 effective POMs and diff the sorted set. Removing a `pluginManagement` entry
  for a plugin that turns out to be bound somewhere silently downgrades it to a lifecycle default.
- **Full GAV and scope of every resolved dependency.** `./mvnw -o dependency:list` over the reactor, before and
  after, diffed. Removing a `dependencyManagement` entry can shift a *transitive* version even when no module
  declares that artifact directly. On the prune commit the only delta was ScalaCheck and its `test-interface`
  leaving the test classpath, which was the intent.
