diff --git a/pom.xml b/pom.xml
index ebdd52f7..c07b55f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -140,6 +140,12 @@
org.apache.pdfbox
pdfbox
${pdfbox.version}
+
+
+ commons-logging
+ commons-logging
+
+
diff --git a/scripts/test_commons_logging_dependency_contract.py b/scripts/test_commons_logging_dependency_contract.py
new file mode 100644
index 00000000..e10716da
--- /dev/null
+++ b/scripts/test_commons_logging_dependency_contract.py
@@ -0,0 +1,48 @@
+import re
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+POM = ROOT / "pom.xml"
+
+
+def _pom_text() -> str:
+ return POM.read_text(encoding="utf-8")
+
+
+def _dependency_block(pom: str, group_id: str, artifact_id: str) -> str:
+ pattern = re.compile(
+ r"\s*"
+ rf"{re.escape(group_id)}\s*"
+ rf"{re.escape(artifact_id)}"
+ r".*?",
+ re.DOTALL,
+ )
+ match = pattern.search(pom)
+ assert match is not None
+ return match.group(0)
+
+
+def test_pdfbox_excludes_standalone_commons_logging_provider() -> None:
+ pom = _pom_text()
+ pdfbox = _dependency_block(pom, "org.apache.pdfbox", "pdfbox")
+
+ exclusion = re.compile(
+ r"\s*"
+ r"commons-logging\s*"
+ r"commons-logging\s*"
+ r""
+ )
+ assert exclusion.search(pdfbox) is not None
+
+
+def test_project_does_not_reintroduce_commons_logging_directly() -> None:
+ pom = _pom_text()
+
+ direct_commons_logging = re.compile(
+ r"\s*"
+ r"commons-logging\s*"
+ r"commons-logging"
+ )
+ assert direct_commons_logging.search(pom) is None
+ _dependency_block(pom, "org.springframework.boot", "spring-boot-starter-log4j2")
diff --git a/src/test/java/com/clearfolio/viewer/config/CommonsLoggingRuntimeBindingTest.java b/src/test/java/com/clearfolio/viewer/config/CommonsLoggingRuntimeBindingTest.java
new file mode 100644
index 00000000..1748203b
--- /dev/null
+++ b/src/test/java/com/clearfolio/viewer/config/CommonsLoggingRuntimeBindingTest.java
@@ -0,0 +1,35 @@
+package com.clearfolio.viewer.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that the supported runtime contains exactly one Commons Logging API provider.
+ */
+final class CommonsLoggingRuntimeBindingTest {
+
+ @Test
+ void commonsLoggingApiIsProvidedOnlyBySpringJcl() throws Exception {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ List providers = Collections.list(
+ loader.getResources("org/apache/commons/logging/LogFactory.class"))
+ .stream()
+ .map(URL::toExternalForm)
+ .sorted()
+ .toList();
+
+ assertEquals(
+ 1,
+ providers.size(),
+ () -> "Expected one Commons Logging API provider but found: " + providers);
+ assertTrue(
+ providers.getFirst().contains("spring-jcl"),
+ () -> "Expected Spring's spring-jcl bridge to own Commons Logging, but found: "
+ + providers.getFirst());
+ }
+}