--- 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 ``; profiles inherited from an ancestor are not listed at all, which is why module effective POMs have no `` section. So a profile moving between POMs shows up as a large `` addition on the declaring project and nothing anywhere else. Diff the `` 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 `` 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 ``/`` pairs from the `` 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.