feat(archunit-rules): NoBlanketCatch rule (W1 OTEL sweep, ADR-0013 §3) #15

Merged
hibryda merged 3 commits from feat/archunit-no-blanket-catch into main 2026-05-30 19:38:07 +02:00
Owner

NoBlanketCatch ArchUnit rule — W1 OTEL sweep (ADR-0013 §3)

TL;DR: shared rule forbidding catch (Exception|Throwable) outside the @RestControllerAdvice/@ControllerAdvice boundary, so domain failures stay typed (each service's XxxError + OtelErrorEvents). Introduced in warn mode (~115 pre-existing blanket catches across 8 services, audit Memora #3620). Sibling to EntityVersionParity.

How (the interesting part)

ArchUnit's high-level DSL can't inspect catch-clause types. The rule uses JavaCodeUnit.getTryCatchBlocks()TryCatchBlock.getCaughtThrowables() (archunit 1.3.0, verified via javap) and flags any block catching java.lang.Exception/Throwable. The boundary is excluded by annotation FQN string (areNotAnnotatedWith("...RestControllerAdvice")), so this module needs no spring-web compile dep (only the test fixture does → test scope).

Warn semantics

ArchUnit has no native warn. NoBlanketCatchArchTest.evaluateForWarning(classes) returns the failure-report text (or ""); the per-service test logs it without asserting → build stays green until catches are typed, then flip to a hard @ArchTest (mirrors the EntityVersionParity sample-in-main convention).

Self-test

NoBlanketCatchSelfTest + 3 fixtures: negative blanket-catch (flagged); positive specific-catch (IOException, allowed); positive @RestControllerAdvice catching Exception (allowed — exercises the FQN exclusion). Positive + negative packages imported separately (no cross-pollution; vacuous-pass guarded).

Verification

mvn -pl archunit-rules test → 4/4 green (2 new), BUILD SUCCESS, zero warnings (parent failOnWarning).

Footer. im2be-platform-libs • feat/archunit-no-blanket-catch → main • #332 W1 • verified mvn test.

## NoBlanketCatch ArchUnit rule — W1 OTEL sweep (ADR-0013 §3) **TL;DR:** shared rule forbidding `catch (Exception|Throwable)` outside the `@RestControllerAdvice`/`@ControllerAdvice` boundary, so domain failures stay typed (each service's `XxxError` + `OtelErrorEvents`). Introduced in **warn mode** (~115 pre-existing blanket catches across 8 services, audit Memora #3620). Sibling to `EntityVersionParity`. ### How (the interesting part) ArchUnit's high-level DSL can't inspect `catch`-clause types. The rule uses `JavaCodeUnit.getTryCatchBlocks()` → `TryCatchBlock.getCaughtThrowables()` (archunit 1.3.0, verified via `javap`) and flags any block catching `java.lang.Exception`/`Throwable`. The boundary is excluded by **annotation FQN string** (`areNotAnnotatedWith("...RestControllerAdvice")`), so this module needs **no spring-web compile dep** (only the test fixture does → test scope). ### Warn semantics ArchUnit has no native warn. `NoBlanketCatchArchTest.evaluateForWarning(classes)` returns the failure-report text (or `""`); the per-service test logs it without asserting → build stays green until catches are typed, then flip to a hard `@ArchTest` (mirrors the EntityVersionParity sample-in-main convention). ### Self-test `NoBlanketCatchSelfTest` + 3 fixtures: negative blanket-catch (flagged); positive specific-catch (`IOException`, allowed); positive `@RestControllerAdvice` catching `Exception` (allowed — exercises the FQN exclusion). Positive + negative packages imported separately (no cross-pollution; vacuous-pass guarded). ### Verification `mvn -pl archunit-rules test` → 4/4 green (2 new), BUILD SUCCESS, **zero warnings** (parent `failOnWarning`). **Footer.** im2be-platform-libs • feat/archunit-no-blanket-catch → main • #332 W1 • verified mvn test.
feat(archunit-rules): NoBlanketCatch rule — no catch(Exception|Throwable) outside boundary
All checks were successful
im2be-platform-libs CI / mvn install (pull_request) Successful in 1m13s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
2ee144000e
The shared ArchUnit rule for the W1 OTEL sweep (audit Memora #3620): domain failures
must be typed (each service's XxxError + OtelErrorEvents), so a blanket
catch (Exception|Throwable) is allowed only at the @RestControllerAdvice /
@ControllerAdvice boundary (ADR-0013 §3, Rule 02). ~115 blanket catches exist across
the 8 services; this rule surfaces them.

- NoBlanketCatch.noBlanketCatchOutsideBoundary() — ArchRule. Inspects catch-clause
  types via JavaCodeUnit.getTryCatchBlocks() → TryCatchBlock.getCaughtThrowables()
  (archunit 1.3.0; the high-level DSL can't see catch clauses). Boundary excluded by
  annotation FQN string → no spring-web COMPILE dep on this module.
- NoBlanketCatchArchTest.evaluateForWarning(JavaClasses) — warn-mode helper (services
  log the failure report, no assertion) until catches are typed, then flip to a hard
  @ArchTest. Mirrors EntityVersionParityArchTest's sample-in-main convention.
- Self-test (NoBlanketCatchSelfTest) + 3 fixtures: negative blanket-catch (flagged),
  positive specific-catch (allowed), positive @RestControllerAdvice boundary catching
  Exception (allowed — exercises the FQN exclusion). spring-web added test-scope only.

Verified: mvn -pl archunit-rules test → 4/4 green (2 new), BUILD SUCCESS, zero warnings
(parent failOnWarning). README archunit-rules entry updated.

Superseded by round 2.

Show previous round

hib-pr-reviewer review — PR #15 (affinity-intelligence-rework/im2be-platform-libs)

Round 1 — head 2ee144000eff, base main, trigger opened

TL;DR: CONDITIONAL_APPROVE — 1 agreed finding (Throwable fixture gap), 2 unique-to-A verified (RuntimeException scope + meta-annotation false positives), 2 unique-to-B verified (@ControllerAdvice fixture gap + Javadoc naming collision); all minor/info, no blocking issues.

Summary

Arbitration — NoBlanketCatch ArchUnit rule (PR #15, round 1)

First run for this PR (no prior Memora history). Read NoBlanketCatch.java, NoBlanketCatchSelfTest.java, NoBlanketCatchArchTest.java, BlanketCatchService.java, and BoundaryAdvice.java at HEAD.

Agreed (A-2 ∩ B-2): Both reviewers independently flagged missing catch (Throwable) negative fixture — kept as one finding (used B's line 53, the @Test start; A cited line 55 which is the @DisplayName).

Unique-to-A, verified: A-1 (RuntimeException absent from GENERIC_THROWABLES, no explanatory comment) confirmed at line 56–57 — kept. A-3 (areNotAnnotatedWith(String) checks only directly-present annotations, not meta-annotations) confirmed at lines 78–79 — kept; this is a real ArchUnit behaviour gap that will produce false positives for any consumer using a composed @GlobalExceptionHandler-style annotation.

Unique-to-B, verified: B-1 (no positive fixture exercising the @ControllerAdvice FQN constant; drift or typo would be undetected) confirmed by reading SelfTest lines 32–50 and BoundaryAdvice.java (uses only @RestControllerAdvice) — kept. B-3 (Javadoc block-mode template names consumer class NoBlanketCatchArchTest, identical to the library class itself) confirmed at NoBlanketCatchArchTest.java lines 32–35 — kept as info.

All five findings hold under verification. No blocking issues. Verdict: CONDITIONAL_APPROVE.

Memora run summary persisted (id 397).

Blast Radius

All changes are contained within the archunit-rules submodule, adding new rule infrastructure (one rule class, one helper, one self-test, two fixture classes). The rule is opt-in for downstream consumers. The meta-annotation gap (finding 3) is the widest concern — it will produce false positives for any consumer service that already uses a composed boundary annotation — but this surfaces as CI noise rather than a build break during the current warn-mode sweep.

BLAST_SCORE: 3/10

CI status (head 2ee144000eff)

Overall: ✓ success

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (5)

[MINOR] Agreed: no catch (Throwable) negative fixture — Throwable branch of GENERIC_THROWABLES untested

archunit-rules/src/test/java/com/aim2be/platform/archunit/NoBlanketCatchSelfTest.java:53

Both reviewers A and B independently flagged this. GENERIC_THROWABLES contains both "java.lang.Exception" and "java.lang.Throwable" (NoBlanketCatch.java line 57), but flagsBlanketCatchOutsideBoundary() (lines 53–70) only exercises catch (Exception) via BlanketCatchService. A regression or typo in the "java.lang.Throwable" entry — wrong package, wrong capitalisation — would go undetected by the self-test.

Fix: add a BlanketThrowableCatchService fixture to fixtures/negative/ with catch (Throwable t), add a negative.contain(BlanketThrowableCatchService.class) guard, and assert result.getFailureReport().toString().contains("Throwable") alongside the existing Exception assertion (or add a parallel @Test).

Verified: BlanketCatchService.java line 18 is catch (Exception e) only; no catch (Throwable …) anywhere in the fixture tree.

[MINOR] GENERIC_THROWABLES silently omits java.lang.RuntimeException — undocumented scope boundary

archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatch.java:57

GENERIC_THROWABLES = Set.of("java.lang.Exception", "java.lang.Throwable") does not include "java.lang.RuntimeException". A developer migrating catch (Exception e)catch (RuntimeException e) satisfies the rule letter but defeats the ADR-0013 §3 structured-error.event goal just as thoroughly, because RuntimeException is equally a blanket catch erasing typed error signals.

With no Javadoc explaining the intentional scope, every future reviewer (and every PR author who spots the bypass) will have the same question.

Fix (either):

  1. Add "java.lang.RuntimeException" to the set and add a BlanketCatchRuntimeExceptionService negative fixture.
  2. Add an explicit Javadoc comment like: "RuntimeException is intentionally excluded — unchecked blanket catches are covered by linting rule Y per ADR-0013 §3.2."

Verified: line 57 is Set.of("java.lang.Exception", "java.lang.Throwable").

[MINOR] areNotAnnotatedWith(String) checks only directly-present annotations — false positives for composed @GlobalExceptionHandler-style boundaries

archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatch.java:78

ArchUnit's areNotAnnotatedWith(String) (the FQN-string overload) tests only for annotations directly present on a class. A consuming service that introduces a composed annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestControllerAdvice  // meta-annotation
public @interface GlobalExceptionHandler {}

will have its @GlobalExceptionHandler classes not excluded — the rule will flag them as violations every CI run, burning trust with teams you need to adopt it.

Fix: replace the two areNotAnnotatedWith predicates with a DescribedPredicate that uses JavaClass.isMetaAnnotatedWith:

private static final DescribedPredicate<JavaClass> IS_BOUNDARY =
    DescribedPredicate.describe(
        "annotated or meta-annotated with @RestControllerAdvice / @ControllerAdvice",
        c -> c.isMetaAnnotatedWith(REST_CONTROLLER_ADVICE)
          || c.isMetaAnnotatedWith(CONTROLLER_ADVICE));

return classes()
        .that(IS_BOUNDARY.negate())
        .should(notCatchGenericThrowables())
        

Also add a meta-annotated fixture to passesForCompliantFixtures() so the path is tested.

Verified: lines 78–79 are areNotAnnotatedWith(REST_CONTROLLER_ADVICE) / areNotAnnotatedWith(CONTROLLER_ADVICE).

[MINOR] No positive fixture exercises the @ControllerAdvice exclusion branch — FQN constant untested

archunit-rules/src/test/java/com/aim2be/platform/archunit/NoBlanketCatchSelfTest.java:32

passesForCompliantFixtures() (lines 32–50) pins only BoundaryAdvice (@RestControllerAdvice) and SpecificCatchService. The second exclusion constant CONTROLLER_ADVICE = "org.springframework.web.bind.annotation.ControllerAdvice" (NoBlanketCatch.java line 52–53) has zero fixture coverage. A typo in that FQN string, or a Spring package relocation, would never be caught by the self-test.

Fix: add a ControllerAdviceWithBlanketCatch fixture to fixtures/positive/ annotated @ControllerAdvice and catching Exception. Add a positive.contain(ControllerAdviceWithBlanketCatch.class) guard and assert the result has no violation, parallel to the existing BoundaryAdvice block.

Verified: BoundaryAdvice.java uses @RestControllerAdvice only; no @ControllerAdvice fixture exists anywhere in the fixture tree.

[INFO] Block-mode Javadoc template names consumer class NoBlanketCatchArchTest — identical to this library class

archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatchArchTest.java:33

The BLOCK-mode template in lines 30–35 of the Javadoc recommends:

class NoBlanketCatchArchTest {
    @ArchTest
    static final ArchRule rule = NoBlanketCatch.noBlanketCatchOutsideBoundary();
}

A consumer who copies this verbatim creates a class named NoBlanketCatchArchTest in their own package, shadowing the library helper in IDE auto-complete and confusing readers who expect the library's NoBlanketCatchArchTest to be the warn-mode helper (not a consumer test class).

Fix: rename the template class in the Javadoc to NoBlanketCatchTest or NoBlanketCatchHardBlockTest, e.g.:

class NoBlanketCatchTest {
    @ArchTest
    static final ArchRule rule = NoBlanketCatch.noBlanketCatchOutsideBoundary();
}

Verified: lines 32–35 of NoBlanketCatchArchTest.java show the template class is literally class NoBlanketCatchArchTest.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 1 • 5 findings (4m/1i) • 2026-05-30T17:14:41.449Z → 2026-05-30T17:17:07.444Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 2._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #15 (affinity-intelligence-rework/im2be-platform-libs) **Round 1** — head `2ee144000eff`, base `main`, trigger `opened` **TL;DR:** CONDITIONAL_APPROVE — 1 agreed finding (Throwable fixture gap), 2 unique-to-A verified (RuntimeException scope + meta-annotation false positives), 2 unique-to-B verified (@ControllerAdvice fixture gap + Javadoc naming collision); all minor/info, no blocking issues. ### Summary ## Arbitration — NoBlanketCatch ArchUnit rule (PR #15, round 1) First run for this PR (no prior Memora history). Read `NoBlanketCatch.java`, `NoBlanketCatchSelfTest.java`, `NoBlanketCatchArchTest.java`, `BlanketCatchService.java`, and `BoundaryAdvice.java` at HEAD. **Agreed (A-2 ∩ B-2):** Both reviewers independently flagged missing `catch (Throwable)` negative fixture — kept as one finding (used B's line 53, the `@Test` start; A cited line 55 which is the `@DisplayName`). **Unique-to-A, verified:** A-1 (`RuntimeException` absent from `GENERIC_THROWABLES`, no explanatory comment) confirmed at line 56–57 — kept. A-3 (`areNotAnnotatedWith(String)` checks only directly-present annotations, not meta-annotations) confirmed at lines 78–79 — kept; this is a real ArchUnit behaviour gap that will produce false positives for any consumer using a composed `@GlobalExceptionHandler`-style annotation. **Unique-to-B, verified:** B-1 (no positive fixture exercising the `@ControllerAdvice` FQN constant; drift or typo would be undetected) confirmed by reading SelfTest lines 32–50 and BoundaryAdvice.java (uses only `@RestControllerAdvice`) — kept. B-3 (Javadoc block-mode template names consumer class `NoBlanketCatchArchTest`, identical to the library class itself) confirmed at NoBlanketCatchArchTest.java lines 32–35 — kept as info. All five findings hold under verification. No blocking issues. Verdict: CONDITIONAL_APPROVE. Memora run summary persisted (id 397). ### Blast Radius All changes are contained within the `archunit-rules` submodule, adding new rule infrastructure (one rule class, one helper, one self-test, two fixture classes). The rule is opt-in for downstream consumers. The meta-annotation gap (finding 3) is the widest concern — it will produce false positives for any consumer service that already uses a composed boundary annotation — but this surfaces as CI noise rather than a build break during the current warn-mode sweep. **BLAST_SCORE: 3/10** ### CI status (head `2ee144000eff`) **Overall: ✓ success** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/92/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/92/jobs/1) | ### Findings (5) #### **[MINOR]** Agreed: no `catch (Throwable)` negative fixture — Throwable branch of `GENERIC_THROWABLES` untested _archunit-rules/src/test/java/com/aim2be/platform/archunit/NoBlanketCatchSelfTest.java:53_ Both reviewers A and B independently flagged this. `GENERIC_THROWABLES` contains both `"java.lang.Exception"` and `"java.lang.Throwable"` (NoBlanketCatch.java line 57), but `flagsBlanketCatchOutsideBoundary()` (lines 53–70) only exercises `catch (Exception)` via `BlanketCatchService`. A regression or typo in the `"java.lang.Throwable"` entry — wrong package, wrong capitalisation — would go undetected by the self-test. **Fix:** add a `BlanketThrowableCatchService` fixture to `fixtures/negative/` with `catch (Throwable t)`, add a `negative.contain(BlanketThrowableCatchService.class)` guard, and assert `result.getFailureReport().toString().contains("Throwable")` alongside the existing `Exception` assertion (or add a parallel `@Test`). Verified: `BlanketCatchService.java` line 18 is `catch (Exception e)` only; no `catch (Throwable …)` anywhere in the fixture tree. #### **[MINOR]** `GENERIC_THROWABLES` silently omits `java.lang.RuntimeException` — undocumented scope boundary _archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatch.java:57_ `GENERIC_THROWABLES = Set.of("java.lang.Exception", "java.lang.Throwable")` does not include `"java.lang.RuntimeException"`. A developer migrating `catch (Exception e)` → `catch (RuntimeException e)` satisfies the rule letter but defeats the ADR-0013 §3 structured-`error.event` goal just as thoroughly, because `RuntimeException` is equally a blanket catch erasing typed error signals. With no Javadoc explaining the intentional scope, every future reviewer (and every PR author who spots the bypass) will have the same question. **Fix (either):** 1. Add `"java.lang.RuntimeException"` to the set and add a `BlanketCatchRuntimeExceptionService` negative fixture. 2. Add an explicit Javadoc comment like: *"RuntimeException is intentionally excluded — unchecked blanket catches are covered by linting rule Y per ADR-0013 §3.2."* Verified: line 57 is `Set.of("java.lang.Exception", "java.lang.Throwable")`. #### **[MINOR]** `areNotAnnotatedWith(String)` checks only directly-present annotations — false positives for composed `@GlobalExceptionHandler`-style boundaries _archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatch.java:78_ ArchUnit's `areNotAnnotatedWith(String)` (the FQN-string overload) tests only for annotations **directly present** on a class. A consuming service that introduces a composed annotation: ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @RestControllerAdvice // meta-annotation public @interface GlobalExceptionHandler {} ``` will have its `@GlobalExceptionHandler` classes **not excluded** — the rule will flag them as violations every CI run, burning trust with teams you need to adopt it. **Fix:** replace the two `areNotAnnotatedWith` predicates with a `DescribedPredicate` that uses `JavaClass.isMetaAnnotatedWith`: ```java private static final DescribedPredicate<JavaClass> IS_BOUNDARY = DescribedPredicate.describe( "annotated or meta-annotated with @RestControllerAdvice / @ControllerAdvice", c -> c.isMetaAnnotatedWith(REST_CONTROLLER_ADVICE) || c.isMetaAnnotatedWith(CONTROLLER_ADVICE)); return classes() .that(IS_BOUNDARY.negate()) .should(notCatchGenericThrowables()) … ``` Also add a meta-annotated fixture to `passesForCompliantFixtures()` so the path is tested. Verified: lines 78–79 are `areNotAnnotatedWith(REST_CONTROLLER_ADVICE)` / `areNotAnnotatedWith(CONTROLLER_ADVICE)`. #### **[MINOR]** No positive fixture exercises the `@ControllerAdvice` exclusion branch — FQN constant untested _archunit-rules/src/test/java/com/aim2be/platform/archunit/NoBlanketCatchSelfTest.java:32_ `passesForCompliantFixtures()` (lines 32–50) pins only `BoundaryAdvice` (`@RestControllerAdvice`) and `SpecificCatchService`. The second exclusion constant `CONTROLLER_ADVICE = "org.springframework.web.bind.annotation.ControllerAdvice"` (NoBlanketCatch.java line 52–53) has zero fixture coverage. A typo in that FQN string, or a Spring package relocation, would never be caught by the self-test. **Fix:** add a `ControllerAdviceWithBlanketCatch` fixture to `fixtures/positive/` annotated `@ControllerAdvice` and catching `Exception`. Add a `positive.contain(ControllerAdviceWithBlanketCatch.class)` guard and assert the result has no violation, parallel to the existing `BoundaryAdvice` block. Verified: `BoundaryAdvice.java` uses `@RestControllerAdvice` only; no `@ControllerAdvice` fixture exists anywhere in the fixture tree. #### **[INFO]** Block-mode Javadoc template names consumer class `NoBlanketCatchArchTest` — identical to this library class _archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatchArchTest.java:33_ The BLOCK-mode template in lines 30–35 of the Javadoc recommends: ```java class NoBlanketCatchArchTest { @ArchTest static final ArchRule rule = NoBlanketCatch.noBlanketCatchOutsideBoundary(); } ``` A consumer who copies this verbatim creates a class named `NoBlanketCatchArchTest` in their own package, shadowing the library helper in IDE auto-complete and confusing readers who expect the library's `NoBlanketCatchArchTest` to be the warn-mode helper (not a consumer test class). **Fix:** rename the template class in the Javadoc to `NoBlanketCatchTest` or `NoBlanketCatchHardBlockTest`, e.g.: ```java class NoBlanketCatchTest { @ArchTest static final ArchRule rule = NoBlanketCatch.noBlanketCatchOutsideBoundary(); } ``` Verified: lines 32–35 of NoBlanketCatchArchTest.java show the template class is literally `class NoBlanketCatchArchTest`. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 1 • 5 findings (4m/1i) • 2026-05-30T17:14:41.449Z → 2026-05-30T17:17:07.444Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
fix(archunit-rules): address NoBlanketCatch #15 R1 findings (5 minor)
All checks were successful
im2be-platform-libs CI / mvn install (pull_request) Successful in 1m18s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
0e4e5e7441
R1 verdict CONDITIONAL_APPROVE; 5 minor/info findings (kept=5):

(1) MINOR NoBlanketCatchSelfTest — Throwable branch of GENERIC_THROWABLES
    untested (only catch(Exception) exercised). Added BlanketThrowableCatchService
    negative fixture + flagsBlanketThrowableCatch test asserting violation-specific
    "catches Throwable" text.
(2) MINOR NoBlanketCatch:57 — RuntimeException silently omitted, undocumented
    scope bypass. The per-service typed XxxError domain types are unchecked
    (RuntimeException subclasses), so catch(RuntimeException) erases the typed
    signal exactly as catch(Exception) does. Added "java.lang.RuntimeException"
    to GENERIC_THROWABLES + BlanketRuntimeCatchService fixture + Javadoc rationale.
(3) MINOR NoBlanketCatch:78 — areNotAnnotatedWith(String) matches only directly
    -present annotations → composed @GlobalExceptionHandler stereotypes
    (meta-annotated with @RestControllerAdvice) would be false-flagged every CI
    run. Replaced the two areNotAnnotatedWith predicates with an IS_BOUNDARY
    DescribedPredicate that checks isAnnotatedWith||isMetaAnnotatedWith for both
    FQNs. Added GlobalExceptionHandler composed-annotation fixture +
    ComposedBoundaryAdvice consumer (regression guard).
(4) MINOR self-test — no plain @ControllerAdvice positive fixture (only
    @RestControllerAdvice). Added ControllerAdviceBoundary fixture.
(5) INFO NoBlanketCatchArchTest — *ArchTest suffix implies a JUnit test.
    Defended (mirrors sibling EntityVersionParityArchTest, rule 10); added a
    Javadoc note clarifying it is a src/main consumer-facing helper with no
    @Test/@ArchTest member, never auto-discovered.

Ripple (rule 63): IS_BOUNDARY collapses the two-clause that() into one
predicate; a single direct-or-meta check on each Spring FQN covers both
annotations and any composed stereotype (because @RestControllerAdvice is
itself meta-annotated with @ControllerAdvice). README module-map line updated
to the widened scope. No consumer wiring exists yet (rule gated on merge+deploy)
so no downstream ripple.

Verification:
- mvn -pl archunit-rules test → Tests run: 6, Failures: 0, Errors: 0 (BUILD SUCCESS)
- NoBlanketCatchSelfTest 2 → 4 tests; composed-boundary test passing empirically
  confirms the meta-annotation exclusion works
- zero [WARNING]/[ERROR] lines (parent failOnWarning enforced)

Superseded by round 3.

Show previous round

hib-pr-reviewer review — PR #15 (affinity-intelligence-rework/im2be-platform-libs)

Round 2 — head 0e4e5e74417b, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept all 3 unique-to-one findings after direct code verification; no blocking issues; 0 dropped.

Summary

Arbitration — Round 2

All three findings are unique-to-one. Each was verified with Read against the HEAD checkout before a keep/drop decision.

A-1 — KEEP (verified)

NoBlanketCatch.java:100 shows @AnalyzeClasses(packagesOf = ApplicationMainClass.class) with no importOptions. The canonical BLOCK-mode template in NoBlanketCatchArchTest.java:30-31 correctly includes importOptions = ImportOption.DoNotIncludeTests.class. A developer copying the Javadoc snippet from the rule class itself (the first natural reference point) would inadvertently scan test-scoped helpers, producing false positives. Confirmed present, confirmed divergent from canonical template.

A-2 — KEEP (verified)

NoBlanketCatchArchTest.java:32 shows class NoBlanketCatchArchTest as the BLOCK-mode template class name — identical to the library helper class. The Javadoc note on lines 38-46 explains the naming rationale but does not remove the footgun: a consumer team that adds the BLOCK-mode class before removing their WARN-mode test file (same package) will shadow the library import and break compilation of any evaluateForWarning() callsite. Confirmed in file at HEAD. Keeping with A's defer annotation (safe follow-on).

B-1 — KEEP (verified)

NoBlanketCatchSelfTest.java does not import NoBlanketCatchArchTest at all; every evaluation path calls NoBlanketCatch.noBlanketCatchOutsideBoundary().evaluate() directly. The evaluateForWarning() wrapper (lines 64-67 of NoBlanketCatchArchTest.java) — specifically its result.hasViolation() ? result.getFailureReport().toString() : "" branch — is entirely untested. An accidental inversion of the condition would go undetected by the current suite. Confirmed no coverage at HEAD.

Dropped: 0 findings. Kept: 3 (all minor). No blocking issues found.

Blast Radius

The diff adds a new shared ArchUnit rule to platform-libs that every service in the mono will onboard during the L0-T0 #7 OTEL sweep. The rule logic and predicate are self-contained, but the public evaluateForWarning() wrapper — the primary consumer-facing API — ships with an untested branch. The two documentation gaps (missing importOptions, shadowing template name) could silently misdirect consuming teams during the rollout.

BLAST_SCORE: 4/10

Risk Indicators

Indicator Value
Sensitive functions NoBlanketCatchArchTest.evaluateForWarning, NoBlanketCatch.noBlanketCatchOutsideBoundary
Migration touched
Test delta
Dependency changes

CI status (head 0e4e5e74417b)

Overall: ✓ success

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (3)

[MINOR] Block-mode Javadoc snippet omits importOptions = ImportOption.DoNotIncludeTests.class

archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatch.java:100

Line 100 shows:

@AnalyzeClasses(packagesOf = ApplicationMainClass.class)
class NoBlanketCatchArchTest {  }

The canonical BLOCK-mode template in NoBlanketCatchArchTest.java:30-31 correctly includes:

@AnalyzeClasses(packagesOf = UserServiceApplication.class,
                importOptions = ImportOption.DoNotIncludeTests.class)

A developer copying the snippet from the rule's own Javadoc (the most natural first reference point) would inadvertently scan test-utility and fixture classes. Because the self-test fixtures include three negative services with blanket catches, this would produce false positives when a consumer's production package happens to be on the same classpath scan. Fix: add importOptions = ImportOption.DoNotIncludeTests.class to the snippet at line 100 to match the canonical template.

[MINOR] BLOCK-mode template class name NoBlanketCatchArchTest is identical to the library helper — shadowing risk

archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatchArchTest.java:32

The BLOCK-mode Javadoc snippet (lines 29-35) names the consumer class NoBlanketCatchArchTest — the same identifier as the library helper being imported for evaluateForWarning() in WARN mode. The Javadoc note at lines 38-46 explains the naming rationale but does not eliminate the footgun: if a consuming service's WARN-mode test file (FooTest.java) imports com.aim2be.platform.archunit.NoBlanketCatchArchTest and that file lives in the same package as the new BLOCK-mode class, the unqualified reference resolves to the consumer-defined class, which has no evaluateForWarning method, breaking compilation. Teams routinely add before they remove.

Suggested mitigation (does not require renaming the library class): rename the template class in the snippet to ApplicationNoBlanketCatchArchTest or another service-namespaced identifier to make it unambiguously a new consumer artifact. The same change should be applied to the parallel snippet in NoBlanketCatch.java:101.

Deferred: safe to address in a follow-on PR once the W1→W2 sweep is underway and consuming services begin adding the BLOCK-mode class.

[MINOR] NoBlanketCatchArchTest.evaluateForWarning() — public consumer-facing API is never exercised by the self-test

archunit-rules/src/test/java/com/aim2be/platform/archunit/NoBlanketCatchSelfTest.java:98

NoBlanketCatchArchTest is not imported anywhere in NoBlanketCatchSelfTest.java. Every evaluation path in the self-test calls NoBlanketCatch.noBlanketCatchOutsideBoundary().evaluate() directly, bypassing the public wrapper entirely. The wrapper's branch logic — result.hasViolation() ? result.getFailureReport().toString() : "" (lines 64-67 of NoBlanketCatchArchTest.java) — is the entry point documented in the README and class Javadoc for consuming services operating in warn mode. If that branch were accidentally inverted or the method returned early, the self-test would still pass.

Add two assertions alongside the existing structure:

@Test
@DisplayName("evaluateForWarning: returns empty string for compliant classes")
void evaluateForWarningReturnsEmptyWhenCompliant() {
    JavaClasses positive = new ClassFileImporter().importPackages(POSITIVE);
    assertThat(NoBlanketCatchArchTest.evaluateForWarning(positive)).isEmpty();
}

@Test
@DisplayName("evaluateForWarning: returns non-empty report for violating classes")
void evaluateForWarningReturnsReportWhenViolating() {
    JavaClasses negative = new ClassFileImporter().importPackages(NEGATIVE);
    assertThat(NoBlanketCatchArchTest.evaluateForWarning(negative)).isNotEmpty();
}

This exercises the wrapper branch that consuming services actually invoke.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 2 • 3 findings (3m) • 2026-05-30T17:29:15.614Z → 2026-05-30T17:31:32.662Z • posted-as: pr-reviewer-bot • model: auto

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 3._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #15 (affinity-intelligence-rework/im2be-platform-libs) **Round 2** — head `0e4e5e74417b`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept all 3 unique-to-one findings after direct code verification; no blocking issues; 0 dropped. ### Summary ## Arbitration — Round 2 All three findings are unique-to-one. Each was verified with `Read` against the HEAD checkout before a keep/drop decision. ### A-1 — KEEP (verified) `NoBlanketCatch.java:100` shows `@AnalyzeClasses(packagesOf = ApplicationMainClass.class)` with no `importOptions`. The canonical BLOCK-mode template in `NoBlanketCatchArchTest.java:30-31` correctly includes `importOptions = ImportOption.DoNotIncludeTests.class`. A developer copying the Javadoc snippet from the rule class itself (the first natural reference point) would inadvertently scan test-scoped helpers, producing false positives. Confirmed present, confirmed divergent from canonical template. ### A-2 — KEEP (verified) `NoBlanketCatchArchTest.java:32` shows `class NoBlanketCatchArchTest` as the BLOCK-mode template class name — identical to the library helper class. The Javadoc note on lines 38-46 explains the naming rationale but does not remove the footgun: a consumer team that adds the BLOCK-mode class before removing their WARN-mode test file (same package) will shadow the library import and break compilation of any `evaluateForWarning()` callsite. Confirmed in file at HEAD. Keeping with A's `defer` annotation (safe follow-on). ### B-1 — KEEP (verified) `NoBlanketCatchSelfTest.java` does not import `NoBlanketCatchArchTest` at all; every evaluation path calls `NoBlanketCatch.noBlanketCatchOutsideBoundary().evaluate()` directly. The `evaluateForWarning()` wrapper (lines 64-67 of `NoBlanketCatchArchTest.java`) — specifically its `result.hasViolation() ? result.getFailureReport().toString() : ""` branch — is entirely untested. An accidental inversion of the condition would go undetected by the current suite. Confirmed no coverage at HEAD. **Dropped**: 0 findings. **Kept**: 3 (all minor). No blocking issues found. ### Blast Radius The diff adds a new shared ArchUnit rule to platform-libs that every service in the mono will onboard during the L0-T0 #7 OTEL sweep. The rule logic and predicate are self-contained, but the public `evaluateForWarning()` wrapper — the primary consumer-facing API — ships with an untested branch. The two documentation gaps (missing `importOptions`, shadowing template name) could silently misdirect consuming teams during the rollout. **BLAST_SCORE: 4/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `NoBlanketCatchArchTest.evaluateForWarning`, `NoBlanketCatch.noBlanketCatchOutsideBoundary` | | Migration touched | — | | Test delta | — | | Dependency changes | — | ### CI status (head `0e4e5e74417b`) **Overall: ✓ success** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/93/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/93/jobs/1) | ### Findings (3) #### **[MINOR]** Block-mode Javadoc snippet omits `importOptions = ImportOption.DoNotIncludeTests.class` _archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatch.java:100_ Line 100 shows: ```java @AnalyzeClasses(packagesOf = ApplicationMainClass.class) class NoBlanketCatchArchTest { … } ``` The canonical BLOCK-mode template in `NoBlanketCatchArchTest.java:30-31` correctly includes: ```java @AnalyzeClasses(packagesOf = UserServiceApplication.class, importOptions = ImportOption.DoNotIncludeTests.class) ``` A developer copying the snippet from the rule's own Javadoc (the most natural first reference point) would inadvertently scan test-utility and fixture classes. Because the self-test fixtures include three negative services with blanket catches, this would produce false positives when a consumer's production package happens to be on the same classpath scan. Fix: add `importOptions = ImportOption.DoNotIncludeTests.class` to the snippet at line 100 to match the canonical template. #### **[MINOR]** BLOCK-mode template class name `NoBlanketCatchArchTest` is identical to the library helper — shadowing risk _archunit-rules/src/main/java/com/aim2be/platform/archunit/NoBlanketCatchArchTest.java:32_ The BLOCK-mode Javadoc snippet (lines 29-35) names the consumer class `NoBlanketCatchArchTest` — the same identifier as the library helper being imported for `evaluateForWarning()` in WARN mode. The Javadoc note at lines 38-46 explains the naming rationale but does not eliminate the footgun: if a consuming service's WARN-mode test file (`FooTest.java`) imports `com.aim2be.platform.archunit.NoBlanketCatchArchTest` and that file lives in the same package as the new BLOCK-mode class, the unqualified reference resolves to the consumer-defined class, which has no `evaluateForWarning` method, breaking compilation. Teams routinely add before they remove. Suggested mitigation (does not require renaming the library class): rename the template class in the snippet to `ApplicationNoBlanketCatchArchTest` or another service-namespaced identifier to make it unambiguously a new consumer artifact. The same change should be applied to the parallel snippet in `NoBlanketCatch.java:101`. *Deferred: safe to address in a follow-on PR once the W1→W2 sweep is underway and consuming services begin adding the BLOCK-mode class.* #### **[MINOR]** `NoBlanketCatchArchTest.evaluateForWarning()` — public consumer-facing API is never exercised by the self-test _archunit-rules/src/test/java/com/aim2be/platform/archunit/NoBlanketCatchSelfTest.java:98_ `NoBlanketCatchArchTest` is not imported anywhere in `NoBlanketCatchSelfTest.java`. Every evaluation path in the self-test calls `NoBlanketCatch.noBlanketCatchOutsideBoundary().evaluate()` directly, bypassing the public wrapper entirely. The wrapper's branch logic — `result.hasViolation() ? result.getFailureReport().toString() : ""` (lines 64-67 of `NoBlanketCatchArchTest.java`) — is the entry point documented in the README and class Javadoc for consuming services operating in warn mode. If that branch were accidentally inverted or the method returned early, the self-test would still pass. Add two assertions alongside the existing structure: ```java @Test @DisplayName("evaluateForWarning: returns empty string for compliant classes") void evaluateForWarningReturnsEmptyWhenCompliant() { JavaClasses positive = new ClassFileImporter().importPackages(POSITIVE); assertThat(NoBlanketCatchArchTest.evaluateForWarning(positive)).isEmpty(); } @Test @DisplayName("evaluateForWarning: returns non-empty report for violating classes") void evaluateForWarningReturnsReportWhenViolating() { JavaClasses negative = new ClassFileImporter().importPackages(NEGATIVE); assertThat(NoBlanketCatchArchTest.evaluateForWarning(negative)).isNotEmpty(); } ``` This exercises the wrapper branch that consuming services actually invoke. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 2 • 3 findings (3m) • 2026-05-30T17:29:15.614Z → 2026-05-30T17:31:32.662Z • posted-as: pr-reviewer-bot • model: auto</sub> </details>
fix(archunit-rules): address NoBlanketCatch #15 R2 findings (3 minor)
All checks were successful
im2be-platform-libs CI / mvn install (pull_request) Successful in 1m15s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
70630c1cc1
R2 verdict CONDITIONAL_APPROVE; 3 minor findings (kept=3, dropped=0):

(1) MINOR NoBlanketCatch.java:100 — BLOCK-mode Javadoc snippet omitted
    importOptions=ImportOption.DoNotIncludeTests.class, diverging from the
    canonical template; a developer copying the rule-class snippet would scan
    test fixtures (which legitimately carry blanket catches) → false positives.
    Added importOptions to the snippet + a sentence explaining why it is required.
(2) MINOR NoBlanketCatchArchTest.java:32 — the BLOCK-mode template class in the
    Javadoc was named NoBlanketCatchArchTest, identical to this module's helper;
    a consumer adding that class in the same package before removing their
    WARN-mode test would shadow the library import and break evaluateForWarning()
    callsites. Renamed the snippet class to NoBlanketCatchGateTest in both
    snippets (rule class + helper) + added a note on the naming requirement.
(3) MINOR NoBlanketCatchSelfTest — evaluateForWarning() (the consumer-facing
    public API) had an untested branch (the
    result.hasViolation() ? report : "" wrapper). Added
    evaluateForWarningReportsViolationsAndEmptyWhenClean exercising both
    branches: negative package → non-empty report containing "catches Exception";
    positive package → empty string.

Ripple (rule 63): findings (1)+(2) are documentation-only; (3) is additive test
coverage. No production logic changed, no consumer wiring exists yet → no
downstream ripple.

Verification:
- mvn -pl archunit-rules test → Tests run: 7, Failures: 0, Errors: 0 (BUILD SUCCESS)
- NoBlanketCatchSelfTest 4 → 5 tests
- zero [WARNING]/[ERROR] lines (parent failOnWarning enforced)

hib-pr-reviewer review — PR #15 (affinity-intelligence-rework/im2be-platform-libs)

Round 3 — head 70630c1cc14c, base main, trigger synchronize

TL;DR: NO_NEW_FINDINGS — No new findings this round.

Summary

[quorum-converged] A=0 = B=0. Round 3 resolution: all 8 prior-round findings are closed.

# Prior finding Status
R1-1 catch (Throwable) negative fixture absent Fixed — BlanketThrowableCatchService added; flagsBlanketThrowableCatch pins and asserts it
R1-2 GENERIC_THROWABLES omitted RuntimeException Fixed — all three FQNs present in Set.of(…) at line 74
R1-3 areNotAnnotatedWith was direct-only Fixed — IS_BOUNDARY now uses both isAnnotatedWith and isMetaAnnotatedWith (lines 87-90)
R1-4 No @ControllerAdvice positive fixture Fixed — ControllerAdviceBoundary added; pinned at NoBlanketCatchSelfTest.java:52-54
R1-5 Block-mode Javadoc named NoBlanketCatchArchTest (shadowing) Fixed — template now uses NoBlanketCatchGateTest; explicit warning added in Javadoc
R2-6 Block-mode snippet omitted importOptions = DoNotIncludeTests Fixed — NoBlanketCatch.java lines 100-101 now show it in both snippets
R2-7 Shadowing risk reiterated (same as R1-5) Fixed — same NoBlanketCatchGateTest rename + the Javadoc paragraph in NoBlanketCatchArchTest (lines 38-42)
R2-8 evaluateForWarning public API never exercised Fixed — evaluateForWarningReportsViolationsAndEmptyWhenClean (lines 94-110) exercises both the violation and compliant branches

All three negative fixtures (BlanketCatchService, BlanketThrowableCatchService, BlanketRuntimeCatchService) are pinned and independently asserted. All four positive fixtures (SpecificCatchService, BoundaryAdvice, ControllerAdviceBoundary, ComposedBoundaryAdvice) are pinned. The GlobalExceptionHandler composed annotation is correctly defined with @RestControllerAdvice as meta-annotation (line 19 of GlobalExceptionHandler.java). No new actionable issues found.

CI status (head 70630c1cc14c)

Overall: ✓ success

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings

No new findings this round.

Quorum converged on empty findings (A + B both returned 0).

Verdict

NO_NEW_FINDINGS


hib-pr-reviewer • round 3 • 0 findings • 2026-05-30T17:33:35.675Z → 2026-05-30T17:37:23.858Z • posted-as: pr-reviewer-bot • model: auto • [bookkeeping fallback]

## hib-pr-reviewer review — PR #15 (affinity-intelligence-rework/im2be-platform-libs) **Round 3** — head `70630c1cc14c`, base `main`, trigger `synchronize` **TL;DR:** NO_NEW_FINDINGS — No new findings this round. ### Summary [quorum-converged] A=0 = B=0. **Round 3 resolution: all 8 prior-round findings are closed.** | # | Prior finding | Status | |---|---|---| | R1-1 | `catch (Throwable)` negative fixture absent | ✅ Fixed — `BlanketThrowableCatchService` added; `flagsBlanketThrowableCatch` pins and asserts it | | R1-2 | `GENERIC_THROWABLES` omitted `RuntimeException` | ✅ Fixed — all three FQNs present in `Set.of(…)` at line 74 | | R1-3 | `areNotAnnotatedWith` was direct-only | ✅ Fixed — `IS_BOUNDARY` now uses both `isAnnotatedWith` and `isMetaAnnotatedWith` (lines 87-90) | | R1-4 | No `@ControllerAdvice` positive fixture | ✅ Fixed — `ControllerAdviceBoundary` added; pinned at `NoBlanketCatchSelfTest.java:52-54` | | R1-5 | Block-mode Javadoc named `NoBlanketCatchArchTest` (shadowing) | ✅ Fixed — template now uses `NoBlanketCatchGateTest`; explicit warning added in Javadoc | | R2-6 | Block-mode snippet omitted `importOptions = DoNotIncludeTests` | ✅ Fixed — `NoBlanketCatch.java` lines 100-101 now show it in both snippets | | R2-7 | Shadowing risk reiterated (same as R1-5) | ✅ Fixed — same `NoBlanketCatchGateTest` rename + the Javadoc paragraph in `NoBlanketCatchArchTest` (lines 38-42) | | R2-8 | `evaluateForWarning` public API never exercised | ✅ Fixed — `evaluateForWarningReportsViolationsAndEmptyWhenClean` (lines 94-110) exercises both the violation and compliant branches | All three negative fixtures (`BlanketCatchService`, `BlanketThrowableCatchService`, `BlanketRuntimeCatchService`) are pinned and independently asserted. All four positive fixtures (`SpecificCatchService`, `BoundaryAdvice`, `ControllerAdviceBoundary`, `ComposedBoundaryAdvice`) are pinned. The `GlobalExceptionHandler` composed annotation is correctly defined with `@RestControllerAdvice` as meta-annotation (line 19 of `GlobalExceptionHandler.java`). No new actionable issues found. ### CI status (head `70630c1cc14c`) **Overall: ✓ success** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/94/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/94/jobs/1) | ### Findings **No new findings this round.** _Quorum converged on empty findings (A + B both returned 0)._ ### Verdict **NO_NEW_FINDINGS** --- <sub>hib-pr-reviewer • round 3 • 0 findings • 2026-05-30T17:33:35.675Z → 2026-05-30T17:37:23.858Z • posted-as: pr-reviewer-bot • model: auto • [bookkeeping fallback]</sub>
hibryda deleted branch feat/archunit-no-blanket-catch 2026-05-30 19:38:07 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
affinity-intelligence-rework/im2be-platform-libs!15
No description provided.