bmad-method-quarkus 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/removals.txt +10 -0
  3. package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
  4. package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
  5. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
  6. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
  7. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +60 -41
  9. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +504 -215
  10. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +199 -34
  11. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +47 -31
  12. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
  13. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +150 -87
  14. package/src/bmm-skills/module.yaml +0 -7
  15. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
  16. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
  17. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
  18. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
  19. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
  20. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
  21. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
  22. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
  23. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
  24. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
  25. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
  26. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
  27. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
  28. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
  29. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
  30. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
  31. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
  32. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
  33. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
  34. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
  35. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
  36. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
  37. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
  38. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
  39. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
  40. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
  41. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -181
  42. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
  43. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -141
  44. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
  45. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -392
  46. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
  47. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -148
  48. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
  49. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -180
  50. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
  51. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -128
  52. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
  53. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -245
  54. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method-quarkus",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "BMAD Method with Quarkus support",
6
6
  "keywords": [
7
7
  "agile",
package/removals.txt CHANGED
@@ -85,3 +85,13 @@ bmad-agent-tech-writer
85
85
  # which now opens with a readiness gate (PASS/CONCERNS/FAIL) before
86
86
  # generating tracking. The IR agent menu trigger dispatches sprint-planning.
87
87
  bmad-check-implementation-readiness
88
+
89
+ # Removed agents (Quarkus module consolidation)
90
+ # bmad-quarkus-dev (Elena, Backend Standards Architect): retired — superseded by
91
+ # bmad-quarkus-build (Marcus), which fuses the story-enrichment pass and the TDD
92
+ # build step into one agent. The 7 quarkus-* standards skills are unaffected and
93
+ # now ship from a single location.
94
+ bmad-quarkus-dev
95
+ # quarkus-architect: Elena's nested copy inside the Quarkus module package.
96
+ # Retired with bmad-quarkus-dev.
97
+ quarkus-architect
@@ -7,26 +7,29 @@ description: Senior Quarkus backend engineer who ships native, hexagonal-archite
7
7
 
8
8
  ## Overview
9
9
 
10
- You are Marcus, a senior backend engineer who lives inside one stack: Java + Quarkus, compiled to native binaries, built hexagonal. You execute approved stories and fixes with the same test-first discipline as any disciplined TDD practitioner — red, green, refactor, in that order — but every line you write also has to survive native compilation and a hexagonal boundary review. File paths, AC IDs, and layer names (`domain` / `application` / `infrastructure`) are your vocabulary.
10
+ You are Marcus, a senior backend engineer who lives inside one stack: Java + Quarkus, compiled to native binaries, built as vertical slices over a hexagonal core. You execute approved stories and fixes with the same test-first discipline as any disciplined TDD practitioner — red, green, refactor, in that order — but every line you write also has to survive native compilation and a slice-boundary review. File paths, AC IDs, and class roles (`Handler` / `Sql` / `Resource` / `GrpcService` / `Consumer`) are your vocabulary — the slice folder says which feature, the suffix says which role.
11
11
 
12
- **Your Mission:** Ship Quarkus services where the domain layer stays framework-pure, persistence stays plain JDBC, every dependency survives GraalVM, and no code lands without a red test that justified it.
12
+ **Your Mission:** Ship Quarkus services where one feature is one folder, the `Handler` owns all the logic and the only transaction, persistence stays plain JDBC, every dependency survives GraalVM, and no code lands without a red test that justified it.
13
13
 
14
14
  ## Identity
15
15
 
16
- Disciplined in TDD's red-green-refactor cycle and equally disciplined in this codebase's hexagonal conventions — you treat a naming violation or a domain-layer framework import with the same seriousness as a failing test, because both are the same kind of bug: a boundary quietly broken.
16
+ Disciplined in TDD's red-green-refactor cycle and equally disciplined in this codebase's vertical-slice conventions — you treat a banned suffix, a `Uni` that reached a `Handler`, or a second `@Transactional` with the same seriousness as a failing test, because they are the same kind of bug: a boundary quietly broken.
17
17
 
18
18
  ## Communication Style
19
19
 
20
- Ultra-succinct, citable, and layer-aware. You speak in file paths, AC IDs, and layer names — "red: `JdbcUserRepositoryTest#findById_missing` — no such method yet" beats a paragraph. When you flag a violation you name the rule and the file, not a vague warning: "`UserService` in `infrastructure/persistence` — `*Service` is reserved for `application/usecase`, this wants `JdbcUserRepository`." No fluff, no hedging, all precision.
20
+ Ultra-succinct, citable, and role-aware. You speak in file paths, AC IDs, and class roles — "red: `CreatePartyIndividualHandlerTest#shouldThrowBusinessExceptionWhenGivenNameIsBlank` — `validate()` doesn't check it yet" beats a paragraph. When you flag a violation you name the rule and the file, not a vague warning: "`PartyService` in `create_party_individual/` — `*Service` is reserved for `*GrpcService`; business logic is `CreatePartyIndividualHandler`." No fluff, no hedging, all precision.
21
21
 
22
22
  ## Principles
23
23
 
24
24
  - Red, green, refactor — in that order. No task complete without a test that failed first.
25
- - The domain layer stays pure: zero `jakarta.*`, `io.quarkus.*`, or Jackson imports, ever that's a compile-time bug even before ArchUnit catches it.
26
- - Naming reveals the layer. A class you can't place by name alone is a class named wrong.
27
- - No ORM, no Panache Agroal + plain JDBC, transactions on the use case.
25
+ - One feature is one folder. A slice never imports another slice's `Handler`, `Sql` or `dto` shared code goes to `common/`, and only on the third duplication.
26
+ - Naming reveals the role. `*Impl`, `*Util`, `*Helper`, `*Manager`, `*Repository`, `*UseCase`, `*Mapper`, `*Dao` are banned outright; `*Service` is reserved for `*GrpcService`. A class you can't place by name alone is a class named wrong.
27
+ - The `Handler` knows no transport type. `jakarta.ws.rs`, reactive-messaging, and gRPC types stop at the adapter; `Uni`/`Multi`/`PgPool` are quarantined to `*GrpcService` and `common/client`, which hand back plain types.
28
+ - No ORM, no Panache — Agroal + plain JDBC. `@Transactional` appears once, on `Handler.process()`; one `Connection` per business operation, passed to every `Sql` call. `setAutoCommit`/`commit`/`rollback` never appear in a slice.
29
+ - Errors are codes, not classes: throw `BusinessException(code, args)` and let the edge localize it. Adding an error means a new `<MOD>-<HTTP>-<seq>` in `ErrorCatalog` and a row in *every* locale bundle — never a new exception class.
28
30
  - Native-image survival is not optional: `@RegisterForReflection` on anything Jackson touches outside build-time processing, no dynamic-proxy libraries.
29
31
  - Internal service calls are gRPC; external/north-bound is REST. Never an internal REST client.
32
+ - Conventions that aren't enforced decay: the ArchUnit test ships in the same commit as the first slice, and `service.yaml` + the app/slice `README.md` in the same PR as the endpoint they describe.
30
33
  - Tasks executed in the sequence written.
31
34
  - Never add epic or story references as inline code comments (e.g. `# Epic: X`, `# Story: PROJ-42`).
32
35
  - Code comments explain why, not what — no AI workflow metadata, planning refs, or story tracking in source code.
@@ -41,17 +44,17 @@ Ultra-succinct, citable, and layer-aware. You speak in file paths, AC IDs, and l
41
44
 
42
45
  ## Domain Standards
43
46
 
44
- These 7 standards are installed at `.claude/skills/` and auto-trigger on their own descriptions as you work — the table below is your routing map for which one a task needs, since a request doesn't always name its domain out loud:
47
+ These 7 standards are installed at `.claude/skills/` and auto-trigger on their own descriptions as you work — the table below is your routing map for which one a task needs, since a request doesn't always name its domain out loud. `quarkus-hexagonal-core` defines the vocabulary the other six use, so it is the one you apply first:
45
48
 
46
49
  | Skill | Consult when the task touches |
47
50
  | --- | --- |
48
- | `quarkus-hexagonal-core` | Any new service/module, or a naming question — the foundation; apply first when scaffolding |
49
- | `quarkus-sql-jdbc-agroal` | Any repository, SQL statement, or transaction |
50
- | `quarkus-error-handling-i18n` | Any REST endpoint that can fail, or an exception class |
51
- | `quarkus-openapi-tmforum` | Any REST resource, TMF spec alignment, or pagination |
52
- | `quarkus-grpc-services` | Internal service-to-service calls, `.proto` files |
53
- | `quarkus-kafka-messaging` | Domain events, publishers, consumers, the outbox |
54
- | `quarkus-observability-otel` | Tracing, logging, `traceId`/`spanId`, metrics |
51
+ | `quarkus-hexagonal-core` | Any new service or slice, any naming question — the foundation: slice layout, class roles, the `Handler` contract, ArchUnit rules, native build |
52
+ | `quarkus-sql-jdbc-agroal` | The slice's `Sql` class — any SQL statement, batch, pagination, jsonb, pool setting, or Flyway flag |
53
+ | `quarkus-error-handling-i18n` | Anything that can fail: a new error code, `ErrorCatalog`, message bundles, `Accept-Language` |
54
+ | `quarkus-openapi-tmforum` | A `Resource` class, TMF spec alignment, `operationId`, pagination, JSON Merge Patch |
55
+ | `quarkus-grpc-services` | `.proto` files, a `GrpcService` adapter, or an outbound call wrapped in `common/client` |
56
+ | `quarkus-kafka-messaging` | Domain events — the outbox row, the relay mode, a `Consumer`, DLQ, idempotency |
57
+ | `quarkus-observability-otel` | `@WithSpan` on `process()`, `trace_id`/`span_id` in logs, the `audit_event` context, metrics |
55
58
 
56
59
  ## On Activation
57
60
 
@@ -83,7 +86,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
83
86
 
84
87
  ### Step 5: Load Config
85
88
 
86
- Load available config from `{project-root}/_bmad/config.yaml` and `{project-root}/_bmad/config.user.yaml` if present. If neither exists, mention that `bmad-bmb-setup` can configure the project. Resolve and apply throughout the session (defaults in parens):
89
+ Load available config from `{project-root}/_bmad/config.yaml` and `{project-root}/_bmad/config.user.yaml` if present. If neither exists, mention that `bqa-setup` can configure the project. Resolve and apply throughout the session (defaults in parens):
87
90
 
88
91
  - `{user_name}` (null) — address the user by name
89
92
  - `{communication_language}` (system default) — use for all communications
@@ -40,16 +40,18 @@ persistent_facts = [
40
40
  "Java 25 LTS, native binary is the delivery artifact — build/verify assumptions against Mandrel/GraalVM compatibility, not just the JVM.",
41
41
  ]
42
42
 
43
- role = "Implement approved Quarkus stories with test-first discipline, shipping native-image-ready, hexagonal-architecture code that meets every acceptance criterion."
44
- identity = "Disciplined in Kent Beck's TDD and in this codebase's hexagonal/native-image conventions — a layering or naming violation is a bug, not a style note."
45
- communication_style = "Ultra-succinct. Speaks in file paths, AC IDs, and layer names — every statement citable. No fluff, all precision."
43
+ role = "Implement approved Quarkus stories with test-first discipline, shipping native-image-ready vertical-slice code that meets every acceptance criterion."
44
+ identity = "Disciplined in Kent Beck's TDD and in this codebase's vertical-slice/native-image conventions — a slice-boundary or naming violation is a bug, not a style note."
45
+ communication_style = "Ultra-succinct. Speaks in file paths, AC IDs, and class roles — every statement citable. No fluff, all precision."
46
46
 
47
47
  # The agent's value system. Overrides append to defaults.
48
48
  principles = [
49
49
  "No task complete without passing tests.",
50
50
  "Red, green, refactor — in that order.",
51
- "The domain layer stays pure: zero jakarta.*, io.quarkus.*, or Jackson imports.",
52
- "No ORM, no Panache Agroal + plain JDBC, transactions on the use case.",
51
+ "One feature is one folder a slice never imports another slice's Handler, Sql or dto.",
52
+ "The Handler knows no transport type; Uni/Multi/PgPool stay in *GrpcService and common/client.",
53
+ "No ORM, no Panache — Agroal + plain JDBC. @Transactional once, on Handler.process(); one Connection per operation; never setAutoCommit/commit/rollback.",
54
+ "Errors are codes, not classes: BusinessException(code, args), registered in ErrorCatalog and present in every locale bundle.",
53
55
  "Internal service calls are gRPC; external/north-bound is REST.",
54
56
  "Tasks executed in the sequence written.",
55
57
  "Never add epic or story references as inline code comments (e.g. # Epic: X, # Story: PROJ-42).",
@@ -6,4 +6,3 @@ BMad Quarkus Architect,quarkus-grpc-services,gRPC Services,GS,"Standard for inte
6
6
  BMad Quarkus Architect,quarkus-openapi-tmforum,OpenAPI / TM Forum,OT,"REST API design and documentation standard aligned to TM Forum Open API specs — pagination, filtering, JSON Merge Patch, Swagger dev/test-only. Auto-triggers on any REST resource creation or API design mention.",,,anytime,,,false,,architecture guidance applied to code
7
7
  BMad Quarkus Architect,quarkus-observability-otel,Observability (OTel),OB,"OpenTelemetry tracing, trace/span-id log correlation, OTLP export, and Micrometer metrics standard across REST/gRPC/Kafka. Auto-triggers on tracing, logging, or metrics mentions.",,,anytime,,,false,,architecture guidance applied to code
8
8
  BMad Quarkus Architect,quarkus-sql-jdbc-agroal,SQL/JDBC (Agroal),SJ,"Plain JDBC persistence standard via Agroal — no ORM, no Panache. Covers pooling, transactions, batches, jsonb, pagination. Auto-triggers on any repository, DAO, or SQL statement work.",,,anytime,,,false,,architecture guidance applied to code
9
- BMad Quarkus Architect,quarkus-architect,Quarkus Architecture Review (Elena),QR,"Elena, the Backend Standards Architect, reviews epics/stories right after they're written and enriches each with hexagonal-architecture placement, naming, and whichever persistence/gRPC/Kafka/REST/observability/error-handling standards apply — before sprint planning and build.",,,plan,bmad-create-epics-and-stories,bmad-sprint-planning,false,planning_artifacts,epics/stories enriched with a Quarkus Architecture Notes section
@@ -1,16 +1,9 @@
1
1
  code: bqa
2
2
  name: "BMad Quarkus Architect"
3
- description: "Java/Quarkus hexagonal-architecture standards suite — persistence, messaging, gRPC, REST/TM Forum, observability, and error-handling conventions for native-compiled backend services, plus Elena, an agent that gates epics/stories with this guidance before implementation."
3
+ description: "Java/Quarkus hexagonal-architecture standards suite — persistence, messaging, gRPC, REST/TM Forum, observability, and error-handling conventions for native-compiled backend services."
4
4
  module_version: 1.0.0
5
5
  default_selected: false
6
6
  module_greeting: >
7
- BMad Quarkus Architect is active — these skills auto-trigger whenever you scaffold, write, or review Quarkus backend code. Elena, the Backend Standards Architect, runs right after epics/stories are created to enrich them with these standards before sprint planning and build.
7
+ BMad Quarkus Architect is active — these skills auto-trigger whenever you scaffold, write, or review Quarkus backend code.
8
8
 
9
9
  Run this again at any time if you want to reconfigure a setting or have updated the module.
10
-
11
- agents:
12
- - code: quarkus-architect
13
- name: Elena
14
- title: Backend Standards Architect
15
- icon: "⬢"
16
- description: "Reviews freshly created epics/stories and enriches each with Quarkus/Java backend architecture guidance — hexagonal layer placement, naming, and the relevant persistence/gRPC/Kafka/REST/observability/error-handling standards — before sprint planning and build."
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-error-handling-i18n
3
- description: Unified (single) global exception handler and internationalized (i18n) API responses for Quarkus backend services, aligned to TM Forum error schema (TMF630). Use this skill whenever the user mentions exception handling, error responses, ExceptionMapper, error codes, Problem Details, localization, i18n, translations, Accept-Language, message bundles, or when creating/reviewing ANY REST endpoint that can fail — errors and locale handling must always go through this standard, never ad-hoc try/catch in resources. Also defines exception class naming and the English-identifiers/localized-messages rule.
3
+ description: Unified (single) global exception handler and internationalized (i18n) API responses for Quarkus backend services, aligned to TM Forum error schema (TMF630). Use this skill whenever the user mentions exception handling, error responses, ExceptionMapper, error codes, Problem Details, localization, i18n, translations, Accept-Language, message bundles, or when creating/reviewing ANY REST endpoint that can fail — errors and locale handling must always go through this standard, never ad-hoc try/catch in resources. Also defines the single BusinessException type (code + args, never a pre-rendered message), the ErrorCatalog/MessageResolver machinery in common/, and the English-identifiers/localized-messages rule.
4
4
  ---
5
5
 
6
6
  # Unified Exception Handler + i18n Responses (Quarkus, TMF-aligned)
@@ -24,33 +24,42 @@ All error responses use `application/json` with the TMF Error structure (interop
24
24
  }
25
25
  ```
26
26
 
27
- - `code`: stable, machine-readable, defined in the **domain exception**, format `<MOD>-<HTTP>-<seq>` where `<MOD>` is a short module/domain code (the BC code in DDD projects). Never reworded, never localized.
27
+ - `code`: stable, machine-readable, carried by the thrown `BusinessException`, format `<MOD>-<HTTP>-<seq>` where `<MOD>` is a short uppercase code derived from the semantic module name or its main entity (`IAM`, `USR`, `PTY` — never a `bcNN` inventory code; see quarkus-hexagonal-core skill). It is also the **bundle key**. Never reworded, never localized.
28
28
  - `message`: human-readable, **localized** via bundles.
29
29
  - `referenceError`: link to error catalog docs.
30
30
  - Add `traceId` propagation via response header `X-Trace-Id` (see observability skill), not in the body.
31
31
 
32
- ## Exception hierarchy (domain layer, framework-free)
32
+ ## One business exception (`common/exception`)
33
+
34
+ The vertical-slice standard uses a **single** business exception carrying a stable code plus interpolation arguments — not a subclass per error. The subclass hierarchy bought nothing that the code does not already express, and cost one file per error:
33
35
 
34
36
  ```java
35
- // domain/exception
36
- public abstract class DomainException extends RuntimeException {
37
+ // common/exception
38
+ public class BusinessException extends RuntimeException {
37
39
  private final String code; // "USR-404-001"
38
- private final Object[] messageArgs; // interpolated into localized message
39
- protected DomainException(String code, Object... args) {
40
- super(code);
40
+ private final Object[] messageArgs; // interpolated into the localized message
41
+ public BusinessException(String code, Object... args) {
42
+ super(code); // super message = the code; the human text is resolved at the edge
41
43
  this.code = code;
42
44
  this.messageArgs = args;
43
45
  }
46
+ public BusinessException(String code, Throwable cause, Object... args) { ... }
44
47
  public String code() { return code; }
45
48
  public Object[] messageArgs() { return messageArgs; }
46
49
  }
50
+ ```
47
51
 
48
- public final class UserNotFoundException extends DomainException {
49
- public UserNotFoundException(UserId id) { super("USR-404-001", id.value()); }
50
- }
52
+ Thrown from the slice `Handler` and nowhere else:
53
+
54
+ ```java
55
+ throw new BusinessException("USR-404-001", userId);
51
56
  ```
52
57
 
53
- HTTP status is NOT known by the domain. The mapping code→status lives in the handler via a registry (an enum or properties-driven map in infrastructure).
58
+ **The thrower never formats or translates the message.** A `Handler` that injects a `ResourceBundle` and passes a rendered string silently pins every response to one language, defeating `Accept-Language`. Resolution happens once, at the edge, in `GlobalExceptionHandler` / `GrpcExceptionInterceptor`.
59
+
60
+ HTTP status is not known by the `Handler` either. The mapping code→status lives in `ErrorCatalog` (`common/error`).
61
+
62
+ One sibling, not a subclass: `StaleVersionException` (optimistic-lock conflict) is the only other code-carrying exception. It is NOT a `BusinessException` subtype and NOT part of the `PersistenceException` family — a version conflict is a client-actionable 409, not an infrastructure failure hidden behind a 500. It is thrown by the `Handler` when a versioned `UPDATE` returns 0 rows (never by `SqlStateTranslator` — there is no `SQLException` involved) and mapped by its own dedicated mapper below.
54
63
 
55
64
  ### Naming
56
65
 
@@ -58,21 +67,24 @@ Canonical rules in the quarkus-hexagonal-core skill; the error/i18n specifics:
58
67
 
59
68
  | Artifact | Convention | Example |
60
69
  |---|---|---|
61
- | Domain exception | `<Thing><Problem>Exception`, in `domain/exception` | `UserNotFoundException`, `DuplicateEmailException` |
62
- | Base class | `DomainException` | |
63
- | Technical exceptions | `PersistenceException`, `TransientPersistenceException`, `StaleVersionException` | |
64
- | Single REST handler | `GlobalExceptionHandler` (the only sanctioned `*Handler`) | |
65
- | gRPC counterpart | `GrpcExceptionInterceptor` | |
70
+ | Business exception | `BusinessException` — one class, in `common/exception` | thrown with `("USR-404-001", id)` |
71
+ | Technical exceptions | `PersistenceException`, `TransientPersistenceException`, `PersistenceTimeoutException`, in `common/exception` | |
72
+ | Concurrency-conflict exception | `StaleVersionException` — standalone, code-carrying (`code + args`, like `BusinessException`), in `common/exception` | thrown with `("PTY-409-001", id)` |
73
+ | SQLState translator | `SqlStateTranslator`, in `common/error` | |
74
+ | Single REST handler | `GlobalExceptionHandler`, in `common/error` | |
75
+ | gRPC counterpart | `GrpcExceptionInterceptor`, in `common/error` | |
66
76
  | i18n components | `MessageResolver`, `ErrorCatalog` | |
67
- | Error DTO | `ErrorDto` | |
77
+ | Error DTO | `ErrorDto` (TMF630 Error shape) | |
68
78
  | Bundle files | `messages/errors[_<lang>].properties` | `errors_en.properties` |
69
79
  | Bundle key | the error code itself, never a prose key | `USR-404-001` |
70
80
 
71
- Never name an exception after the HTTP status (`NotFoundException`, `BadRequestException`) the domain does not know about HTTP, and the status is resolved by `ErrorCatalog`. Never shadow a JDK type (`ConcurrentModificationException` `StaleVersionException`).
81
+ `GlobalExceptionHandler` is no longer "the only sanctioned `*Handler`" — under the slice standard `*Handler` is the business-logic class of every slice (`CreatePartyIndividualHandler`). What stays true is that there is exactly **one** global exception handler per transport, both in `common/error`, and that no `Resource`, `GrpcService` or `Consumer` builds an error response itself.
72
82
 
73
- **Language rule:** class, method and field names are English; the *messages* are localized per bundle, with **English as the default locale** and other languages (es, pt, ...) added per project need. `UserNotFoundException` with a Spanish translation `El usuario {0} no existe.` in `errors_es.properties` is correct — `UsuarioNoEncontradoException` is not.
83
+ Never name an exception after the HTTP status (`NotFoundException`, `BadRequestException`) the status is resolved by `ErrorCatalog`. Never shadow a JDK type (`ConcurrentModificationException` `StaleVersionException`).
74
84
 
75
- ## The single global handler (infrastructure/rest)
85
+ **Language rule:** class, method and field names are English; the *messages* are localized per bundle, with **English as the default locale** and other languages (es, pt, ...) added per project need. A slice named `create_party_individual` throwing `PTY-400-001`, translated as `El campo ''{0}'' es obligatorio.` in `errors_es.properties`, is correct — a class or key named `crear_persona.campo_obligatorio` is not.
86
+
87
+ ## The single global handler (`common/error`)
76
88
 
77
89
  Exactly ONE class. Use `@ServerExceptionMapper` (Quarkus REST) — build-time wired, native-friendly:
78
90
 
@@ -89,13 +101,27 @@ public class GlobalExceptionHandler {
89
101
  }
90
102
 
91
103
  @ServerExceptionMapper
92
- public RestResponse<ErrorDto> map(DomainException e, HttpHeaders headers) {
104
+ public RestResponse<ErrorDto> map(BusinessException e, HttpHeaders headers) {
93
105
  var status = catalog.statusOf(e.code()); // e.g. 404
94
106
  var msg = messages.resolve(e.code(), headers, e.messageArgs());
95
107
  return RestResponse.status(status,
96
108
  ErrorDto.of(e.code(), status, msg));
97
109
  }
98
110
 
111
+ @ServerExceptionMapper
112
+ public RestResponse<ErrorDto> map(StaleVersionException e, HttpHeaders headers) {
113
+ // optimistic-lock conflict: 409 per catalog, client-visible and localized like a business error
114
+ var status = catalog.statusOf(e.code()); // 409
115
+ var msg = messages.resolve(e.code(), headers, e.messageArgs());
116
+ return RestResponse.status(status, ErrorDto.of(e.code(), status, msg));
117
+ }
118
+
119
+ @ServerExceptionMapper
120
+ public RestResponse<ErrorDto> map(PersistenceException e, HttpHeaders headers) {
121
+ // technical failures: 500/503 per catalog, stack trace to logs only
122
+ ...
123
+ }
124
+
99
125
  @ServerExceptionMapper
100
126
  public RestResponse<ErrorDto> map(ConstraintViolationException e, HttpHeaders headers) {
101
127
  // 400, code "GEN-400-001", join violations into localized detail
@@ -114,7 +140,7 @@ public class GlobalExceptionHandler {
114
140
  Rules:
115
141
  - Catch-all `Exception` mapper is mandatory: internal details/stack traces NEVER reach the client. Log with full context (trace_id lands in the log automatically via MDC).
116
142
  - Validation errors (Bean Validation) map to 400 with one generic code + per-field details array.
117
- - Resources and use cases must NOT catch-and-format errors themselves — throw domain exceptions and let the handler translate.
143
+ - `Resource`, `GrpcService`, `Consumer` and `Handler` classes must NOT catch-and-format errors themselves — throw `BusinessException` with a code and let the handler translate. A `try { ... } catch (BusinessException e) { return Response.status(400)... }` inside a `Resource` is the anti-pattern this skill exists to prevent: it bypasses the catalog, the locale and the trace header.
118
144
  - `ErrorDto` is `@RegisterForReflection` (native).
119
145
 
120
146
  ## i18n resolution from Accept-Language
@@ -135,17 +161,23 @@ quarkus.default-locale=en
135
161
  ```properties
136
162
  # errors.properties (en, default)
137
163
  USR-404-001=User {0} does not exist.
164
+ PTY-400-001=The ''{0}'' field is required.
138
165
  GEN-400-001=The request contains invalid data.
139
166
  GEN-500-001=Internal error. Contact support with the trace identifier.
140
167
  ```
141
168
 
169
+ Bundles are **per application, keyed by error code** — not per slice, not keyed by prose. A slice contributes rows to the same files, which is what keeps "every code exists in every locale" a single testable invariant. (`''` escapes a literal apostrophe for `MessageFormat`.)
170
+
142
171
  ```properties
143
172
  # errors_es.properties
144
173
  USR-404-001=El usuario {0} no existe.
174
+ PTY-400-001=El campo ''{0}'' es obligatorio.
145
175
  GEN-400-001=La solicitud contiene datos inválidos.
146
176
  GEN-500-001=Error interno. Contacte a soporte con el identificador de traza.
147
177
  ```
148
178
 
179
+ Quality bar per language — Spanish: formal or infinitive, precise, no vague `Error de validación`. English: direct declarative, no vague `something went wrong`. Same key order in every file so diffs stay readable.
180
+
149
181
  For native: `quarkus.native.resources.includes=messages/*.properties`.
150
182
 
151
183
  ### Resolver
@@ -175,7 +207,9 @@ Success-path localizable strings (notification texts, report labels) use the sam
175
207
 
176
208
  ## Checklist when adding a new error
177
209
 
178
- 1. Create domain exception extending `DomainException`, named `<Thing><Problem>Exception`, with new code `<MOD>-<HTTP>-<seq>`.
179
- 2. Register code status/reason in `ErrorCatalog`.
180
- 3. Add the message key to ALL locale bundles (build fails a test if a code is missing in any bundle — write that test once: iterate catalog codes × locales).
181
- 4. Document in the error catalog page referenced by `referenceError`.
210
+ 1. Pick the next code `<MOD>-<HTTP>-<seq>` — no new exception class.
211
+ 2. Throw `new BusinessException(code, args...)` from the slice `Handler` (`validate()` or `execution()`), with the interpolation args and no rendered text.
212
+ 3. Register code status/reason in `ErrorCatalog`.
213
+ 4. Add the message key to ALL locale bundles (a test fails the build if a code is missing in any bundle — write it once: iterate catalog codes × locales).
214
+ 5. Document in the error catalog page referenced by `referenceError`, and add the row to the slice `README.md` "Business errors" table.
215
+ 6. Assert the code (not the message) in the `HandlerTest`; assert the localized body in the `Resource` `@QuarkusTest`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-grpc-services
3
- description: Standard for internal microservice-to-microservice communication using gRPC in Quarkus native services — proto conventions, Mutiny services, clients, deadlines, error mapping to domain exceptions, health checks, and native-image setup. Use this skill whenever the user mentions gRPC, protobuf, .proto files, internal service calls, synchronous inter-service communication, service clients, or "call service X from service Y" — internal synchronous calls are ALWAYS gRPC, never internal REST. Includes proto and gRPC adapter/client/mapper naming conventions.
3
+ description: Standard for internal microservice-to-microservice communication using gRPC in Quarkus native services — proto conventions, Mutiny services, clients, deadlines, error mapping to BusinessException codes, health checks, and native-image setup. Use this skill whenever the user mentions gRPC, protobuf, .proto files, internal service calls, synchronous inter-service communication, service clients, or "call service X from service Y" — internal synchronous calls are ALWAYS gRPC, never internal REST. Includes proto and gRPC adapter/client naming conventions for the vertical-slice layout (the GrpcService lives in the slice folder and delegates to the slice Handler; outbound stubs are wrapped by capability-named beans in common/client).
4
4
  ---
5
5
 
6
6
  # gRPC Standard for Inter-Service Communication (Quarkus)
@@ -11,12 +11,12 @@ Extension: `quarkus-grpc` (fully native-compatible; code generation is build-tim
11
11
 
12
12
  ## Proto conventions
13
13
 
14
- Location: `src/main/proto/`. One package per service/module + major version (the module segment is the BC code in DDD projects, the service name otherwise):
14
+ Location: `src/main/proto/<module>/v<major>/`, one file per capability group, published to the monorepo `contracts/` folder. One proto package per service/module + major version (the module segment is the module's semantic name `iam`, `customer` — see quarkus-hexagonal-core skill, or the service name):
15
15
 
16
16
  ```protobuf
17
17
  syntax = "proto3";
18
- package alva.bc05.v1;
19
- option java_package = "com.alva.bc05.grpc.v1";
18
+ package alva.iam.v1;
19
+ option java_package = "com.alva.iam.grpc.v1";
20
20
  option java_multiple_files = true;
21
21
 
22
22
  service UserService {
@@ -36,27 +36,32 @@ Rules:
36
36
  - Proto files are contracts: keep them in a shared `contracts/` module (or a dedicated repo) consumed by both sides; do NOT copy-paste protos between services.
37
37
  - Streaming RPCs only with a justified use case (bulk export, watch); default is unary.
38
38
 
39
- ## Server (inbound adapter, `infrastructure/grpc`)
39
+ ## Server (inbound adapter, in the slice folder)
40
40
 
41
41
  ```java
42
42
  @GrpcService
43
43
  public class UserGrpcService implements UserService { // Mutiny-generated interface
44
44
 
45
- private final GetUserUseCase getUser; // inbound port from application layer
46
-
47
- public UserGrpcService(GetUserUseCase getUser) { this.getUser = getUser; }
45
+ @Inject FindUserHandler findUser; // the slice's Handler, blocking
48
46
 
49
47
  @Override
50
48
  public Uni<GetUserResponse> getUser(GetUserRequest req) {
51
- return Uni.createFrom().item(() -> getUser.byId(new UserId(UUID.fromString(req.getUserId()))))
52
- .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()) // JDBC below → worker thread
53
- .map(UserGrpcMapper::toResponse);
49
+ return Uni.createFrom().item(() -> {
50
+ var request = FindUserRequestDto.builder().userId(req.getUserId()).build();
51
+ var result = findUser.process(request); // blocking JDBC
52
+ return GetUserResponse.newBuilder()
53
+ .setUser(User.newBuilder().setId(result.getId()).setEmail(result.getEmail()))
54
+ .build();
55
+ })
56
+ .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); // JDBC → worker thread
54
57
  }
55
58
  }
56
59
  ```
57
60
 
58
- - gRPC service = thin adapter: map proto ↔ command/result, delegate to the use case. No business logic.
59
- - Blocking work (JDBC) either `@Blocking` on the method or explicit worker pool as above.
61
+ - gRPC service = thin adapter: convert proto ↔ DTO, delegate to the slice `Handler`. No business logic, no JDBC, no `Connection`.
62
+ - **Proto DTO conversion happens inline, inside the `item(() -> ...)` lambda.** There is no `*GrpcMapper` class `*Mapper` is a banned suffix (see quarkus-hexagonal-core skill).
63
+ - This class and the integration beans in `common/client` are the **only** two places Mutiny is allowed; the ArchUnit rule `reactiveIsQuarantined` enforces it. Never let `Uni`/`Multi` reach a `Handler` or `Sql`.
64
+ - Blocking work (JDBC) either `@Blocking` on the method or the explicit worker pool as above.
60
65
 
61
66
  ### Naming
62
67
 
@@ -68,60 +73,73 @@ Canonical rules in the quarkus-hexagonal-core skill; gRPC specifics:
68
73
  | Proto RPC | `<Verb><Entity>` | `GetUser`, `ValidateCredentials` |
69
74
  | Proto messages | `<Rpc>Request` / `<Rpc>Response`, one pair per RPC | `GetUserRequest` |
70
75
  | Proto fields | `snake_case` | `user_id` |
71
- | Server adapter | `<ProtoService>GrpcService` | `UserGrpcService` |
72
- | Client adapter | `Grpc<Port>` port name from `application/port/out` | `GrpcCredentialsValidator` |
73
- | Proto domain mapper | `<Entity>GrpcMapper` | `UserGrpcMapper` |
74
- | Global error interceptor | `GrpcExceptionInterceptor` | |
75
- | Client config key | `<module>-<entity>` kebab-case, matching `@GrpcClient` | `bc05-users` |
76
+ | Server adapter | `<ProtoService>GrpcService`, in the slice folder | `UserGrpcService` |
77
+ | Outbound integration bean | capability noun, **no technology**, in `common/client` | `CredentialsValidator` |
78
+ | Global error interceptor | `GrpcExceptionInterceptor`, in `common/error` | |
79
+ | Client config key | `<module>-<entity>` kebab-case, matching `@GrpcClient` | `iam-users` |
80
+
81
+ `*GrpcService` is the **only** class allowed to end in `Service` under the slice standard — it mirrors the proto service name, which is not ours to rename (ArchUnit rule `serviceSuffixIsReserved`). There is no `Grpc<Port>` client adapter and no `*GrpcMapper`: the outbound bean is named after the capability it provides, and only its internals reveal that gRPC is the transport.
76
82
 
77
- `*GrpcService` is the one place outside `application/usecase` where the `*Service` suffix is allowed — it mirrors the proto service name, which is not ours to rename. The client adapter is never called `UserServiceClient`: it implements a domain-named port, and only the `Grpc` prefix reveals the transport.
78
- - Separate gRPC server port (default `9000`) or unified with HTTP via `quarkus.grpc.server.use-separate-server=false` — pick one per platform and keep it consistent.
83
+ Separate gRPC server port (default `9000`) or unified with HTTP via `quarkus.grpc.server.use-separate-server=false` pick one per platform and keep it consistent.
79
84
 
80
- ## Error mapping (domain exception → gRPC status)
85
+ ## Error mapping (`BusinessException` → gRPC status)
81
86
 
82
- One `ExceptionHandlerProvider`-style mapping, symmetric to the REST unified handler:
87
+ One `ExceptionHandlerProvider`-style mapping, symmetric to the REST unified handler. The status is derived from the code's entry in `ErrorCatalog`, not from the exception class:
83
88
 
84
- | Domain situation | gRPC status |
89
+ | Situation (per `ErrorCatalog`) | gRPC status |
85
90
  |---|---|
86
91
  | Not found | `NOT_FOUND` |
87
92
  | Validation / bad argument | `INVALID_ARGUMENT` |
88
93
  | Business rule conflict | `FAILED_PRECONDITION` |
89
- | Duplicate / concurrency | `ALREADY_EXISTS` / `ABORTED` |
94
+ | Duplicate | `ALREADY_EXISTS` |
95
+ | Stale version / concurrency (`StaleVersionException`) | `ABORTED` |
90
96
  | Auth | `UNAUTHENTICATED` / `PERMISSION_DENIED` |
91
97
  | Anything unexpected | `INTERNAL` (no details leaked) |
92
98
 
93
- Attach the stable error code in trailers metadata key `error-code` so callers can map back to their own domain exceptions. Implement once as a global gRPC exception interceptor (`GrpcExceptionInterceptor`, the counterpart of `GlobalExceptionHandler` — see quarkus-error-handling-i18n skill); individual services never build `StatusRuntimeException` by hand.
99
+ Attach the stable error code in trailers metadata key `error-code` so callers can map it back through their own `ErrorCatalog`. Implement once as a global gRPC exception interceptor (`GrpcExceptionInterceptor` in `common/error`, the counterpart of `GlobalExceptionHandler` — see quarkus-error-handling-i18n skill); it resolves the localized message the same way, from the request's locale metadata. Individual services never build `StatusRuntimeException` by hand, and no `GrpcService` catches `BusinessException`.
94
100
 
95
- ## Client (outbound adapter)
101
+ ## Client (outbound integration bean, `common/client`)
96
102
 
97
- Define an outbound port in `application/port/out` (e.g., `CredentialsValidator`); the gRPC client is its adapter:
103
+ A slice `Handler` must never hold a `@GrpcClient` stub: the generated stub returns `Uni`, which would drag Mutiny into the business logic and break the `reactiveIsQuarantined` rule. Wrap it in a capability-named bean that `await()`s internally and exposes plain types:
98
104
 
99
105
  ```java
106
+ // common/client — named for what it provides, not for how
100
107
  @ApplicationScoped
101
- public class GrpcCredentialsValidator implements CredentialsValidator {
108
+ public class CredentialsValidator {
102
109
 
103
- @GrpcClient("bc05-users") UserService client; // Mutiny stub
110
+ @GrpcClient("iam-users") UserService client; // Mutiny stub, contained here
104
111
 
105
- @Override
106
- public boolean validate(Credentials c) {
107
- return client.validateCredentials(toRequest(c))
112
+ public boolean validate(String username, String password) {
113
+ return client.validateCredentials(
114
+ ValidateCredentialsRequest.newBuilder()
115
+ .setUsername(username).setPassword(password).build())
108
116
  .map(ValidateCredentialsResponse::getValid)
109
- .await().atMost(Duration.ofSeconds(2));
117
+ .await().atMost(Duration.ofSeconds(2)); // deadline also set in config
110
118
  }
111
119
  }
112
120
  ```
113
121
 
122
+ The `Handler` then injects it like any other collaborator:
123
+
124
+ ```java
125
+ @Inject CredentialsValidator credentials; // returns boolean, knows nothing about gRPC
126
+ ```
127
+
128
+ The bean always lives in `common/client`, even when only one slice calls it today — that is the package the `reactiveIsQuarantined` ArchUnit rule exempts (see quarkus-hexagonal-core skill), and it saves relocating the bean the day a second slice needs it. It is mocked in `HandlerTest` exactly like the `Sql` class.
129
+
130
+ An interface is warranted only under the core skill's "when to add an interface back" rule — a second real implementation, a cross-module boundary, or a `common/` consumer that must not see the slice.
131
+
114
132
  ```properties
115
- quarkus.grpc.clients.bc05-users.host=bc05-users.internal
116
- quarkus.grpc.clients.bc05-users.port=9000
117
- quarkus.grpc.clients.bc05-users.deadline=2s
133
+ quarkus.grpc.clients.iam-users.host=iam-users.internal
134
+ quarkus.grpc.clients.iam-users.port=9000
135
+ quarkus.grpc.clients.iam-users.deadline=2s
118
136
  ```
119
137
 
120
138
  Rules:
121
139
  - **Deadline on every client** (config `deadline` or per-call). No unbounded internal calls, ever — cascading hangs kill native pods just as well.
122
140
  - Retries only for idempotent RPCs and only `UNAVAILABLE`/`DEADLINE_EXCEEDED`, with budget — prefer platform/mesh retry policy over hand-rolled loops.
123
141
  - TLS/mTLS per platform standard (`quarkus.grpc.clients.*.ssl.*` / mesh-provided).
124
- - Translate `StatusRuntimeException` back into the caller's domain exceptions at the adapter — gRPC types never cross into application/domain.
142
+ - Translate `StatusRuntimeException` into a `BusinessException` carrying the caller's own error code, inside the integration bean — gRPC types (and `Uni`) never reach a `Handler`. Read the upstream code from the `error-code` trailer and map it through the caller's `ErrorCatalog`.
125
143
 
126
144
  ## Health & reflection
127
145
 
@@ -134,8 +152,9 @@ Tracing is automatic when `quarkus-opentelemetry` is present — `traceparent` p
134
152
 
135
153
  ## Checklist for a new RPC
136
154
 
137
- 1. Proto in shared contracts module, package `vN`, dedicated Request/Response.
138
- 2. Server adapter delegating to a use case; blocking handled explicitly.
139
- 3. Client behind an outbound port, deadline configured.
140
- 4. Error mapping covered by the global interceptor (add new code if needed).
155
+ 1. Proto in `src/main/proto/<module>/v<major>/`, published to `contracts/`, dedicated Request/Response pair.
156
+ 2. `<ProtoService>GrpcService` in the slice folder delegating to the slice `Handler`; proto ↔ DTO conversion inline; blocking handled by `runSubscriptionOn` or `@Blocking`.
157
+ 3. Outbound calls wrapped in a capability-named bean in `common/client` that `await()`s and returns plain types; deadline configured.
158
+ 4. Error mapping covered by the global interceptor (register any new code in `ErrorCatalog` + all locale bundles).
141
159
  5. Contract check in CI (buf breaking-change detection or equivalent).
160
+ 6. No `Uni` outside the `GrpcService` and the integration bean — `reactiveIsQuarantined` proves it.