bmad-method-quarkus 1.0.5 → 1.0.7

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.
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.5",
4
+ "version": "1.0.7",
5
5
  "description": "BMAD Method with Quarkus support",
6
6
  "keywords": [
7
7
  "agile",
@@ -44,7 +44,7 @@ Ultra-succinct, citable, and role-aware. You speak in file paths, AC IDs, and cl
44
44
 
45
45
  ## Domain Standards
46
46
 
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:
47
+ These 9 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 eight use, so it is the one you apply first. Eight are universal; `quarkus-temporal-workflows` applies only to projects that actually use Temporal:
48
48
 
49
49
  | Skill | Consult when the task touches |
50
50
  | --- | --- |
@@ -55,6 +55,8 @@ These 7 standards are installed at `.claude/skills/` and auto-trigger on their o
55
55
  | `quarkus-grpc-services` | `.proto` files, a `GrpcService` adapter, or an outbound call wrapped in `common/client` |
56
56
  | `quarkus-kafka-messaging` | Domain events — the outbox row, the relay mode, a `Consumer`, DLQ, idempotency |
57
57
  | `quarkus-observability-otel` | `@WithSpan` on `process()`, `trace_id`/`span_id` in logs, the `audit_event` context, metrics |
58
+ | `quarkus-security-standards` | Any credential/URL/API key, `.env`/`.env.example`, `application.properties` secrets, OIDC/JWT auth, CORS/TLS, or a security review |
59
+ | `quarkus-temporal-workflows` | **Temporal projects only** — a saga/compensation, a `@WorkflowInterface`/`@ActivityInterface`, `quarkus.temporal.*` configuration, a multi-slice process one transaction can't make atomic, or scaffolding a **worker-only deployable** (no REST, no slices — it overrides the slice layout) |
58
60
 
59
61
  ## On Activation
60
62
 
@@ -86,15 +88,17 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
86
88
 
87
89
  ### Step 5: Load the House Standards
88
90
 
89
- Read all seven Quarkus standards **in full** before doing any work. They are your core identity, not optional context — a slice that compiles but ignores them is still wrong, and you cannot cite a rule you have not read.
91
+ Read the eight universal Quarkus standards **in full** before doing any work. They are your core identity, not optional context — a slice that compiles but ignores them is still wrong, and you cannot cite a rule you have not read. The ninth is conditional: read it only when the project uses Temporal.
90
92
 
91
- 1. `{project-root}/.claude/skills/quarkus-hexagonal-core/SKILL.md` — **read this one first**; the other six use the vocabulary it defines
93
+ 1. `{project-root}/.claude/skills/quarkus-hexagonal-core/SKILL.md` — **read this one first**; the other eight use the vocabulary it defines
92
94
  2. `{project-root}/.claude/skills/quarkus-sql-jdbc-agroal/SKILL.md`
93
95
  3. `{project-root}/.claude/skills/quarkus-error-handling-i18n/SKILL.md`
94
96
  4. `{project-root}/.claude/skills/quarkus-openapi-tmforum/SKILL.md`
95
97
  5. `{project-root}/.claude/skills/quarkus-grpc-services/SKILL.md`
96
98
  6. `{project-root}/.claude/skills/quarkus-kafka-messaging/SKILL.md`
97
99
  7. `{project-root}/.claude/skills/quarkus-observability-otel/SKILL.md`
100
+ 8. `{project-root}/.claude/skills/quarkus-security-standards/SKILL.md`
101
+ 9. `{project-root}/.claude/skills/quarkus-temporal-workflows/SKILL.md` — **only if the project uses Temporal** (`io.quarkiverse.temporal:quarkus-temporal` in a `pom.xml`, a `quarkus.temporal.*` property, or an `orchestration/` package). Skip it otherwise; it governs nothing in a project without workflows.
98
102
 
99
103
  These are **sibling** skills installed flat at `.claude/skills/`, not nested under this one — a bare `skills/<name>/SKILL.md` path resolves from `{skill-root}`, finds nothing, and fails silently. If a file is missing at that path, try `{project-root}/src/bmm-skills/agents/bmad-quarkus-build/skills/<name>/SKILL.md` (pre-install staging layout) and use whichever resolves. If neither resolves, **say so before writing any code** rather than working from memory.
100
104
 
@@ -75,7 +75,7 @@ Canonical rules in the quarkus-hexagonal-core skill; the error/i18n specifics:
75
75
  | gRPC counterpart | `GrpcExceptionInterceptor`, in `common/error` | |
76
76
  | i18n components | `MessageResolver`, `ErrorCatalog` | |
77
77
  | Error DTO | `ErrorDto` (TMF630 Error shape) | |
78
- | Bundle files | `messages/errors[_<lang>].properties` | `errors_en.properties` |
78
+ | Bundle files | `messages/errors[_<lang>].properties` — the default (en) bundle is `errors.properties`, with no suffix | `errors.properties`, `errors_es.properties` |
79
79
  | Bundle key | the error code itself, never a prose key | `USR-404-001` |
80
80
 
81
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.
@@ -189,7 +189,11 @@ public class MessageResolver {
189
189
  var locale = headers.getAcceptableLanguages().stream()
190
190
  .findFirst().filter(l -> !"*".equals(l.getLanguage()))
191
191
  .orElse(Locale.ENGLISH); // default locale = en
192
- var bundle = ResourceBundle.getBundle("messages.errors", locale);
192
+ // getNoFallbackControl is REQUIRED: the default Control falls back to the JVM's
193
+ // default locale before the base bundle, so `Accept-Language: fr` on a pod with
194
+ // LANG=es returns Spanish instead of the English default.
195
+ var bundle = ResourceBundle.getBundle("messages.errors", locale,
196
+ ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES));
193
197
  var pattern = bundle.containsKey(code) ? bundle.getString(code)
194
198
  : bundle.getString("GEN-500-001");
195
199
  return MessageFormat.format(pattern, args);
@@ -140,6 +140,7 @@ Rules:
140
140
  - Retries only for idempotent RPCs and only `UNAVAILABLE`/`DEADLINE_EXCEEDED`, with budget — prefer platform/mesh retry policy over hand-rolled loops.
141
141
  - TLS/mTLS per platform standard (`quarkus.grpc.clients.*.ssl.*` / mesh-provided).
142
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`.
143
+ - **Exception: a bean called from a Temporal activity rethrows the raw `StatusRuntimeException`.** Flattening it to `BusinessException` destroys the `Status.Code`, and that code is the only thing telling Temporal whether the failure is retryable — collapsed, an `UNAVAILABLE` blip becomes indistinguishable from an `INVALID_ARGUMENT` and would permanently fail the saga. The activity classifies it instead (see quarkus-temporal-workflows skill §9). Keep such a bean's activity-facing method separate from any `Handler`-facing one rather than changing behaviour based on the caller.
143
144
 
144
145
  ## Health & reflection
145
146
 
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  name: quarkus-hexagonal-core
3
- description: Foundation for building Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with VERTICAL SLICE architecture over a hexagonal core (ports & adapters collapsed into one folder per feature). Applies to ANY Java/Quarkus backend project — DDD bounded contexts, standalone services, internal tools alike. Use this skill whenever creating, scaffolding, reviewing, or modifying ANY Quarkus backend service, slice, feature, REST/gRPC/Kafka adapter, Sql data class, Handler, or DTO — even if the user only says "create a service", "add an endpoint", "new microservice", or "backend project". Also the canonical source for the SLICE FILE LAYOUT (dto/, Handler, Sql, Resource, GrpcService, Consumer, README) and CLASS NAMING CONVENTIONS, the domain-monorepo layout (apps/, libs/, contracts/, deploy/, db/, docs/), deployable app naming ({module}-{service or submodule}-ms, tenant-agnostic), the mandatory per-app service.yaml + README.md standard, and the Handler contract (@Transactional only on process(), never setAutoCommit/commit/rollback, one Connection per business operation). Enforces Java 25, JDBC-only persistence via Agroal (NO Panache, NO Hibernate/ORM), ArchUnit slice-isolation + naming tests, and native-image compatibility. Project-specific directives (CLAUDE.md, ADRs, explicit instructions) override this standard where they conflict.
3
+ description: Foundation for Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with VERTICAL SLICE architecture over a hexagonal core. Use when creating, scaffolding, reviewing or modifying ANY Quarkus backend service, slice, feature, REST/gRPC/Kafka adapter, Sql data class, Handler or DTO — even if the user only says "create a service", "add an endpoint" or "new microservice". Canonical source for the SLICE FILE LAYOUT, CLASS NAMING CONVENTIONS, the domain-monorepo layout, deployable naming (`-ms` with an API, `-worker` without one), the mandatory per-app service.yaml + README standard including API gateway base paths, and the Handler contract (@Transactional only on process(), one Connection per business operation). Enforces Java 25, JDBC-only persistence via Agroal (NO Panache/ORM), ArchUnit slice-isolation + naming tests, and native-image compatibility. Project directives (CLAUDE.md, ADRs) override this where they conflict.
4
4
  ---
5
5
 
6
6
  # Quarkus Vertical Slice + Hexagonal Core (Native-First)
7
7
 
8
- Foundation skill for all backend services. The other skills in this set (sql-jdbc-agroal, error-handling-i18n, openapi-tmforum, grpc-services, kafka-messaging, observability-otel) build on the structure defined here and use the vocabulary defined here — apply this one first when scaffolding.
8
+ Foundation skill for all backend services. The other eight skills in this set (`quarkus-sql-jdbc-agroal`, `quarkus-error-handling-i18n`, `quarkus-openapi-tmforum`, `quarkus-grpc-services`, `quarkus-kafka-messaging`, `quarkus-observability-otel`, `quarkus-security-standards`, and `quarkus-temporal-workflows` on Temporal projects) build on the structure defined here and use the vocabulary defined here — apply this one first when scaffolding.
9
9
 
10
10
  **Scope & precedence:** these standards apply to any Java/Quarkus backend project, whether or not it is organized as DDD bounded contexts. **Module naming:** every module/bounded context is identified in code by a short, lowercase, semantic name (`iam`, `tenant`, `wallet`, `customer`) — never by an inventory/governance code like `bc01`/`bc02`. If the organization keeps a numbered BC inventory, the `bcNN` ↔ name mapping lives in governance docs (domain README identity table, service catalog); packages, topics, config keys, URLs, metrics and proto packages only ever carry the semantic name. They are the default, not law: a project directive that says otherwise (CLAUDE.md, an ADR, an explicit user instruction) wins over this skill. When you deviate because of such a directive, follow the directive and mention which rule was overridden.
11
11
 
12
+ **One structural exception:** a **Temporal worker-only deployable** (`{module}-{capability}-worker`) — an app that hosts workflows and activities and exposes no API — has no vertical slices, no `Resource`, no `<Slice>Handler`/`<Slice>Sql` pair and usually no datasource. The naming rules, monorepo layout, native build, Dockerfile and the `service.yaml` + README standard below still apply to it in full; the slice layout, the REST adapter rules and the slice-oriented ArchUnit rules do not. See the quarkus-temporal-workflows skill §3 before scaffolding one — do not give it slices because this skill's checklist says so.
13
+
12
14
  ## The central idea: one feature = one folder
13
15
 
14
16
  Classic layered hexagonal architecture spreads a single feature across eight or more files (inbound port, use case, command, result, outbound port, JDBC adapter, DTO, mapper) in four packages. This standard keeps the **hexagon's dependency rule** but collapses it into a **vertical slice**: everything one business capability needs lives in one folder, and the hexagon is expressed by *class role*, not by package depth.
@@ -48,6 +50,7 @@ One monorepo per domain (bounded context in DDD projects) holds **every deployab
48
50
  │ │ ├── service.yaml # structured metadata — JSON-Schema validated in CI
49
51
  │ │ └── README.md # per-app operational doc (see "Per-app documentation")
50
52
  │ ├── iam-identity-ms/ # a second backend in the same domain, same internal structure
53
+ │ ├── iam-onboarding-worker/ # Temporal worker — no API, no slices (see temporal skill)
51
54
  │ ├── iam-admin-mf/ # microfrontend remote — own build, same service.yaml + README rule
52
55
  │ └── iam-shell-module/ # host/shell that composes the remotes
53
56
  ├── libs/ # code shared *within* this domain — never across domains
@@ -61,7 +64,7 @@ One monorepo per domain (bounded context in DDD projects) holds **every deployab
61
64
  └── README.md # domain onboarding doc (see "Domain README")
62
65
  ```
63
66
 
64
- **Scope of this skill inside the monorepo:** everything from "Slice layout" onward governs `-ms` apps only. `-mf` and `-module` apps are first-class residents of the same repo — they share `contracts/`, `libs/web/`, `deploy/` and `docs/`, and they carry the same mandatory `service.yaml` + `README.md` — but their internal structure is not this standard's business. A frontend never reaches a backend except through a published contract in `contracts/`; there is no shared runtime code between an `-ms` and an `-mf`.
67
+ **Scope of this skill inside the monorepo:** everything from "Slice layout" onward governs `-ms` apps only. A `-worker` app is a backend and follows this skill's naming, native build and documentation rules, but it has no slices — its internal structure belongs to the quarkus-temporal-workflows skill. `-mf` and `-module` apps are first-class residents of the same repo — they share `contracts/`, `libs/web/`, `deploy/` and `docs/`, and they carry the same mandatory `service.yaml` + `README.md` — but their internal structure is not this standard's business. A frontend never reaches a backend except through a published contract in `contracts/`; there is no shared runtime code between an `-ms` and an `-mf`.
65
68
 
66
69
  ### Deployable app naming (tenant-agnostic)
67
70
 
@@ -69,7 +72,9 @@ One monorepo per domain (bounded context in DDD projects) holds **every deployab
69
72
  {module}-{service or submodule}-{type-suffix}
70
73
  ```
71
74
 
72
- Suffixes: `-ms` (backend microservice — every Quarkus service here), `-mf` (microfrontend remote), `-module` (host/shell). Example: `iam-management-ms`. **Client and tenant never appear in app or code names** — they are runtime configuration (namespaces, labels, Helm values) applied at deploy time; the code is identical for every client and tenant.
75
+ Suffixes: `-ms` (backend microservice serving an API), `-worker` (backend deployable that serves **no** API currently Temporal workflow workers; see the quarkus-temporal-workflows skill), `-mf` (microfrontend remote), `-module` (host/shell). Examples: `iam-management-ms`, `customer-onboarding-worker`.
76
+
77
+ `-ms` vs `-worker` is a deploy-time contract, not a label: an `-ms` gets an ingress, HTTP probes and RPS-based scaling; a `-worker` gets none of those and scales on queue backlog. An app that serves an API *and* hosts a worker is an `-ms`. **Client and tenant never appear in app or code names** — they are runtime configuration (namespaces, labels, Helm values) applied at deploy time; the code is identical for every client and tenant.
73
78
 
74
79
  ## Slice layout (the canonical structure)
75
80
 
@@ -179,7 +184,7 @@ Several slices may expose operations under the same TMF base path (`create_party
179
184
 
180
185
  | Kind | Convention | Example |
181
186
  |---|---|---|
182
- | Deployable app | `{module}-{service or submodule}-{type}` kebab-case, tenant-agnostic (`-ms` backend) | `iam-management-ms` |
187
+ | Deployable app | `{module}-{service or submodule}-{type}` kebab-case, tenant-agnostic (`-ms` backend with an API, `-worker` backend without one) | `iam-management-ms`, `customer-onboarding-worker` |
183
188
  | Slice package | lowercase `snake_case`, verb-first | `com.alva.customer.create_party_individual` |
184
189
  | SQL constant | `UPPER_SNAKE_CASE` matching the method | `INSERT_PARTY`, `SELECT_PARTY_BY_ID` |
185
190
  | SQL table / column | `snake_case`, singular table, schema-qualified | `customer.party`, `created_at` |
@@ -228,7 +233,7 @@ private void validate(CreatePartyIndividualRequestDto request) {
228
233
 
229
234
  - Opens **one** `Connection` from the injected `DataSource` in try-with-resources and passes it to every `Sql` call in the operation. This is the performance contract: one connection per business operation, whether or not a transaction is active. Under JTA, Agroal would return the same enlisted connection anyway; on a read path with no `@Transactional`, per-method `getConnection()` would acquire *N* separate pool leases for *N* queries — passing `conn` makes both paths cost exactly one.
230
235
  - Calls `${SERVICE_CLASS_PREFIX}Sql` methods in business order (insert `party` first to get `partyId`, reuse it for dependent inserts). Before writing a call, re-read the real `Sql` method signature and pass exactly those parameters, in that order and type.
231
- - Catches every checked `SQLException` and rethrows it as `BusinessException` via `SqlStateTranslator` an unchecked exception is what triggers the container rollback, and a propagated checked exception would not.
236
+ - Catches every checked `SQLException` and rethrows it **unchecked** via `SqlStateTranslator` — as a `BusinessException` for the states that mean a business conflict (`23505`, `23503`) and as the `PersistenceException` family for everything else (see the mapping table in quarkus-sql-jdbc-agroal skill §9). An unchecked exception is what triggers the container rollback; a propagated checked one would not.
232
237
  - Date fields arrive from the DTO as `String`. To use them as `LocalDate`/`ZonedDateTime`, declare a **new local variable** and convert explicitly (`LocalDate.parse(...)`) — never reassign the DTO field.
233
238
  - Domain events are published by inserting an outbox row through `OutboxEventRecorder` from `common/messaging`, on this method's `Connection` and therefore in this same transaction — never by emitting to Kafka directly (see quarkus-kafka-messaging skill). Who relays that row (Debezium, `OutboxRelayJob`, or a post-commit emit) is a runtime property, `app.messaging.outbox.relay-mode`, and changes nothing here.
234
239
  - **The outbox is cross-cutting, not slice-local: never declare `insertOutboxEvent` on a `<Slice>Sql`.** It is the same table, the same columns and the same envelope in every slice, so the "duplicate twice, extract on the third" rule below does not apply — there is nothing slice-specific to discover. The shared `Connection` is what keeps the write transactional; the class's location is irrelevant to that guarantee. Same for `AuditRecorder` and any other table `common/` owns.
@@ -321,7 +326,7 @@ Full standard (pool config, batches, jsonb, pagination, keyset paging, SQLState
321
326
  - Constants are text blocks or comma-first concatenations, schema-qualified, with positional `?` placeholders only. **Check the real schema name before writing a query** — if the schema or table is a reserved word (`order`, `user`, `group`), quote it: `"order".party`.
322
327
  - If the PK has a database default (`id uuid DEFAULT customer.uuidv7() NOT NULL`), the `id` column is excluded from the INSERT column list and never passed as a parameter. Prefer `INSERT ... RETURNING id` over `getGeneratedKeys()`.
323
328
  - **Every method's first parameter is `Connection conn`** — supplied by the Handler, never obtained inside the method. That signature is the contract the Handler must honour exactly.
324
- - Methods `throws SQLException` — they never swallow it and never translate it. Translation to `BusinessException` happens in the Handler via `SqlStateTranslator`.
329
+ - Methods `throws SQLException` — they never swallow it and never translate it. Translation happens in the Handler via `SqlStateTranslator`, into `BusinessException` or the `PersistenceException` family depending on the SQLState.
325
330
  - **No transaction control here.** No `commit`, no `rollback`, no `setAutoCommit`.
326
331
  - Row mapping is hand-written into DTOs (`PartyDto.builder()...`) — no reflection-based mappers, which also keeps native images small.
327
332
 
@@ -344,7 +349,7 @@ All three are thin: extract metadata, invoke `handler.process(...)`, format the
344
349
  - `<Resource>Resource` class with `@Path` + `@Tag`, injecting the slice `Handler`(s).
345
350
  - Returns `RestResponse<T>` synchronously. Never `Uni`/`Multi`.
346
351
  - `@Blocking` on JDBC-backed methods. With a plain (non-reactive) return type Quarkus REST already dispatches to a worker thread, so the annotation is redundant *today* — keep it as an explicit threading contract that survives a later signature change. `@RunOnVirtualThread` is the alternative for high-concurrency blocking endpoints (see sql skill §8).
347
- - Extracts `tenantId`, `partyId`, `partyRolList`, `language` from headers when the contract requires them, and sets them on the request DTO before calling `process()`.
352
+ - Reads `tenantId` — and any other value that drives authorization, such as `partyId` or `partyRolList` — from the **validated token** (`@Inject JsonWebToken jwt` → `jwt.getClaim(...)`, or `@Claim` — not `SecurityIdentity`, which carries roles and mechanism attributes rather than claims), never from a request header the caller controls (see the Tenancy model rule under "Per-app documentation" and quarkus-security-standards skill §4). `language` and other non-security metadata may come from headers. It sets them on the request DTO before calling `process()`.
348
353
  - Full OpenAPI annotations per method: `@Operation` with `operationId`, one `@APIResponse` per relevant status, `@Parameter` for headers and path/query params, `Location` header on create.
349
354
 
350
355
  **gRPC** — full standard in **quarkus-grpc-services**:
@@ -394,10 +399,15 @@ Code shared across **apps** in the domain goes to `libs/` and uses constructor i
394
399
 
395
400
  ## Scaffolding checklist
396
401
 
397
- When creating a new service:
402
+ When creating a new service. A **Temporal worker-only deployable** (quarkus-temporal-workflows skill §3) follows the same steps with a reduced scope — it has no slices and no `Resource`:
398
403
 
399
- 1. App folder `apps/<module>-<service-or-submodule>-ms/` per the monorepo layout above.
400
- 2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (latest 3.x LTS), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-narayana-jta`, `quarkus-hibernate-validator`, `quarkus-smallrye-health`, `quarkus-smallrye-openapi`, `quarkus-opentelemetry`, `quarkus-flyway` (kept even where migrations are disabled — see sql skill §10), plus Lombok and skill-specific extensions as needed.
404
+ - **Step 2 — add** `io.quarkiverse.temporal:quarkus-temporal` and `quarkus-grpc`; **keep** `quarkus-arc`, `quarkus-smallrye-health` and the three observability extensions; **drop** `quarkus-rest*`, `quarkus-smallrye-openapi`, `quarkus-hibernate-validator` and `quarkus-oidc`, and — unless it owns a table of its own — `quarkus-agroal`, `quarkus-jdbc-*`, `quarkus-narayana-jta` and `quarkus-flyway`.
405
+ - **Step 3 —** keep only the error/exception classes it actually throws.
406
+ - **Step 4 —** drop the datasource and Flyway blocks from the baseline below whenever step 2 dropped those extensions. **Leaving `quarkus.datasource.db-kind=postgresql` in place without a JDBC driver fails the Quarkus build** with "Unable to find a JDBC driver corresponding to the database kind". Add the `quarkus.temporal.*` block that skill's §2 defines instead.
407
+ - **Step 5 —** skip the slice-oriented rules and ship the ArchUnit subset that skill lists.
408
+
409
+ 1. App folder `apps/<module>-<service-or-submodule>-ms/` per the monorepo layout above — `-worker` instead of `-ms` for a Temporal worker deployable.
410
+ 2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (latest 3.x LTS), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-narayana-jta`, `quarkus-hibernate-validator`, `quarkus-smallrye-health`, `quarkus-smallrye-openapi`, `quarkus-opentelemetry`, `quarkus-micrometer-registry-prometheus` (mandatory metrics — brings `quarkus-micrometer` transitively) and `quarkus-logging-json` (mandatory structured logs) — the three observability extensions are all required, see that skill — `quarkus-oidc` wherever the app exposes an authenticated API (security skill §4), `quarkus-flyway` (kept even where migrations are disabled — see sql skill §10), plus Lombok and skill-specific extensions as needed.
401
411
  3. `common/` package: `BusinessException`, `ErrorCatalog`, `ErrorDto`, `GlobalExceptionHandler`, `MessageResolver`, `SqlStateTranslator`, `StringUtils`.
402
412
  4. `application.properties` baseline (below).
403
413
  5. ArchUnit test (below) — in the same commit as the first slice, not later.
@@ -434,8 +444,12 @@ quarkus.flyway.migrate-at-start=false
434
444
  app.messaging.outbox.relay-mode=debezium
435
445
  app.messaging.outbox.poller.every=off
436
446
 
447
+ # Metrics — Micrometer is mandatory in every service (see quarkus-observability-otel skill)
448
+ quarkus.micrometer.export.prometheus.enabled=true
449
+ quarkus.datasource.metrics.enabled=true
450
+
437
451
  # i18n (see error-handling skill) — locales must be declared for native
438
- quarkus.locales=en,es
452
+ quarkus.locales=en,es,pt
439
453
  quarkus.default-locale=en
440
454
  quarkus.native.resources.includes=messages/*.properties
441
455
 
@@ -484,13 +498,13 @@ Rules:
484
498
 
485
499
  Every deployable under `apps/<app>/` carries two files, versioned with the code and updated in the **same PR** that changes the service's behavior:
486
500
 
487
- - **`service.yaml`** — structured metadata validated against a JSON Schema in CI: identity (domain, system, component type, lifecycle), ownership (team, SRE, support group), criticality, tenancy (model, data isolation), runtime (type, version, ports), provided APIs with their contract files in `contracts/`, dependencies with `hard`/`soft` requirement, and the observability contract (log format, levels, required fields, data-protection rules). It feeds the service catalog and the dependency map.
501
+ - **`service.yaml`** — structured metadata validated against a JSON Schema in CI: identity (domain, system, component type, lifecycle), **the API gateway base path(s) under `metadata.gateway`** (see "Gateway routing" below), ownership (team, SRE, support group), criticality, tenancy (model, data isolation), runtime (type, version, ports), provided APIs with their contract files in `contracts/`, dependencies with `hard`/`soft` requirement, and the observability contract (log format, levels, required fields, data-protection rules). It feeds the service catalog, the dependency map and the gateway route configuration.
488
502
  - **`README.md`** (per app) — operational doc written so a system administrator who has never seen the code can configure, deploy and recover the service. No `TBD` / "see code" (a justified `N/A` is allowed). No manual changelog section — Git keeps the history.
489
503
 
490
504
  Required `README.md` sections:
491
505
 
492
506
  1. **Identity** — app name, module (`part-of`), type (`ms`), owning team, repository, criticality, part of core yes/no
493
- 2. **Purpose & responsibility** — description + out of scope
507
+ 2. **Purpose, responsibility & gateway routes** — description + out of scope, **plus the API gateway base path(s) this service is published under** (see "Gateway routing" below). A `-worker` app declares `N/A — publishes no API`.
494
508
  3. **Slice inventory** — table: slice folder, capability, exposed channels (REST/gRPC/Kafka), link to the slice README
495
509
  4. **Tenancy model** — `shared` | `per-tenant` | `pool`; tenant identification (validated JWT claim, never a free header); data isolation; per-tenant parameters
496
510
  5. **APIs exposed** — table: api/version, protocol, route/topic, contract file, auth, visibility
@@ -509,6 +523,39 @@ Required `README.md` sections:
509
523
 
510
524
  Rule of completeness: if a variable, secret or dependency is not documented here, it does not exist — CI validates the `service.yaml` schema and the mandatory `README.md` sections as a blocking gate.
511
525
 
526
+ ### Gateway routing (README §2 + `service.yaml` `metadata.gateway`)
527
+
528
+ Infra configures the API gateway from these two files, so the route a service will be published under is declared **before** it is deployed, in the same PR as the endpoint.
529
+
530
+ **The base path is the module's semantic name**, the same name packages, topics, config keys and metrics already use: `/iam`, `/customer`, `/wallet` — **never the inventory code** (`/bc01` is wrong; the `bcNN` ↔ name mapping lives in governance docs, not in a URL). Every north-bound REST API of every `-ms` in the module therefore hangs off one shared prefix, and the API's own path continues from there per the quarkus-openapi-tmforum skill: `/{module}/{context}/{apiName}/v{major}`, e.g. `/iam/tmf-api/digitalIdentityManagement/v4`.
531
+
532
+ README §2 carries the human-readable table:
533
+
534
+ | Base path | Protocol | Visibility | Purpose |
535
+ |---|---|---|---|
536
+ | `/iam` | REST | public | north-bound TMF identity APIs |
537
+
538
+ `service.yaml` carries the same facts as structured metadata:
539
+
540
+ ```yaml
541
+ metadata:
542
+ name: iam-management-ms
543
+ module: iam
544
+ gateway:
545
+ base-paths:
546
+ - path: /iam
547
+ protocol: REST
548
+ visibility: public # public | partner | internal
549
+ purpose: north-bound TMF identity APIs
550
+ ```
551
+
552
+ Rules:
553
+ - **`service.yaml` is authoritative; the README renders the same facts for humans.** They must agree — two copies of one fact drift, and here the drift is a misrouted service. CI cross-checks them.
554
+ - **Only north-bound REST appears here.** Internal gRPC never crosses the gateway (internal calls are gRPC, external is REST — see "Baseline constraints"), so a `*GrpcService` contributes no gateway route.
555
+ - **A `-worker` app declares none** — `metadata.gateway.base-paths: []` and `N/A` in the README. It publishes no API by definition (see the quarkus-temporal-workflows skill).
556
+ - List **every** prefix when a service is published under more than one — a legacy alias kept during a migration, or a second public API — each with its own purpose, so nobody has to guess which is being retired.
557
+ - The declared path must match the ingress/gateway configuration in the GitOps repo. A README that disagrees with what is deployed is a defect, not a stale doc: it is the input infra works from.
558
+
512
559
  ### Slice README (inside the slice folder)
513
560
 
514
561
  Every slice carries its own `README.md`, generated from the real code — no invented routes, fields or tables. Sections:
@@ -547,7 +594,10 @@ class ArchitectureTest {
547
594
  .namingSlices("slice $1")
548
595
  .should().notDependOnEachOther()
549
596
  .ignoreDependency(resideInAPackage("..common.."), alwaysTrue())
550
- .ignoreDependency(alwaysTrue(), resideInAPackage("..common.."));
597
+ .ignoreDependency(alwaysTrue(), resideInAPackage("..common.."))
598
+ // Temporal only: orchestration/ may reach into slices, never the reverse (deliberately
599
+ // asymmetric — see quarkus-temporal-workflows skill §3). Omit on non-Temporal projects.
600
+ .ignoreDependency(resideInAPackage("..orchestration.."), alwaysTrue());
551
601
 
552
602
  // --- Hexagon dependency rule inside the slice ------------------------------
553
603
  @ArchTest
@@ -646,7 +696,9 @@ static final ArchRule businessExceptionIsTheOnlyOne = classes()
646
696
  .should().resideInAPackage("..common.exception..");
647
697
 
648
698
  @ArchTest
649
- static final ArchRule bannedSuffixes = noClasses().should().haveSimpleNameEndingWith("Impl")
699
+ static final ArchRule bannedSuffixes = noClasses()
700
+ .that().resideOutsideOfPackage("..orchestration..") // Temporal workflow/activity Impl pair — see below
701
+ .should().haveSimpleNameEndingWith("Impl")
650
702
  .orShould().haveSimpleNameEndingWith("Util") // note: "Utils" is deliberately absent — common/util/StringUtils
651
703
  .orShould().haveSimpleNameEndingWith("Helper")
652
704
  .orShould().haveSimpleNameEndingWith("Manager")
@@ -657,7 +709,7 @@ static final ArchRule bannedSuffixes = noClasses().should().haveSimpleNameEnding
657
709
  .orShould().haveSimpleNameEndingWith("DTO");
658
710
  ```
659
711
 
660
- `common/util/StringUtils` and the `Jdbc` namespace helper (sql skill §7) are the sanctioned exceptions to `bannedSuffixes` exclude them by fully-qualified name rather than weakening the rule.
712
+ `common/util/StringUtils`, the `Jdbc` namespace helper (sql skill §7) and, on Temporal projects, the `*WorkflowImpl`/`*ActivitiesImpl` pair inside `..orchestration..` (whose interface+implementation split the SDK requires — see quarkus-temporal-workflows skill §3) are the sanctioned exceptions to `bannedSuffixes`. Exclude them by package or fully-qualified name, as above, rather than weakening the rule.
661
713
 
662
714
  Rules the compiler cannot express (enforce in code review and in the generator checklists):
663
715
 
@@ -686,7 +738,7 @@ Rules the compiler cannot express (enforce in code review and in the generator c
686
738
 
687
739
  `<Slice>HandlerTest` is mandatory and mirrors the slice. Rules:
688
740
 
689
- - Mockito only — `@Mock <Slice>Sql sql;`, `@Mock DataSource dataSource;`, `@Mock Connection connection;`, `@InjectMocks <Slice>Handler handler;`, with `when(dataSource.getConnection()).thenReturn(connection)` in `@BeforeEach`. Never a real database, never `@QuarkusTest` for pure business rules, never both `@InjectMocks` (Mockito) and `@InjectMock` (Quarkus) in one file.
741
+ - Mockito only — `@Mock <Slice>Sql sql;`, `@Mock DataSource dataSource;`, `@Mock Connection connection;`, `@InjectMocks <Slice>Handler handler;`. Stub `when(dataSource.getConnection()).thenReturn(connection)` **inside the tests that reach `execution()`**, not in `@BeforeEach`: the mandatory validation-failure test (scenario 2 below) never opens a connection, so a shared stub fails it with Mockito's `UnnecessaryStubbingException` under the default strict stubs. Use `lenient()` only if you genuinely prefer the shared setup. Never a real database, never `@QuarkusTest` for pure business rules, never both `@InjectMocks` (Mockito) and `@InjectMock` (Quarkus) in one file.
690
742
  - Test names: `should<Result>When<Condition>` — `shouldReturnPartyIdWhenRequestIsValid`, `shouldThrowBusinessExceptionWhenGivenNameIsBlank`.
691
743
  - Minimum scenarios per public business path:
692
744
 
@@ -130,7 +130,9 @@ That is the whole dual-write guarantee — one connection, one transaction, no p
130
130
  Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter` — but it lives in a **`common/` capability bean**, never in a slice. `@Channel`/`Emitter` are `org.eclipse.microprofile.reactive.messaging` types, and the `handlerKnowsNoTransport` ArchUnit rule forbids them in a `Handler`:
131
131
 
132
132
  ```java
133
- // common/audit/AuditRecorder.java — the Handler injects AuditRecorder, not the Emitter
133
+ // common/audit/AuditEventPublisher.java — the Handler injects this bean, not the Emitter.
134
+ // Distinct from AuditRecorder (common/audit), which writes the local audit_event table
135
+ // on the Handler's Connection — see quarkus-observability-otel skill.
134
136
  @Inject @Channel("audit-out") Emitter<AuditEventPayload> audit;
135
137
  ```
136
138
 
@@ -314,12 +316,12 @@ mp.messaging.incoming.user-registered-in.connector=smallrye-kafka
314
316
  mp.messaging.incoming.user-registered-in.topic=alva.iam.user.registered.v1
315
317
  mp.messaging.incoming.user-registered-in.group.id=${quarkus.application.name}
316
318
  mp.messaging.incoming.user-registered-in.auto.offset.reset=earliest
317
- mp.messaging.incoming.user-registered-in.failure-strategy=delayed-retry-then-dead-letter-queue
319
+ mp.messaging.incoming.user-registered-in.failure-strategy=dead-letter-queue
318
320
  mp.messaging.incoming.user-registered-in.dead-letter-queue.topic=alva.iam.user.registered.v1.dlq
319
321
  ```
320
322
 
321
323
  Rules:
322
- - **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` checked/inserted through the slice's `Sql` on the same `Connection`, in the same transaction as the side effects. Kafka is at-least-once; duplicates WILL happen.
324
+ - **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` checked/inserted through the cross-cutting `ProcessedEventSql` in `common/messaging` — never a per-slice copy, it is the same table for every slice (see quarkus-sql-jdbc-agroal skill §6) — on the `Connection` the `Handler` already owns, in the same transaction as the side effects. Kafka is at-least-once; duplicates WILL happen.
323
325
  - Consumer group = service name; stable across deployments.
324
326
  - Failure strategy: bounded retries with delay, then DLQ topic `<topic>.dlq`. DLQ messages keep original headers plus `dead-letter-reason`. A DLQ must have an owner and an alert — never a silent graveyard.
325
327
  - Poison-pill safety: deserialization failures also route to DLQ (`...deserialization-failure-handler` or failure strategy), never block the partition.