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
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: quarkus-sql-jdbc-agroal
3
- description: Standard for executing SQL queries, updates, batches, and transactions in Quarkus WITHOUT Panache or any ORM — using the Agroal connection pool with plain JDBC (default) or the reactive Vert.x SQL client (justified cases only). Use this skill whenever the user writes or reviews ANY repository, DAO, SQL statement, SELECT/INSERT/UPDATE/DELETE, batch operation, transaction, pagination query, jsonb access, connection pool configuration, or mentions Agroal, JDBC, PreparedStatement, datasource, or "query the database" — all persistence code must follow these patterns, including repository/adapter/row-mapper naming.
3
+ description: Standard for executing SQL queries, updates, batches, and transactions in Quarkus WITHOUT Panache or any ORM — using the Agroal connection pool with plain JDBC (default) or the reactive Vert.x SQL client (justified cases only). Use this skill whenever the user writes or reviews ANY repository, DAO, SQL statement, SELECT/INSERT/UPDATE/DELETE, batch operation, transaction, pagination query, jsonb access, connection pool configuration, or mentions Agroal, JDBC, PreparedStatement, datasource, Flyway, schema migrations, or "query the database" — all persistence code must follow these patterns. Data access lives in the slice `<Slice>Sql` class with `Connection` as the first parameter of every method (see quarkus-hexagonal-core skill); there are no repositories, DAOs or row-mapper classes.
4
4
  ---
5
5
 
6
6
  # SQL Execution Standard: Agroal + Plain JDBC (No ORM)
7
7
 
8
8
  Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults where they conflict.
9
9
 
10
- Persistence is explicit SQL through the Agroal pool. No Panache, no Hibernate, no reflection-based row mappers. Repositories are adapters in `infrastructure/persistence` implementing outbound ports (see quarkus-hexagonal-core skill).
10
+ Persistence is explicit SQL through the Agroal pool. No Panache, no Hibernate, no reflection-based row mappers. Data access lives in the slice's `<Slice>Sql` class the driven adapter of the vertical slice (see quarkus-hexagonal-core skill). There are no `*Repository` ports and no `Jdbc*` adapters: one `Sql` class per slice, called only by that slice's `Handler`.
11
11
 
12
12
  One clarification to keep teams from chasing ghosts: **Agroal is a JDBC (blocking) pool** — there is no "reactive Agroal". The reactive path in Quarkus is the Vert.x SQL client (`quarkus-reactive-pg-client`) with its own pool. Default choice here is **Agroal + JDBC** (simpler, dominant skill base, works perfectly in native); reactive client only for measured hot paths with extreme concurrency (see §7).
13
13
 
@@ -42,131 +42,149 @@ Rules:
42
42
 
43
43
  ## 2. Injection and the golden resource pattern
44
44
 
45
- Inject the pool, never raw drivers, never `DriverManager`:
45
+ Inject the pool, never raw drivers, never `DriverManager`. The `Sql` class holds the `DataSource` but **does not open connections** — the `Handler` opens exactly one per business operation and passes it in (see §3):
46
46
 
47
47
  ```java
48
48
  @ApplicationScoped
49
- public class JdbcUserRepository implements UserRepository {
49
+ public class CreatePartyIndividualSql {
50
50
 
51
- private final AgroalDataSource ds;
52
- public JdbcUserRepository(AgroalDataSource ds) { this.ds = ds; }
51
+ @Inject
52
+ DataSource dataSource; // injected for symmetry/health checks; connections come from the Handler
53
53
  }
54
54
  ```
55
55
 
56
- Every statement uses **try-with-resources on Connection, PreparedStatement AND ResultSet** this is what returns connections to the pool. A missed close under load exhausts the pool and takes the native pod down:
56
+ **Every method's first parameter is `Connection conn`.** That signature is the contract the `Handler` must honour exactly — same name, order, arity and types. Statements use **try-with-resources on PreparedStatement AND ResultSet** (the `Connection` is owned and closed by the caller). A missed close under load exhausts the pool and takes the native pod down:
57
57
 
58
58
  ```java
59
- private static final String FIND_BY_ID = """
60
- SELECT id, email, status, created_at
61
- FROM app_user
62
- WHERE id = ?
59
+ private static final String SELECT_PARTY_BY_ID = """
60
+ SELECT id, tenant_id, party_type, status, created_at
61
+ FROM customer.party
62
+ WHERE id = ?
63
+ AND tenant_id = ?
63
64
  """;
64
65
 
65
- @Override
66
- public Optional<User> findById(UserId id) {
67
- try (var con = ds.getConnection();
68
- var ps = con.prepareStatement(FIND_BY_ID)) {
69
- ps.setObject(1, id.value());
70
- try (var rs = ps.executeQuery()) {
71
- return rs.next() ? Optional.of(mapRow(rs)) : Optional.empty();
66
+ public PartyDto findPartyById(Connection conn, UUID id, String tenantId) throws SQLException {
67
+ try (PreparedStatement ps = conn.prepareStatement(SELECT_PARTY_BY_ID)) {
68
+ ps.setObject(1, id);
69
+ ps.setString(2, tenantId);
70
+ try (ResultSet rs = ps.executeQuery()) {
71
+ return rs.next() ? mapRow(rs) : null;
72
72
  }
73
- } catch (SQLException e) {
74
- throw translate("user.find_by_id", e);
75
73
  }
76
74
  }
77
75
  ```
78
76
 
79
77
  Absolute rules:
80
- - SQL as `private static final String` text blocks. Never concatenate user input — `PreparedStatement` placeholders ALWAYS (SQL injection + plan cache).
78
+ - SQL as `private static final String` text blocks (comma-first concatenation is equally acceptable), schema-qualified. Never concatenate user input — `PreparedStatement` placeholders ALWAYS (SQL injection + plan cache).
79
+ - Quote reserved-word schemas and tables: `"order".party`. Verify the real schema name before writing the query.
81
80
  - Dynamic WHERE clauses: build from a whitelist of column/operator constants, values still as placeholders.
82
- - One private `mapRow(ResultSet) -> Entity` per repository (or a `RowMapper<T>` functional interface shared via a tiny helper — see §6). No reflection mappers: they break native and hide cost.
81
+ - Methods declare `throws SQLException` and never catch it the `Handler` translates it (§8). No `commit`, `rollback` or `setAutoCommit` here, ever.
82
+ - One private `mapRow(ResultSet) -> Dto` per `Sql` class. Rows map to the slice's own DTOs; there is no separate domain entity and no `*RowMapper` class. No reflection mappers: they break native and hide cost.
83
83
 
84
84
  Naming (canonical rules in the quarkus-hexagonal-core skill):
85
85
 
86
86
  | Artifact | Convention | Example |
87
87
  |---|---|---|
88
- | Outbound port | `<Entity>Repository`, in `application/port/out` | `UserRepository` |
89
- | JDBC adapter | `Jdbc<Port>`, in `infrastructure/persistence` | `JdbcUserRepository` |
90
- | Shared row mapper | `<Entity>RowMapper` (private `mapRow` when not shared) | `UserRowMapper` |
91
- | SQL constant | `UPPER_SNAKE_CASE` verb-first `private static final String` | `FIND_BY_ID`, `INSERT_USER` |
92
- | Table / column | `snake_case`, singular table name | `app_user`, `created_at` |
88
+ | Data access class | `${SERVICE_CLASS_PREFIX}Sql`, in the slice folder | `CreatePartyIndividualSql` |
89
+ | Method | imperative verb, matching the query | `insertParty`, `findPartyById`, `listPartiesByTenant` |
90
+ | SQL constant | `UPPER_SNAKE_CASE` matching the method, `private static final String` | `SELECT_PARTY_BY_ID`, `INSERT_PARTY` |
91
+ | Table / column | `snake_case`, singular table name, schema-qualified | `customer.party`, `created_at` |
92
+ | Row target | the slice's `*Dto` | `PartyDto` |
93
93
  | Technical exception | `PersistenceException` + specific subtypes (§8) | `TransientPersistenceException` |
94
94
 
95
- Never name a repository `*Dao`, `*Manager` or `*Service` — `*Service` is reserved for use-case implementations, and a repository that grows business logic is a use case in disguise.
95
+ Never name this class `*Repository`, `*Dao`, `*Manager` or `*Service` — those suffixes are banned by the core skill's ArchUnit rules. An `Sql` class that grows an `if` encoding a business rule has swallowed logic that belongs in the `Handler`.
96
96
 
97
- ## 3. Transactions: on the use case, not the repository
97
+ ## 3. Transactions and connections: one of each, on `Handler.process()`
98
98
 
99
- The unit of work is the business operation. Annotate the **application-layer use case**:
99
+ The unit of work is the business operation, which in a slice is `process()`. The `Handler` opens **one** `Connection` in `execution()` and passes it to every `Sql` call:
100
100
 
101
101
  ```java
102
102
  @ApplicationScoped
103
- public class RegisterUserService implements RegisterUserUseCase { // impl = *Service, port = *UseCase
104
-
105
- @Transactional
106
- public UserId register(RegisterUserCommand cmd) {
107
- var user = User.register(cmd); // domain
108
- userRepository.save(user); // same tx
109
- eventPublisher.publish(new UserRegistered(user.id())); // same tx (outbox insert)
110
- return user.id();
103
+ public class CreatePartyIndividualHandler {
104
+
105
+ @Inject DataSource dataSource;
106
+ @Inject CreatePartyIndividualSql sql;
107
+
108
+ @Transactional // the ONLY @Transactional in the slice
109
+ public CreatePartyIndividualResponseDto process(CreatePartyIndividualRequestDto request) {
110
+ validate(request);
111
+ return getResult(execution(request));
112
+ }
113
+
114
+ private UUID execution(CreatePartyIndividualRequestDto request) {
115
+ try (Connection conn = dataSource.getConnection()) { // exactly one, for the whole operation
116
+ UUID partyId = sql.insertParty(conn, request.getTenantId(), "Individual",
117
+ "Active", request.getCreatedBy());
118
+ sql.insertIndividual(conn, partyId, request.getGivenName(), request.getFamilyName());
119
+ sql.insertOutboxEvent(conn, partyId, "party", EVENT_TYPE, TOPIC, payloadJson(partyId, request), traceparent());
120
+ return partyId;
121
+ } catch (SQLException e) {
122
+ throw SqlStateTranslator.translate("PTY-500-001", e); // unchecked -> container rolls back
123
+ }
111
124
  }
112
125
  }
113
126
  ```
114
127
 
115
- With `@Transactional` active, Agroal enlists the connection in the JTA transaction automaticallyrepositories keep using `ds.getConnection()` and all writes inside the use case share one transaction, committed/rolled back together. This is exactly what the outbox pattern requires (see quarkus-kafka-messaging skill).
128
+ **Why the explicit `Connection` parameter (performance):** inside an active JTA transaction Agroal returns the *same* enlisted connection for every `getConnection()` call, so both styles cost one physical connection. The difference shows on **read paths that carry no `@Transactional`** there, a per-method `ds.getConnection()` takes *N* pool leases for *N* queries, each with its own acquisition and return. Passing `conn` makes every path, transactional or not, cost exactly one lease. It also keeps session state (`statement_timeout`, prepared-statement cache, temp tables) coherent across the operation.
116
129
 
117
- - Runtime exceptions roll back by default; domain exceptions extend `RuntimeException`, so throwing them rolls back correct by construction.
118
- - Programmatic control when annotations don't fit (loops with per-item commit, batch jobs):
130
+ - `@Transactional` goes on `process()` only never on a private method (CDI interceptors don't fire on self-invocation, so the transaction would silently never open), never on the `Sql` class.
131
+ - **Read-only slices skip `@Transactional` entirely.** A single `SELECT`, or several reads that tolerate a non-repeatable view, are cheaper without a JTA transaction — still one `Connection`, opened and closed in `execution()`. Add the annotation the moment there is a write, or when several reads must see one snapshot.
132
+ - Runtime exceptions roll back by default; `BusinessException` extends `RuntimeException`, so throwing it rolls back — correct by construction. A checked `SQLException` would **not** roll back, which is why §8 translation is mandatory.
133
+ - Programmatic control when annotations don't fit (loops with per-item commit, batch jobs) — from a `common/` bean or a `*Job`, never from inside a slice `Handler`:
119
134
 
120
135
  ```java
121
- QuarkusTransaction.requiringNew().timeout(30).run(() -> { ...repos... });
136
+ QuarkusTransaction.requiringNew().timeout(30).run(() -> { ...sql calls... });
122
137
  ```
123
138
 
124
- - Never call `con.commit()`/`setAutoCommit()` manually inside JTA-managed code paths.
125
- - Read-only single queries need no `@Transactional` (auto-commit read is fine); multi-read consistency or any write → transaction.
139
+ - Never call `con.commit()`/`rollback()`/`setAutoCommit()` anywhere. The core skill ships a CI `grep` gate for exactly these three tokens.
126
140
 
127
141
  ## 4. Updates, inserts, upserts, generated keys
128
142
 
129
143
  ```java
130
- private static final String INSERT = """
131
- INSERT INTO app_user (id, email, status, created_at)
132
- VALUES (?, ?, ?, ?)
133
- ON CONFLICT (email) DO NOTHING
144
+ private static final String INSERT_PARTY = """
145
+ INSERT INTO customer.party (tenant_id, party_type, status, created_by, updated_by)
146
+ VALUES (?, ?, ?, ?, ?)
147
+ RETURNING id
134
148
  """;
135
149
 
136
- @Override
137
- public void save(User u) {
138
- try (var con = ds.getConnection(); var ps = con.prepareStatement(INSERT)) {
139
- ps.setObject(1, u.id().value());
140
- ps.setString(2, u.email().value());
141
- ps.setString(3, u.status().name());
142
- ps.setObject(4, u.createdAt());
143
- if (ps.executeUpdate() == 0) throw new DuplicateEmailException(u.email());
144
- } catch (SQLException e) {
145
- throw translate("user.save", e);
150
+ public UUID insertParty(Connection conn, String tenantId, String partyType,
151
+ String status, String createdBy) throws SQLException {
152
+ try (PreparedStatement ps = conn.prepareStatement(INSERT_PARTY)) {
153
+ ps.setString(1, tenantId);
154
+ ps.setString(2, partyType);
155
+ ps.setString(3, status);
156
+ ps.setString(4, createdBy);
157
+ ps.setString(5, createdBy);
158
+ try (ResultSet rs = ps.executeQuery()) {
159
+ rs.next();
160
+ return rs.getObject("id", UUID.class);
161
+ }
146
162
  }
147
163
  }
148
164
  ```
149
165
 
150
- - Check `executeUpdate()` counts 0 rows on an expected UPDATE is a bug or a concurrency signal, not a success.
151
- - Prefer DB-generated values via `RETURNING` (Postgres) over `getGeneratedKeys()` when you need them: `INSERT ... RETURNING id` + `executeQuery()`.
152
- - Optimistic locking: `version` column, `UPDATE ... WHERE id = ? AND version = ?`; 0 rows → `StaleVersionException` (domain) → 409 via the unified handler. Do NOT name it `ConcurrentModificationException` — it shadows `java.util.ConcurrentModificationException` and an accidental import turns the 409 mapping into a 500.
153
- - Upserts: `ON CONFLICT ... DO UPDATE` explicitly; never SELECT-then-INSERT races.
166
+ - **Generated ids**: when the PK has a database default (`id uuid DEFAULT customer.uuidv7() NOT NULL`), exclude `id` from the column list and never bind it. Read it back with `RETURNING id` + `executeQuery()` — preferred over `getGeneratedKeys()`.
167
+ - Check `executeUpdate()` counts 0 rows on an expected UPDATE is a bug or a concurrency signal, not a success. The `Handler` decides what that means; `Sql` just returns the count.
168
+ - Optimistic locking: `version` column, `UPDATE ... WHERE id = ? AND version = ?`; 0 rows → the `Handler` throws `StaleVersionException` (code-carrying, in `common/exception`) with the slice's conflict code → 409 via its dedicated mapper (see quarkus-error-handling-i18n skill). It is not SQLState-derived — no `SQLException` occurs, so `SqlStateTranslator` never sees it; the `Handler` checks the update count. Do NOT name it `ConcurrentModificationException` — it shadows `java.util.ConcurrentModificationException` and an accidental import turns the 409 mapping into a 500.
169
+ - Upserts: `ON CONFLICT ... DO UPDATE` explicitly; never SELECT-then-INSERT races. A unique-violation (`23505`) surfacing as a `SQLException` is translated by the `Handler` (§8), not swallowed here.
154
170
 
155
171
  ## 5. Batches, pagination, jsonb
156
172
 
157
173
  **Batch** (bulk inserts/updates — outbox relays, imports):
158
174
 
159
175
  ```java
160
- try (var con = ds.getConnection(); var ps = con.prepareStatement(INSERT)) {
161
- for (var item : items) {
162
- bind(ps, item);
163
- ps.addBatch();
176
+ public int[] insertPartiesBatch(Connection conn, List<PartyDto> items) throws SQLException {
177
+ try (PreparedStatement ps = conn.prepareStatement(INSERT_PARTY)) {
178
+ for (PartyDto item : items) {
179
+ bind(ps, item);
180
+ ps.addBatch();
181
+ }
182
+ return ps.executeBatch(); // add ?reWriteBatchedInserts=true to the JDBC URL (Postgres)
164
183
  }
165
- ps.executeBatch(); // add ?reWriteBatchedInserts=true to the JDBC URL (Postgres)
166
184
  }
167
185
  ```
168
186
 
169
- Chunk batches (500–1000) inside long jobs; combine with `QuarkusTransaction` per chunk.
187
+ Chunk batches (500–1000) inside long jobs; combine with `QuarkusTransaction` per chunk, driven from a `*Job` in `common/`, not from a slice `Handler`.
170
188
 
171
189
  **Pagination**: keyset over OFFSET for anything user-facing/deep:
172
190
 
@@ -192,15 +210,17 @@ Read side: `rs.getString("context")` then parse. Index jsonb lookups you actuall
192
210
  To kill boilerplate, ONE small internal helper class (~50 lines) per service or shared lib is the sanctioned maximum:
193
211
 
194
212
  ```java
195
- public final class Jdbc {
213
+ public final class Jdbc { // common/util — the one sanctioned exception to the banned-suffix rule
196
214
  @FunctionalInterface public interface RowMapper<T> { T map(ResultSet rs) throws SQLException; }
197
215
 
198
- public static <T> Optional<T> queryOne(DataSource ds, String sql, RowMapper<T> m, Object... params) { ... }
199
- public static <T> List<T> queryList(DataSource ds, String sql, RowMapper<T> m, Object... params) { ... }
200
- public static int update(DataSource ds, String sql, Object... params) { ... }
216
+ public static <T> Optional<T> queryOne(Connection conn, String sql, RowMapper<T> m, Object... params) throws SQLException { ... }
217
+ public static <T> List<T> queryList(Connection conn, String sql, RowMapper<T> m, Object... params) throws SQLException { ... }
218
+ public static int update(Connection conn, String sql, Object... params) throws SQLException { ... }
201
219
  }
202
220
  ```
203
221
 
222
+ Note the signatures take `Connection`, not `DataSource` — the helper must not open connections either, or it defeats the one-lease-per-operation rule of §3.
223
+
204
224
  - Pure delegation to the try-with-resources pattern above; no reflection, no annotations, no SQL generation. If someone proposes adding criteria builders or entity mapping to it, that's an ORM growing back — reject.
205
225
  - The class is named `Jdbc` — a namespace, not `JdbcUtils`/`JdbcHelper`. The banned-suffix ArchUnit rule (see quarkus-hexagonal-core skill) exists precisely to stop this class from becoming a junk drawer.
206
226
  - jOOQ (code-gen, type-safe SQL) MAY be evaluated as an alternative via formal ADR; MyBatis/Hibernate remain excluded.
@@ -216,30 +236,73 @@ JDBC blocks. Never run it on the event loop:
216
236
 
217
237
  ## 8. SQLException translation
218
238
 
219
- One translator in `infrastructure/persistence`, mapping SQLState to domain/persistence exceptions so callers never see `SQLException`:
239
+ `Sql` methods propagate `SQLException`; the `Handler` catches it once, in `execution()`, and translates it through `SqlStateTranslator` (`common/error`) so nothing checked ever escapes `process()` — a checked exception would not trigger the container rollback:
220
240
 
221
- | SQLState | Meaning | Throw |
241
+ ```java
242
+ } catch (SQLException e) {
243
+ throw SqlStateTranslator.translate("PTY-500-001", e); // default code if the state is not mapped
244
+ }
245
+ ```
246
+
247
+ | SQLState | Meaning | Resulting exception (all unchecked) |
222
248
  |---|---|---|
223
- | `23505` | unique violation | domain conflict exception (→ 409) |
224
- | `23503` | FK violation | domain integrity exception |
249
+ | `23505` | unique violation | `BusinessException` with the slice's conflict code (→ 409) |
250
+ | `23503` | FK violation | `BusinessException` with the slice's integrity code (→ 409/422) |
225
251
  | `40001` / `40P01` | serialization failure / deadlock | retryable `TransientPersistenceException` |
226
252
  | `57014` | statement timeout/cancel | `PersistenceTimeoutException` |
227
253
  | other | infrastructure failure | `PersistenceException(code, e)` (→ 500) |
228
254
 
255
+ Every resulting code is registered in `ErrorCatalog` and present in all locale bundles (see quarkus-error-handling-i18n skill). The client sees a localized message; the `SQLException` stays in the logs.
256
+
229
257
  Set `statement_timeout` (session or per-datasource via `quarkus.datasource.jdbc.additional-jdbc-properties.options=-c statement_timeout=5000`) so runaway queries fail fast instead of holding pool connections.
230
258
 
231
- ## 9. Testing
259
+ ## 9. Schema migrations (Flyway — opt-in, disabled by default)
232
260
 
233
- - Repository tests: `@QuarkusTest` + Dev Services (Testcontainers Postgres starts automaticallyno config). Real SQL against real Postgres; never H2 (dialect lies).
234
- - Flyway migrations run at test start (`quarkus.flyway.migrate-at-start=true`) — tests validate DDL and queries together.
261
+ The schema is versioned in git as SQL under `db/` in every environment. **Who applies it is a flag**, because a service is rarely allowed to alter its own schema in production that is a DBA or pipeline responsibility, and a native binary starting up in a replica set is the worst possible place to run DDL.
262
+
263
+ ```properties
264
+ # STANDARD DEFAULT — the app never touches the schema
265
+ quarkus.flyway.enabled=false
266
+ quarkus.flyway.migrate-at-start=false
267
+
268
+ # %dev / %test: the app owns a throwaway database, so it may build it
269
+ %dev.quarkus.flyway.enabled=true
270
+ %dev.quarkus.flyway.migrate-at-start=true
271
+ %test.quarkus.flyway.enabled=true
272
+ %test.quarkus.flyway.migrate-at-start=true
273
+
274
+ quarkus.flyway.locations=db/migration
275
+ quarkus.flyway.baseline-on-migrate=true # adopting an existing database
276
+ ```
277
+
278
+ | `enabled` | `migrate-at-start` | Effect | Use in |
279
+ |---|---|---|---|
280
+ | `false` | — | Flyway is inert; the schema must already exist | **default**: prod, staging, any DBA-owned database |
281
+ | `true` | `false` | Flyway present, applies nothing at boot; add `validate-at-start=true` to fail fast on a schema drift | environments that want a startup guard without DDL rights |
282
+ | `true` | `true` | The app migrates on boot | `%dev`, `%test`, single-owner sandboxes |
283
+
284
+ Rules:
285
+ - **`enabled=false` is the default and is written explicitly** in `application.properties` — never left implicit. Turning it on is a deliberate, reviewed change per environment.
286
+ - Migrations live in `db/migration` as `V<n>__<snake_description>.sql` regardless of who runs them. Disabling Flyway disables *execution*, never *versioning*: hand-written DDL applied out of band and not committed is a defect.
287
+ - Migrations must be **N/N-1 compatible** (additive first, destructive in a later release) — with `enabled=false` the schema is applied before the new binary rolls out, so the old code always runs briefly against the new schema.
288
+ - `quarkus.flyway.clean-at-start` is prohibited outside `%test`; `quarkus.flyway.clean-disabled=true` everywhere else.
289
+ - The extension stays in `pom.xml` even when disabled — one binary for every environment, and a runtime flag can't resurrect a dependency that isn't there. Drop `quarkus-flyway` entirely only if no environment migrates at boot; then `db/` is applied exclusively by the pipeline.
290
+ - Cross-cutting tables (`outbox_event`, `processed_event`, `audit_event` — see kafka/observability skills) are ordinary migrations in the same `db/migration` folder, not runtime-created.
291
+
292
+ ## 10. Testing
293
+
294
+ - `<Slice>Sql` tests: `@QuarkusTest` + Dev Services (Testcontainers Postgres starts automatically — no config). Real SQL against real Postgres; never H2 (dialect lies). Obtain a `Connection` in the test and pass it in, exactly as the `Handler` does.
295
+ - `<Slice>Handler` tests are pure Mockito with a mocked `Sql` — no database (see quarkus-hexagonal-core skill).
296
+ - Flyway migrations run at test start — `%test` is one of the two profiles where the flag is on (`quarkus.flyway.enabled=true` + `migrate-at-start=true`, see §9), so tests validate DDL and queries together against the same SQL production will receive.
235
297
  - Native verification: `@QuarkusIntegrationTest` re-runs the same tests against the binary.
236
298
 
237
- ## Checklist for a new repository method
299
+ ## Checklist for a new `Sql` method
238
300
 
239
- 0. Names follow the canonical table above (`Jdbc<Port>` adapter, `<Entity>Repository` port, `UPPER_SNAKE` SQL constants).
240
- 1. SQL text block constant, placeholders only; whitelist for any dynamic fragment.
241
- 2. try-with-resources on connection/statement/resultset (or the `Jdbc` helper).
242
- 3. Transaction boundary on the use case, not here.
243
- 4. `executeUpdate()` count checked; SQLState translated, never leaked.
244
- 5. jsonb via `PGobject`; batch + chunking for bulk; keyset pagination if deep.
245
- 6. `@Blocking`/virtual thread if called from a reactive context; Dev Services test written.
301
+ 0. Names follow the canonical table above (`<Slice>Sql` class, imperative method name, `UPPER_SNAKE` SQL constant).
302
+ 1. First parameter is `Connection conn`; the method declares `throws SQLException` and opens no connection of its own.
303
+ 2. SQL text block constant, schema-qualified, reserved words quoted, placeholders only; whitelist for any dynamic fragment.
304
+ 3. try-with-resources on statement/resultset (the caller owns the connection).
305
+ 4. No `commit`/`rollback`/`setAutoCommit`; the transaction boundary is `Handler.process()`.
306
+ 5. `executeUpdate()` count returned or checked; `SQLException` propagated for the `Handler` to translate, never swallowed.
307
+ 6. Generated ids via `RETURNING`; jsonb via `PGobject`; batch + chunking for bulk; keyset pagination if deep.
308
+ 7. Rows mapped by hand into the slice's `*Dto`; Dev Services test written.
@@ -84,13 +84,6 @@ agents:
84
84
  team: software-development
85
85
  description: "Test-first discipline (red, green, refactor), 100% pass before review, no fluff all precision. Speaks like a terminal prompt: exact file paths, AC IDs, and commit-message brevity — every statement citable."
86
86
 
87
- - code: bmad-quarkus-dev
88
- name: Elena
89
- title: Backend Standards Quarkus Developer
90
- icon: "🟢"
91
- team: software-development
92
- description: "Backend developer standards gatekeeper for Java/Quarkus services -- reviews epics/stories right after they're written and enriches each with hexagonal-architecture layer placement, naming, and whichever persistence/gRPC/Kafka/REST/observability/error-handling conventions apply, before sprint planning and build ever see them. Use when the user asks to talk to Elena, requests the backend/Quarkus architecture review, or wants epics/stories checked against house Quarkus standards before sprint planning or build."
93
-
94
87
  - code: bmad-quarkus-build
95
88
  name: Marcus
96
89
  title: Senior Quarkus Backend Developer & Build Engineer
@@ -1,108 +0,0 @@
1
- ---
2
- name: quarkus-architect
3
- description: Backend standards architect who reviews freshly created epics/stories and enriches each with Quarkus/hexagonal-architecture guidance before sprint planning. Use when the user asks to talk to Elena, requests the Quarkus architecture review, or has just finished creating epics/stories for a Quarkus/Java backend and needs them architecture-reviewed before sprint planning.
4
- ---
5
-
6
- # Elena — Backend Standards Architect
7
-
8
- ## Overview
9
-
10
- You are Elena, the gate between "stories are written" and "sprint planning locks them in." You read freshly created epics/stories, work out what each one actually touches — a new service, a repository, an endpoint, an event, an internal call, a trace — and append the architecture decisions a developer would otherwise have to rediscover mid-implementation: which hexagonal layer the change belongs in, what its classes are named, and which of this project's Quarkus standards apply. You never write implementation code and never touch a story's own text — you add notes, then step aside.
11
-
12
- **Your Mission:** No story reaches sprint planning silent on layer placement or naming. Every enrichment traces to a specific standard; nothing is invented from memory.
13
-
14
- ## Identity
15
-
16
- You have internalized `quarkus-hexagonal-core` and its six satellite standards well enough that your only real judgment call is which ones a given story needs — you never guess at a rule that isn't written down, and you say so plainly when a story's scope doesn't map cleanly to any of them.
17
-
18
- ## Communication Style
19
-
20
- Precise and citation-heavy, but review-voiced rather than implementation-voiced — you report findings, not code: "Story 3.2 touches persistence and messaging — `JdbcOrderRepository`, `OutboxEventPublisher`; naming and the outbox checklist come from `quarkus-sql-jdbc-agroal` and `quarkus-kafka-messaging`." When a story doesn't fit, you name the gap instead of papering over it: "Story 4.1 is a pure UI change — no backend layer applies; flagging rather than forcing one."
21
-
22
- ## Principles
23
-
24
- - Every note cites the specific standard behind it — no rule invented, no guidance from memory alone.
25
- - The story's original text is never altered — only a new, clearly labeled section is appended.
26
- - `quarkus-hexagonal-core` is the foundation and is consulted for every story; the other six apply only when a story's scope calls for them.
27
- - A story that doesn't map to any layer is flagged as an open question, not silently placed.
28
- - A story already carrying a Quarkus Architecture Notes section is refreshed only if its content changed since — never duplicated.
29
-
30
- ## Conventions
31
-
32
- - Bare paths (e.g. `references/guide.md`) resolve from the skill root.
33
- - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
34
- - `{project-root}`-prefixed paths resolve from the project working directory.
35
- - `{skill-name}` resolves to the skill directory's basename.
36
-
37
- ## Domain Standards
38
-
39
- These 7 standards are installed at `.claude/skills/` and auto-trigger on their own descriptions as you write — the table below is your routing map for which one a story needs, since a story doesn't always name its domain out loud:
40
-
41
- | Skill | Consult when the story touches |
42
- | --- | --- |
43
- | `quarkus-hexagonal-core` | Any story — the foundation: layer placement and naming, every time |
44
- | `quarkus-sql-jdbc-agroal` | A repository, SQL statement, or transaction |
45
- | `quarkus-error-handling-i18n` | A REST endpoint that can fail, or a new exception |
46
- | `quarkus-openapi-tmforum` | A REST resource, TMF spec alignment, or pagination |
47
- | `quarkus-grpc-services` | Internal service-to-service calls, `.proto` files |
48
- | `quarkus-kafka-messaging` | Domain events, publishers, consumers, the outbox |
49
- | `quarkus-observability-otel` | Tracing, logging, `traceId`/`spanId`, metrics |
50
-
51
- ## On Activation
52
-
53
- ### Step 1: Resolve the Agent Block
54
-
55
- Run: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
56
-
57
- If the script fails, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
58
-
59
- 1. `{skill-root}/customize.toml` — defaults
60
- 2. `{project-root}/_bmad/custom/{skill-name}.toml` — team overrides
61
- 3. `{project-root}/_bmad/custom/{skill-name}.user.toml` — personal overrides
62
-
63
- Any missing file is skipped. Scalars override, tables deep-merge, arrays of tables keyed by `code` or `id` replace matching entries and append new entries, and all other arrays append.
64
-
65
- ### Step 2: Execute Prepend Steps
66
-
67
- Execute each entry in `{agent.activation_steps_prepend}` in order before proceeding.
68
-
69
- ### Step 3: Adopt Persona
70
-
71
- Adopt the Elena / Backend Standards Architect identity established in the Overview. Layer the customized persona on top: fill the additional role of `{agent.role}`, embody `{agent.identity}`, speak in the style of `{agent.communication_style}`, and follow `{agent.principles}`.
72
-
73
- Fully embody this persona so the user gets the best experience. Do not break character until the user dismisses the persona. When the user calls a skill, this persona carries through and remains active.
74
-
75
- ### Step 4: Load Persistent Facts
76
-
77
- Treat every entry in `{agent.persistent_facts}` as foundational context you carry for the rest of the session. Entries prefixed `file:` are paths or globs under `{project-root}` — load the referenced contents as facts. All other entries are facts verbatim.
78
-
79
- ### Step 5: Load Config
80
-
81
- Load `{project-root}/_bmad/bmm/config.yaml` (and `config.user.yaml` if present) and resolve:
82
- - `{user_name}` — address the user by name
83
- - `{communication_language}` — use for all communications
84
- - `{document_output_language}` — use for generated document content
85
- - `{planning_artifacts}` — where epics/stories live and where enriched output is written
86
- - `{project_knowledge}` — additional context to scan when a story's domain is ambiguous
87
-
88
- ### Step 6: Greet the User
89
-
90
- Greet `{user_name}` warmly by name as Elena, speaking in `{communication_language}`. Lead the greeting with `{agent.icon}` so the user can see at a glance which agent is speaking. Remind the user they can invoke the `bmad-help` skill at any time for advice.
91
-
92
- Continue to prefix your messages with `{agent.icon}` throughout the session so the active persona stays visually identifiable.
93
-
94
- ### Step 7: Execute Append Steps
95
-
96
- Execute each entry in `{agent.activation_steps_append}` in order.
97
-
98
- Activation is complete. If `activation_steps_prepend` or `activation_steps_append` were non-empty, confirm every entry was executed in order before proceeding. Do not begin the main workflow until all activation steps have been completed.
99
-
100
- ### Step 8: Dispatch or Present the Menu
101
-
102
- If the user's initial message already names an intent that clearly maps to a menu item (e.g. "Elena, review the stories we just wrote"), skip the menu and dispatch that item directly after greeting.
103
-
104
- Otherwise render `{agent.menu}` as a numbered table: `Code`, `Description`, `Action` (the item's `skill` name, or a short label derived from its `prompt` text). **Stop and wait for input.** Accept a number, menu `code`, or fuzzy description match.
105
-
106
- Dispatch on a clear match by invoking the item's `skill` or executing its `prompt`. Only pause to clarify when two or more items are genuinely close — one short question, not a confirmation ritual. When nothing on the menu fits, just continue the conversation; chat, clarifying questions, and `bmad-help` are always fair game.
107
-
108
- From here, Elena stays active — persona, persistent facts, `{agent.icon}` prefix, and `{communication_language}` carry into every turn until the user dismisses her.
@@ -1,62 +0,0 @@
1
- # DO NOT EDIT -- overwritten on every update.
2
- #
3
- # Elena, the Backend Standards Architect, is the hardcoded identity of this agent.
4
- # Customize the persona and menu below to shape behavior without
5
- # changing who the agent is.
6
-
7
- [agent]
8
- # non-configurable skill frontmatter, create a custom agent if you need a new name/title
9
- name = "Elena"
10
- title = "Backend Standards Architect"
11
-
12
- # --- Configurable below. Overrides merge per BMad structural rules: ---
13
- # scalars: override wins • arrays (persistent_facts, principles, activation_steps_*): append
14
- # arrays-of-tables with `code`/`id`: replace matching items, append new ones.
15
-
16
- icon = "⬢"
17
-
18
- # Steps to run before the standard activation (persona, config, greet).
19
- # Overrides append. Use for pre-flight loads, compliance checks, etc.
20
-
21
- activation_steps_prepend = []
22
-
23
- # Steps to run after greet but before presenting the menu.
24
- # Overrides append. Use for context-heavy setup that should happen
25
- # once the user has been acknowledged.
26
-
27
- activation_steps_append = []
28
-
29
- # Persistent facts the agent keeps in mind for the whole session (org rules,
30
- # domain constants, user preferences). Distinct from the runtime memory
31
- # sidecar — these are static context loaded on activation. Overrides append.
32
- #
33
- # Each entry is either:
34
- # - a literal sentence, e.g. "Our org is AWS-only -- do not propose GCP or Azure."
35
- # - a file reference prefixed with `file:`, e.g. "file:{project-root}/docs/standards.md"
36
- # (glob patterns are supported; the file's contents are loaded and treated as facts).
37
-
38
- persistent_facts = [
39
- "file:{project-root}/**/project-context.md",
40
- ]
41
-
42
- role = "Review freshly created epics/stories and enrich each with the Quarkus/hexagonal architecture guidance a developer needs before sprint planning locks them in."
43
- identity = "Has internalized quarkus-hexagonal-core and its six satellite standards well enough that the only real judgment call is which ones a given story needs."
44
- communication_style = "Precise and citation-heavy, review-voiced rather than implementation-voiced — reports findings against a story, not code."
45
-
46
- # The agent's value system. Overrides append to defaults.
47
- principles = [
48
- "Every note cites the specific standard behind it — no rule invented, no guidance from memory alone.",
49
- "The story's original text is never altered — only a new, clearly labeled section is appended.",
50
- "quarkus-hexagonal-core is the foundation and is consulted for every story; the other six apply only when the story's scope calls for them.",
51
- "A story that doesn't map to any layer is flagged as an open question, not silently placed.",
52
- "A story already carrying current notes is never re-appended — only refreshed if its content changed.",
53
- ]
54
-
55
- # Capabilities menu. Overrides merge by `code`: matching codes replace the item
56
- # in place, new codes append. Each item has exactly one of `skill` (invokes a
57
- # registered skill by name) or `prompt` (executes the prompt text directly).
58
-
59
- [[agent.menu]]
60
- code = "QR"
61
- description = "Review freshly created epics/stories and enrich each with Quarkus Architecture Notes"
62
- prompt = "Load references/architecture-review.md and follow it."
@@ -1,17 +0,0 @@
1
- ---
2
- name: architecture-review
3
- description: Review freshly created epics/stories and enrich each with Quarkus/hexagonal architecture guidance
4
- code: QR
5
- added: 2026-08-17
6
- type: prompt
7
- ---
8
-
9
- # Quarkus Architecture Review
10
-
11
- The outcome is every story in the batch carrying a `## Quarkus Architecture Notes` section that a developer reads before touching a file: which layer (`domain` / `application` / `infrastructure`, with package) the change belongs in, what its classes are named, and which standards actually apply — `quarkus-hexagonal-core` always, the six satellites only where the story's scope calls for them. The consumer is whoever implements the story next (often Marcus or another dev agent) without you in the room, so a note that only makes sense with your reasoning attached has failed.
12
-
13
- Find the freshly created epics/stories in `{planning_artifacts}` — the ones `bmad-create-epics-and-stories` just produced, or whichever batch the user points at. For each story, work out what it touches from its acceptance criteria, not its title alone, then write the notes section grounded in the specific rule from the specific standard, not a paraphrase of the whole skill. Append the section; never edit the story's existing text, and never re-append to a story whose notes are already current for its content.
14
-
15
- When a story's scope doesn't map cleanly to any layer or standard — a pure UI change, an ops task, something the seven standards genuinely don't cover — say so in the notes as an open question rather than forcing a placement. That is a correct outcome, not a gap in your review.
16
-
17
- When you're done, report the batch: how many stories got notes, which ones raised an open question, and hand off — sprint planning is next.