Skip to content

test(architecture): the rules assert against packages that exist - #1158

Merged
ExtraToast merged 1 commit into
mainfrom
fix/architecture-rules-that-fire
Sep 5, 2026
Merged

test(architecture): the rules assert against packages that exist#1158
ExtraToast merged 1 commit into
mainfrom
fix/architecture-rules-that-fire

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #1157.

Why

ArchitecturePackages declared INFRASTRUCTURE, SECURITY and PERMISSION against
net.blueshell.api.infrastructure, a package architecture ADR-003 removed in the flattening.
ArchUnit does not complain about a glob that matches nothing, so four rules were selecting an empty
set and passing. The most load-bearing of them is the one ADR-007 names as its enforcement: a
*Permission written into the central permission package today would have passed.

Separately, nothing looked at a third-party import at all. Modulith stops at the
net.blueshell.api boundary, so cohort/domain/BrevoTargetStrategy injecting Brevo's generated
ContactsApi — which API ADR-019 forbids in prose — compiled and verified cleanly.

What this achieves

A *Permission in a central package fails the build, and so does a vendor client imported outside
the module that wraps it. Both were rules on paper only.

How

SECURITY and PERMISSION now name security and security.permission, where ADR-003 puts
the auth filter chain and the two evaluators that stay central. That makes both their rules live:
configuration does not depend on web controllers now actually covers the security config, and the
ADR-007 rule now sees the package it is guarding.

INFRASTRUCTURE is deleted rather than repointed. There is no successor package — ADR-003
distributes cross-cutting concerns into security, shared and the application root, so any
repointing would have been a guess dressed as a fix. Its two dependents are answered directly:

  • no field injection in application code listed four layer packages, two of which
    (APPLICATION, INFRASTRUCTURE) no longer exist. It now selects net.blueshell.api...
    Constructor injection is wanted everywhere, so the layer list was never doing anything the
    broader selector does not do better. Still passes: the @Autowired occurrences in main are all
    on constructors.
  • only the web layer reaches a domain module's web package is deleted as redundant. Both
    sides of it were dead — it also selected net.blueshell.api.domain..web.., another grouping level
    the flattening removed — and CrossModuleWebAccessArchitectureTest states the stronger version
    against today's packages: no module reaches another module's web package, whatever layer the
    reach comes from. A note in the class header records the retirement, next to the one already
    there for domain model and domain services must not depend on outer layers.

VendorClientArchitectureTest is new. VENDOR_OWNERS maps each vendor under
net.blueshell.clients to the module that wraps it (brevo -> contact, discord -> sync); any
other module reaching those types is a violation, and so is a vendor with no declared owner, which
makes adding a client a decision someone has to write down. It follows
CrossModuleWebAccessArchitectureTest's shape exactly: a PINNED set, plus a second test that
fails when a pinned reach disappears so the ratchet cannot slip back.

The commented-out cycle rule is gone. Modulith verification covers cycles.

Not in scope

BrevoTargetStrategy is pinned, not fixed — six reaches from one class, the only violation in the
codebase. It is the double-port problem in cohort: the class pushes list membership through
contact's ContactListAdapter but drives Brevo's ContactsApi directly for the list catalog and
folder names, so one integration sits behind two ports and one of them is raw. Removing it means
publishing the catalog side — list enumeration, folder names, the paging that goes with them —
through contact :: api so cohort speaks only to the wrapper, which is a contact API design
change rather than an import edit. Worth its own issue.

The stale-constant problem is wider than the three this issue names: APPLICATION, DOMAIN_WEB,
DOMAIN_APPLICATION, LISTENER, EVENT, FACTORY, QUERY and most of the
PLATFORM_INTEGRATION family also name packages the flattening removed, so more rules than these
are quietly vacuous. Auditing all of them is a bigger sweep than this issue asked for.

Worth a reviewer's attention

Deleting a rule rather than repointing it is the arguable call, and it is made twice — once for the
INFRASTRUCTURE constant and once for the domain-web rule. The test is whether anything is lost:
for the domain-web rule nothing is, because the sibling test is strictly stronger; for the constant
nothing is, because the field-injection rule got wider rather than narrower.

vendorOf keys on the first segment under net.blueshell.clients, so a client published under a
deeper or differently shaped package would read as an unknown vendor and fail loudly. That is the
intended failure mode, but it is a convention the map depends on.

Verification

  • ./gradlew :services:api:test --tests '*ArchitectureTest*' --tests '*ArchTest*' — exit 0, 71
    passed.

  • ./gradlew :services:api:test — exit 0, 916 passed.

  • The ADR-007 rule was watched failing. A ProbePermission class was placed in
    net.blueshell.api.security.permission, and the rule reported:

    Rule 'Domain permission evaluators must not sit in the shared permission package' was violated (1 times):
    Class <net.blueshell.api.security.permission.ProbePermission> does not reside outside of
    package 'net.blueshell.api.security.permission..' in (ProbePermission.kt:0)
    

    DecoratorsArchitectureTest's "evaluators must be @Component" rule caught it in the same run,
    which is a second rule confirmed live. The probe class was removed; the run above is with it gone.

  • The vendor rule was likewise watched failing before PINNED was filled, reporting the three
    reaches BrevoTargetStrategy makes through return types rather than imports — GetFolder,
    GetFolders200Response, GetLists200Response — which is why the pinned set is six entries and
    not the three the issue's git grep shows.


Diff breakdown added removed, scaled to the largest row.

api                                               +127    -45    4
  unit tests         ███████████████████░░░░░░░   +127    -45    4

──────────────────────────────────────────────────────────────────
production                                          +0     -0
tests                                             +127    -45
total (hand-written)                              +127    -45  4 files

Three constants named `net.blueshell.api.infrastructure`, removed by the
architecture ADR-003 flattening, so every rule built on them matched nothing.
SECURITY and PERMISSION now name `security` and `security.permission`, which is
where ADR-003 puts them; INFRASTRUCTURE has no successor package and its two
dependents are answered without it.

Adds the missing vendor-import rule: only the module that wraps a generated
client under `net.blueshell.clients` may import it. The one existing reach,
cohort's BrevoTargetStrategy, is pinned in a decaying allowlist.
@ExtraToast
ExtraToast merged commit 6dbe992 into main Sep 5, 2026
25 checks passed
@ExtraToast
ExtraToast deleted the fix/architecture-rules-that-fire branch September 5, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Three architecture rules check a package that no longer exists

1 participant