From 090302095a6c2fd93d1ed9f98895ed266f265086 Mon Sep 17 00:00:00 2001 From: cuioss oliver <23139298+cuioss@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:45:37 +0200 Subject: [PATCH 1/2] test(arch): make the ADR-0005 framework-agnostic gate non-vacuous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule ran with allowEmptyShould(true), so a renamed or misspelled entry in AGNOSTIC_PACKAGES would have matched zero classes, found zero violations, and stayed green while protecting nothing. Drop that setting on the real rule and add a guard asserting every protected package resolves to at least one production class, naming the offending entry when it does not. Also extend FRAMEWORK_PACKAGES with io.smallrye.., io.netty.. and org.jboss..: the class Javadoc already names the SmallRye Fault-Tolerance guard as the coupling that keeps routing out of the agnostic set, and Netty/JBoss arrive transitively with the Quarkus/Vert.x stack. The negative control keeps allowEmptyShould(true) deliberately — there it is load-bearing, so an emptied control package fails assertThrows loudly instead of passing for the wrong reason. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0192te7j2ceUn2ASBjuXzWPU --- .../arch/FrameworkAgnosticArchTest.java | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/arch/FrameworkAgnosticArchTest.java b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/arch/FrameworkAgnosticArchTest.java index 1ec1ff46..84fa3e46 100644 --- a/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/arch/FrameworkAgnosticArchTest.java +++ b/api-sheriff/src/test/java/de/cuioss/sheriff/gateway/arch/FrameworkAgnosticArchTest.java @@ -17,7 +17,9 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; @@ -57,10 +59,21 @@ class FrameworkAgnosticArchTest { "de.cuioss.sheriff.gateway.pipeline.." }; - /** Framework packages that an agnostic-core class must never depend on. */ + /** + * Framework packages that an agnostic-core class must never depend on. + *

+ * {@code io.smallrye..} is listed because the class Javadoc names the per-route SmallRye + * Fault-Tolerance guard as precisely the coupling that keeps {@code routing} out of the agnostic + * set — leaving it unlisted would let an agnostic-core class acquire that same coupling unchecked. + * {@code io.netty..} and {@code org.jboss..} are listed for the same reason: they arrive + * transitively with the Quarkus/Vert.x stack and are framework by any reading of ADR-0005. + */ private static final String[] FRAMEWORK_PACKAGES = { "io.quarkus..", "io.vertx..", + "io.smallrye..", + "io.netty..", + "org.jboss..", "jakarta.enterprise..", "jakarta.inject..", "org.eclipse.microprofile..", @@ -78,12 +91,66 @@ void agnosticPackagesMustNotDependOnFrameworks() { .that().resideInAnyPackage(AGNOSTIC_PACKAGES) .should().dependOnClassesThat().resideInAnyPackage(FRAMEWORK_PACKAGES) .because("ADR-0005 requires config.model, config.validation, events, forward, and " - + "pipeline to remain framework-agnostic (routing is excluded by design)") - .allowEmptyShould(true); + + "pipeline to remain framework-agnostic (routing is excluded by design)"); rule.check(PRODUCTION_CLASSES); } + /** + * Guards the guard: every entry in {@link #AGNOSTIC_PACKAGES} must actually resolve to at least + * one production class. + *

+ * Without this, a renamed package or a single misspelled entry silently reduces the rule above to + * a no-op — it would match zero classes, find zero violations, and stay green while protecting + * nothing. The negative control below cannot catch that: it exercises its own hardcoded package, + * so it proves the ArchUnit mechanism works while saying nothing about whether the real package + * list still resolves. This test is what makes an empty match loud, and it names the offending + * entry rather than failing generically. + */ + @Test + @DisplayName("ADR-0005 gate is non-vacuous: every protected package resolves to at least one class") + void everyAgnosticPackageResolvesToClasses() { + for (String agnosticPackage : AGNOSTIC_PACKAGES) { + long matched = countClassesIn(agnosticPackage); + + assertTrue(matched > 0, + () -> "ADR-0005 protected package '" + agnosticPackage + "' resolved to NO " + + "production classes — the framework-agnostic rule above is silently " + + "protecting nothing. Fix the entry in AGNOSTIC_PACKAGES, or remove it " + + "deliberately if the package was retired."); + } + } + + /** + * Counts imported production classes residing in {@code packagePattern}, which uses the ArchUnit + * {@code ..} suffix to mean "this package and its subpackages". + *

+ * Deliberately a direct count rather than an {@link ArchRule} with an always-true condition: any + * such condition risks failing for its own reason instead of for emptiness — {@code bePublic() + * .orShould().bePackagePrivate()} looks universal but rejects a private nested class — which + * would make this guard red for a reason that has nothing to do with the gap it exists to detect. + */ + private static long countClassesIn(String packagePattern) { + String base = packagePattern.endsWith("..") + ? packagePattern.substring(0, packagePattern.length() - 2) + : packagePattern; + return PRODUCTION_CLASSES.stream() + .map(JavaClass::getPackageName) + .filter(name -> name.equals(base) || name.startsWith(base + ".")) + .count(); + } + + /** + * Negative control: proves the ArchUnit mechanism actually fails on a real violation, using the + * deliberately framework-coupled {@code quarkus} package as the standing specimen. + *

+ * {@code allowEmptyShould(true)} here is deliberate and must not be removed — the + * asymmetry with the rule above is load-bearing. If the control package ever empties out, this + * setting makes {@code check} pass, which makes {@code assertThrows} fail loudly and tells us the + * control has stopped controlling anything. Removing it would invert that: an empty package would + * make {@code check} throw on emptiness, {@code assertThrows} would be satisfied by the wrong + * exception, and the test would go green while proving nothing. + */ @Test @DisplayName("ADR-0005 gate detects a deliberate framework dependency (negative control)") void gateFailsOnFrameworkDependency() { From 2ee98b9dd5833ce62977a4ebbf7822f1b415cd2d Mon Sep 17 00:00:00 2001 From: cuioss oliver <23139298+cuioss@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:45:44 +0200 Subject: [PATCH 2/2] chore(steward): reconcile marshal.json after plan-marshall upgrade Steward upgrade against plan-marshall 0.1.1274: sync-defaults back-filled system.retention.no_plan_body_days and re-stamped the provisioning fields. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0192te7j2ceUn2ASBjuXzWPU --- .plan/marshal.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.plan/marshal.json b/.plan/marshal.json index 7a38bb43..e36da4df 100644 --- a/.plan/marshal.json +++ b/.plan/marshal.json @@ -303,9 +303,10 @@ "lessons_superseded_days": 0, "temp_on_maintenance": true, "plugin_cache_keep_versions": 5, - "plugin_cache_keep_days": 3 + "plugin_cache_keep_days": 3, + "no_plan_body_days": 7 }, - "provisioned_version": "0.1.1261", - "config_seed_fingerprint": "ccb21c71" + "provisioned_version": "0.1.1274", + "config_seed_fingerprint": "59c217c3" } }