@hellraisercenobit/ai-engineering-gate 0.8.0

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 (100) hide show
  1. package/README.md +82 -0
  2. package/agents/design-pattern-reviewer.md +40 -0
  3. package/agents/modern-typescript-reviewer.md +24 -0
  4. package/agents/testing-pattern-reviewer.md +23 -0
  5. package/bin/ai-engineering-gate.mjs +6 -0
  6. package/contracts/axi/axi-standard.md +273 -0
  7. package/contracts/axi/pin.json +36 -0
  8. package/contracts/members.json +58 -0
  9. package/contracts/schemas/arbitration.schema.json +45 -0
  10. package/contracts/schemas/decision-envelope.schema.json +215 -0
  11. package/contracts/schemas/declaration.schema.json +190 -0
  12. package/contracts/schemas/dispute.schema.json +67 -0
  13. package/contracts/schemas/evidence-append.schema.json +72 -0
  14. package/contracts/schemas/gate-output.schema.json +296 -0
  15. package/contracts/schemas/marker.schema.json +75 -0
  16. package/contracts/schemas/review-envelope.schema.json +378 -0
  17. package/contracts/schemas/verdict-record.schema.json +191 -0
  18. package/contracts/suite-contract.md +270 -0
  19. package/dist/ai-engineering-gate.mjs +2762 -0
  20. package/package.json +47 -0
  21. package/skills/engineering/review-design-patterns/LICENSE +6 -0
  22. package/skills/engineering/review-design-patterns/SKILL.md +196 -0
  23. package/skills/engineering/review-design-patterns/agents/openai.yaml +3 -0
  24. package/skills/engineering/review-design-patterns/references/smell-signatures.md +117 -0
  25. package/skills/engineering/review-design-patterns/review-design-patterns.yaml +25 -0
  26. package/skills/engineering/review-modern-typescript/LICENSE +21 -0
  27. package/skills/engineering/review-modern-typescript/SKILL.md +88 -0
  28. package/skills/engineering/review-modern-typescript/agents/openai.yaml +3 -0
  29. package/skills/engineering/review-modern-typescript/references/smell-signatures.md +36 -0
  30. package/skills/engineering/review-modern-typescript/review-modern-typescript.yaml +22 -0
  31. package/skills/engineering/review-testing-patterns/SKILL.md +108 -0
  32. package/skills/engineering/review-testing-patterns/agents/openai.yaml +4 -0
  33. package/skills/engineering/review-testing-patterns/references/compiler-profile.mjs +103 -0
  34. package/skills/engineering/review-testing-patterns/references/smell-signatures.md +30 -0
  35. package/skills/engineering/review-testing-patterns/review-testing-patterns.yaml +23 -0
  36. package/skills/engineering/transpose-design-patterns/LICENSE +6 -0
  37. package/skills/engineering/transpose-design-patterns/SKILL.md +231 -0
  38. package/skills/engineering/transpose-design-patterns/agents/openai.yaml +3 -0
  39. package/skills/engineering/transpose-design-patterns/references/arbitration.schema.json +45 -0
  40. package/skills/engineering/transpose-design-patterns/references/decision-envelope.schema.json +215 -0
  41. package/skills/engineering/transpose-design-patterns/references/declaration.schema.json +190 -0
  42. package/skills/engineering/transpose-design-patterns/references/design-decision-record.schema.json +731 -0
  43. package/skills/engineering/transpose-design-patterns/references/dispute.schema.json +67 -0
  44. package/skills/engineering/transpose-design-patterns/references/evidence-append.schema.json +72 -0
  45. package/skills/engineering/transpose-design-patterns/references/marker.schema.json +75 -0
  46. package/skills/engineering/transpose-design-patterns/references/pattern-catalog.md +305 -0
  47. package/skills/engineering/transpose-design-patterns/references/record.example.json +120 -0
  48. package/skills/engineering/transpose-design-patterns/references/review-envelope.schema.json +378 -0
  49. package/skills/engineering/transpose-design-patterns/references/suite-contract.md +270 -0
  50. package/skills/engineering/transpose-design-patterns/references/transpose-angular.md +224 -0
  51. package/skills/engineering/transpose-design-patterns/references/transpose-php.md +561 -0
  52. package/skills/engineering/transpose-design-patterns/references/transpose-quarkus.md +283 -0
  53. package/skills/engineering/transpose-design-patterns/references/transpose-react.md +256 -0
  54. package/skills/engineering/transpose-design-patterns/references/transpose-vanilla.md +204 -0
  55. package/skills/engineering/transpose-design-patterns/references/transpose-vue.md +234 -0
  56. package/skills/engineering/transpose-design-patterns/transpose-design-patterns.yaml +24 -0
  57. package/skills/engineering/transpose-modern-typescript/LICENSE +21 -0
  58. package/skills/engineering/transpose-modern-typescript/SKILL.md +90 -0
  59. package/skills/engineering/transpose-modern-typescript/agents/openai.yaml +3 -0
  60. package/skills/engineering/transpose-modern-typescript/references/arbitration.schema.json +45 -0
  61. package/skills/engineering/transpose-modern-typescript/references/catalog.md +38 -0
  62. package/skills/engineering/transpose-modern-typescript/references/collections.md +50 -0
  63. package/skills/engineering/transpose-modern-typescript/references/compatibility.md +61 -0
  64. package/skills/engineering/transpose-modern-typescript/references/decision-envelope.schema.json +215 -0
  65. package/skills/engineering/transpose-modern-typescript/references/decision-record.schema.json +830 -0
  66. package/skills/engineering/transpose-modern-typescript/references/declaration.schema.json +190 -0
  67. package/skills/engineering/transpose-modern-typescript/references/dispute.schema.json +67 -0
  68. package/skills/engineering/transpose-modern-typescript/references/evidence-append.schema.json +72 -0
  69. package/skills/engineering/transpose-modern-typescript/references/idioms.md +74 -0
  70. package/skills/engineering/transpose-modern-typescript/references/marker.schema.json +75 -0
  71. package/skills/engineering/transpose-modern-typescript/references/platform.md +147 -0
  72. package/skills/engineering/transpose-modern-typescript/references/record.example.json +172 -0
  73. package/skills/engineering/transpose-modern-typescript/references/record.md +37 -0
  74. package/skills/engineering/transpose-modern-typescript/references/review-envelope.schema.json +378 -0
  75. package/skills/engineering/transpose-modern-typescript/references/suite-contract.md +270 -0
  76. package/skills/engineering/transpose-modern-typescript/references/types.md +97 -0
  77. package/skills/engineering/transpose-modern-typescript/transpose-modern-typescript.yaml +22 -0
  78. package/skills/engineering/transpose-testing-patterns/SKILL.md +96 -0
  79. package/skills/engineering/transpose-testing-patterns/agents/openai.yaml +4 -0
  80. package/skills/engineering/transpose-testing-patterns/references/arbitration.schema.json +45 -0
  81. package/skills/engineering/transpose-testing-patterns/references/catalog.md +218 -0
  82. package/skills/engineering/transpose-testing-patterns/references/decision-envelope.schema.json +215 -0
  83. package/skills/engineering/transpose-testing-patterns/references/decision-record.schema.json +857 -0
  84. package/skills/engineering/transpose-testing-patterns/references/declaration.schema.json +190 -0
  85. package/skills/engineering/transpose-testing-patterns/references/detect-adapter.mjs +98 -0
  86. package/skills/engineering/transpose-testing-patterns/references/dispute.schema.json +67 -0
  87. package/skills/engineering/transpose-testing-patterns/references/doubles-data.md +45 -0
  88. package/skills/engineering/transpose-testing-patterns/references/evidence-append.schema.json +72 -0
  89. package/skills/engineering/transpose-testing-patterns/references/journal-event.schema.json +115 -0
  90. package/skills/engineering/transpose-testing-patterns/references/marker.schema.json +75 -0
  91. package/skills/engineering/transpose-testing-patterns/references/record.example.json +230 -0
  92. package/skills/engineering/transpose-testing-patterns/references/record.md +25 -0
  93. package/skills/engineering/transpose-testing-patterns/references/review-envelope.schema.json +378 -0
  94. package/skills/engineering/transpose-testing-patterns/references/suite-contract.md +270 -0
  95. package/skills/engineering/transpose-testing-patterns/references/tdd.md +62 -0
  96. package/skills/engineering/transpose-testing-patterns/references/transpose-codeception.md +84 -0
  97. package/skills/engineering/transpose-testing-patterns/references/transpose-karma-jasmine-angular.md +73 -0
  98. package/skills/engineering/transpose-testing-patterns/references/transpose-vitest.md +111 -0
  99. package/skills/engineering/transpose-testing-patterns/references/typescript.md +42 -0
  100. package/skills/engineering/transpose-testing-patterns/transpose-testing-patterns.yaml +23 -0
@@ -0,0 +1,283 @@
1
+ # Quarkus Transposition — Design Patterns (Java 21+, 2026)
2
+
3
+ Generic, portable Quarkus wiring for the patterns in [`pattern-catalog.md`](pattern-catalog.md).
4
+ Framework-specific only — keep business logic framework-agnostic in the catalog. Examples use
5
+ throwaway domains (user, cart, payment, order); substitute your own types. This guide states **common**
6
+ Quarkus practice (Quarkus 3.x, Java 21+), not any single project's house rules.
7
+
8
+ ## Quarkus building blocks
9
+
10
+ The transpositions below lean on a few modern-Quarkus defaults:
11
+
12
+ - **Build-time CDI (ArC)** — wiring is resolved at build time (no runtime classpath scanning, no Portable
13
+ Extensions). Prefer **constructor injection** (final fields, testable; `@Inject` is optional for a
14
+ single constructor) over field injection.
15
+ - **`@ApplicationScoped`** for almost every bean — a _normal_ scope: a client proxy is injected and the
16
+ instance is created lazily. `@Singleton` is a _pseudo-scope_ (no proxy, eager, can't break cycles);
17
+ reach for it only to shave proxy indirection.
18
+ - **Quarkus REST** (`quarkus-rest` + `quarkus-rest-jackson`) — Jakarta REST annotations (`@Path`, `@GET`).
19
+ It is the current name for RESTEasy Reactive; RESTEasy Classic (`quarkus-resteasy`) is legacy. A method
20
+ runs on the Vert.x event loop unless it returns a plain value / is `@Blocking` / `@Transactional` /
21
+ `@RunOnVirtualThread` (then a worker or virtual thread).
22
+ - **Records** for immutable DTOs and value types; **sealed interfaces + pattern-matching `switch`** for
23
+ closed discriminated unions (compiler-checked exhaustiveness, no `default`).
24
+ - **`@ConfigMapping`** interface for typed, grouped, validated config (over scattered `@ConfigProperty`).
25
+ - **Native-image aware** — build-time wiring; payloads reached only by reflection need
26
+ `@RegisterForReflection`, beans resolved only by programmatic lookup need `@Unremovable`.
27
+
28
+ ```java
29
+ @ApplicationScoped
30
+ public class CheckoutService { // proxied, lazy, one shared instance
31
+ private final OrderRepository orders; // constructor injection, final field
32
+ CheckoutService(OrderRepository orders) { this.orders = orders; } // no @Inject needed
33
+ }
34
+ ```
35
+
36
+ ## Pattern → Quarkus wiring
37
+
38
+ ### Strategy → many beans of one interface, selected via CDI (no `switch`)
39
+
40
+ Each strategy is an `@ApplicationScoped` bean implementing the interface with a `supports()` / `key()`
41
+ method. Inject **`@All List<T>`** (immutable, `@Priority`-sorted) and pick by predicate — DI replaces the
42
+ `switch`. For a _closed_ variant set known at compile time, a sealed interface + pattern-matching `switch`
43
+ is the typed alternative.
44
+
45
+ ```java
46
+ interface PaymentStrategy { boolean supports(String method); void pay(long amount); }
47
+
48
+ @ApplicationScoped
49
+ class PaymentService {
50
+ private final List<PaymentStrategy> strategies;
51
+ PaymentService(@All List<PaymentStrategy> strategies) { this.strategies = strategies; }
52
+ void pay(String method, long amount) {
53
+ strategies.stream().filter(s -> s.supports(method)).findFirst()
54
+ .orElseThrow(() -> new IllegalArgumentException(method)).pay(amount);
55
+ }
56
+ }
57
+ ```
58
+
59
+ Disambiguate a one-of lookup with `@Identifier("…")` + `Instance<T>.select(…)` — **not** `@Named`
60
+ (`@Named` also implies `@Default`, causing ambiguity).
61
+
62
+ ### Registry → typed map built from the injected implementations
63
+
64
+ DI _is_ the registry: inject `@All List<T>` and index by the key each bean exposes. New implementations
65
+ are discovered automatically — no central `switch`, no hand-maintained provider map. Duplicate keys fail
66
+ fast when the registry is first used (it is `@ApplicationScoped`, so the constructor runs lazily; make it
67
+ `@Singleton`/`@Startup` if you want the check at boot).
68
+
69
+ ```java
70
+ interface Exporter { String format(); byte[] export(Report r); }
71
+
72
+ @ApplicationScoped
73
+ class ExporterRegistry {
74
+ private final Map<String, Exporter> byFormat;
75
+ ExporterRegistry(@All List<Exporter> exporters) {
76
+ this.byFormat = exporters.stream().collect(toUnmodifiableMap(Exporter::format, identity()));
77
+ }
78
+ Exporter forFormat(String f) {
79
+ return Optional.ofNullable(byFormat.get(f)).orElseThrow(() -> new NoSuchElementException(f));
80
+ }
81
+ }
82
+ ```
83
+
84
+ Gate a bean by config with `@LookupIfProperty` / `@LookupUnlessProperty` (then obtain it via `Instance<T>`).
85
+
86
+ ### Factory → CDI `@Produces` producer method
87
+
88
+ Isolate creation logic in a `@Produces` method when it is non-trivial, config-driven, or builds a
89
+ third-party type you can't annotate; return an abstraction. A `@Disposes` method cleans it up.
90
+
91
+ ```java
92
+ @ApplicationScoped
93
+ class GatewayFactory {
94
+ @Produces @ApplicationScoped @Identifier("primary")
95
+ PaymentGateway primary(GatewayConfig cfg) {
96
+ return new HttpPaymentGateway(cfg.url(), cfg.token()); // returns the abstraction
97
+ }
98
+ }
99
+ ```
100
+
101
+ Caveat: **interceptors are not woven on produced instances** — `@Transactional` / custom bindings on a
102
+ producer-returned object do nothing. If you need interception, make it an annotated bean class instead.
103
+
104
+ ### Command → distinct `@ApplicationScoped` use-case the resource delegates to
105
+
106
+ Encapsulate one action in a focused application-service bean; the Quarkus REST resource stays thin and
107
+ delegates. Put `@Transactional` on the use-case method (the unit of work), never on the resource or the
108
+ domain.
109
+
110
+ ```java
111
+ @ApplicationScoped
112
+ class PlaceOrder { // the use-case / Command
113
+ private final OrderRepository orders;
114
+ PlaceOrder(OrderRepository orders) { this.orders = orders; }
115
+ @Transactional
116
+ OrderId handle(PlaceOrderCommand cmd) { /* domain logic */ return orders.save(cmd.toOrder()); }
117
+ }
118
+
119
+ @Path("/orders")
120
+ class OrderResource {
121
+ private final PlaceOrder placeOrder;
122
+ OrderResource(PlaceOrder placeOrder) { this.placeOrder = placeOrder; }
123
+ @POST OrderId create(@Valid PlaceOrderCommand cmd) { return placeOrder.handle(cmd); }
124
+ }
125
+ ```
126
+
127
+ A Panache repository method or a resource method is **not automatically** the Command — when the action
128
+ must be reusable, composable, or unit-testable in isolation, keep it a distinct `@ApplicationScoped`
129
+ use-case bean the resource/repository **delegates** to. Full rule + layering: catalog → _Command_.
130
+
131
+ ### Adapter / DTO Mapping → records + pure `from`/`to` at the boundary
132
+
133
+ Map DTO ↔ domain/entity at the resource boundary; **never serialize JPA entities** (leaks internals,
134
+ breaks on lazy relations, bloats the native reflection surface). The catalog's canonical mapper is
135
+ TypeScript; the Java equivalent is a record with a static `from` (or, for large surfaces, MapStruct):
136
+
137
+ ```java
138
+ public record UserDto(String id, Instant createdAt) {
139
+ static UserDto from(User u) { return new UserDto(u.id(), u.createdAt()); }
140
+ }
141
+
142
+ // larger surfaces: MapStruct, CDI-injectable and reflection-free (native-safe).
143
+ // Use "jakarta-cdi" on Quarkus 3.x — plain "cdi" can emit javax.inject.@Inject, which ArC won't resolve.
144
+ @Mapper(componentModel = "jakarta-cdi")
145
+ interface UserMapper { UserDto toDto(User u); }
146
+ ```
147
+
148
+ Keep mappers and DTOs in the application/web layer — the domain must not depend on them.
149
+
150
+ ### Composition → CDI injection + interceptors + events (not inheritance)
151
+
152
+ Compose behavior through injected collaborators, **CDI interceptors** for cross-cutting concerns
153
+ (`@InterceptorBinding` + `@AroundInvoke`, ordered by `@Priority`; `@Transactional` is the built-in
154
+ example), and **CDI events** to decouple producers from consumers (`Event<T>` + `@Observes` /
155
+ `@ObservesAsync`). Model variants with sealed records, not class hierarchies.
156
+
157
+ ```java
158
+ @ApplicationScoped
159
+ class OrderService {
160
+ private final Event<OrderPlaced> placed;
161
+ OrderService(Event<OrderPlaced> placed) { this.placed = placed; }
162
+ void place(Order o) { /* … */ placed.fireAsync(new OrderPlaced(o.id())); }
163
+ }
164
+
165
+ @ApplicationScoped
166
+ class EmailNotifier {
167
+ void onOrder(@ObservesAsync OrderPlaced e) { /* offloaded to a worker pool */ }
168
+ }
169
+ ```
170
+
171
+ ### Singleton / shared state → `@ApplicationScoped` bean exposing read-only state
172
+
173
+ The common case for app-wide state: a single `@ApplicationScoped` instance with private state and a
174
+ read-only surface. Because that one instance is hit by **all request threads**, mutable fields are a data
175
+ race — guard them with container-managed `@Lock` (write-exclusive by default, `@Lock(READ)` for
176
+ concurrent reads) or `java.util.concurrent` / atomics.
177
+
178
+ ```java
179
+ @Lock
180
+ @ApplicationScoped
181
+ class FeatureFlags {
182
+ private final Map<String, Boolean> flags = new HashMap<>();
183
+ void set(String key, boolean on) { flags.put(key, on); } // exclusive
184
+ @Lock(value = Lock.Type.READ) boolean isEnabled(String key) { // concurrent reads
185
+ return flags.getOrDefault(key, false);
186
+ }
187
+ }
188
+ ```
189
+
190
+ ## Cross-cutting Quarkus practice
191
+
192
+ ### Keep resources thin
193
+
194
+ Quarkus REST resources orchestrate HTTP, bind/validate input, and delegate. Push logic and mapping into
195
+ `@ApplicationScoped` services and pure functions. (Resource classes are `@Singleton` by default — never
196
+ store per-request mutable state in their fields.)
197
+
198
+ ### Never block the event loop
199
+
200
+ Endpoint methods run on the Vert.x event loop. Blocking work (JDBC, blocking clients, filesystem) must
201
+ either return a Mutiny `Uni` / `Multi`, or be annotated `@Blocking` (worker thread) or
202
+ `@RunOnVirtualThread` (Java 21 virtual thread — the 2026 default for blocking I/O endpoints). Blocking the
203
+ loop stalls every concurrent request.
204
+
205
+ ```java
206
+ @GET @Path("/{id}") @RunOnVirtualThread // imperative blocking on a virtual thread
207
+ UserDto get(@RestPath String id) { return UserDto.from(users.findById(id)); }
208
+ ```
209
+
210
+ ### Reactive only at the async boundary
211
+
212
+ Keep Mutiny `Uni` / `Multi` at the edges (REST signatures, reactive clients/messaging); map DTO→domain in
213
+ the chain and convert to plain values inside imperative code. Never call `.await()` on the event-loop
214
+ thread.
215
+
216
+ ### Transactions at the service boundary
217
+
218
+ `@Transactional` (JTA) wraps the use-case method, making it atomic — not the domain, not the resource. On
219
+ the _reactive_ stack use `@WithTransaction` / `Panache.withTransaction(...)` (returns `Uni`); JTA does not
220
+ apply there.
221
+
222
+ ### Validate at the boundary
223
+
224
+ Annotate DTO fields with `jakarta.validation.constraints.*` and REST params with `@Valid` — Quarkus
225
+ auto-returns HTTP 400 with a violation report. Keep the rules as constraints / pure functions; inject
226
+ `Validator` for programmatic checks in services.
227
+
228
+ ### Persistence behind a repository
229
+
230
+ Prefer a `PanacheRepository<T>` (`@ApplicationScoped`) over the active-record entity for layered, mockable
231
+ code — it keeps query logic off the entity and behind a seam.
232
+
233
+ ### Stay native-image-friendly
234
+
235
+ Wire through build-time CDI, not `Class.forName` / classpath scanning. Add `@RegisterForReflection` to
236
+ payloads reached only by reflection, and `@Unremovable` to beans resolved only via programmatic lookup
237
+ (else `quarkus.arc.remove-unused-beans` drops them). Use the tracing agent in integration tests to find gaps.
238
+
239
+ ### Testing
240
+
241
+ `@QuarkusTest` + REST Assured for app / endpoint tests; `@QuarkusComponentTest` to test one bean in
242
+ isolation; `@InjectMock` / `@InjectSpy` (`quarkus-junit5-mockito`) to replace collaborators;
243
+ `@TestHTTPEndpoint` to resolve paths from the resource; `@QuarkusIntegrationTest` to exercise the packaged
244
+ jar / native image.
245
+
246
+ ## Quarkus Decision Matrix
247
+
248
+ | Problem | Recommended Solution |
249
+ | -------------------------------------- | -------------------------------------------------------------------------- |
250
+ | Shared app state / service | `@ApplicationScoped` bean (read-only surface; `@Lock` if mutable) |
251
+ | Singleton without proxy / lazy needs | `@Singleton` (pseudo-scope, eager) |
252
+ | Depend on an abstraction | inject the interface via the constructor |
253
+ | Interchangeable behaviors | Strategy (`@All List<T>` + `supports()`, no switch) |
254
+ | Closed, finite variant set | sealed interface + pattern-matching `switch` |
255
+ | Plugin / extension system | Registry (map built from `@All List<T>`) |
256
+ | Object creation with logic | Factory (`@Produces` producer method) |
257
+ | Reusable action | Command (distinct `@ApplicationScoped` use-case the resource delegates to) |
258
+ | API ↔ domain shape mismatch | record DTO + static `from` / MapStruct (`componentModel="jakarta-cdi"`) |
259
+ | Cross-cutting behavior | Composition (CDI interceptor / events; no inheritance) |
260
+ | Disambiguate multiple beans | `@Identifier` + `Instance.select` (not `@Named`) |
261
+ | Config-gated bean | `@LookupIfProperty` / `@LookupUnlessProperty` |
262
+ | Typed config | `@ConfigMapping` interface |
263
+ | Runtime validation | Bean Validation (`@Valid` + `jakarta.validation.constraints.*`) |
264
+ | Unit of work / atomicity | `@Transactional` on the service method (reactive: `@WithTransaction`) |
265
+ | Blocking I/O endpoint | `@RunOnVirtualThread` (or `@Blocking`) |
266
+ | Async / streaming boundary | Mutiny `Uni` / `Multi` |
267
+ | Persistence access | `PanacheRepository<T>` (repository over active-record) |
268
+ | Reflectively-reached payload (native) | `@RegisterForReflection` |
269
+ | Bean kept only via programmatic lookup | `@Unremovable` |
270
+
271
+ ## Anti-Patterns to Avoid
272
+
273
+ - field injection instead of constructor injection (loses final fields, testability)
274
+ - business logic in Quarkus REST / JAX-RS resources — delegate to an `@ApplicationScoped` service
275
+ - blocking the IO / event-loop thread without `@Blocking` / `@RunOnVirtualThread` — stalls all requests
276
+ - mutable shared state in an `@ApplicationScoped` bean without `@Lock` / atomics — a data race across threads
277
+ - serializing JPA entities over HTTP / entities leaking across layers — map to record DTOs at the boundary
278
+ - `switch` on a type tag instead of CDI selection (open sets) or an exhaustive sealed `switch` (closed sets)
279
+ - `@Named` for internal bean disambiguation (implicit `@Default` clash) — use `@Identifier`
280
+ - `@Transactional` on a domain entity or a producer-returned instance (not woven) — put it on the service
281
+ - runtime reflection / `Class.forName` / classpath scanning that breaks native image — wire at build time
282
+ - deep inheritance hierarchies — prefer composition + CDI + sealed records
283
+ - a Command merged into a resource/repository method when it must be reusable — keep it a distinct use-case the resource delegates to
@@ -0,0 +1,256 @@
1
+ # React Transposition — Design Patterns (TypeScript 2026)
2
+
3
+ Generic, portable React wiring for the patterns in [`pattern-catalog.md`](pattern-catalog.md).
4
+ Framework-specific only — keep business logic framework-agnostic in the catalog. Examples use
5
+ throwaway domains (user, cart, payment, order); substitute your own types. This guide states **common**
6
+ React practice, not any single project's house rules.
7
+
8
+ ## React building blocks
9
+
10
+ The transpositions below lean on a few modern-React defaults (React 19+):
11
+
12
+ - **Function components + hooks** only — no class components, no `forwardRef` (in React 19 `ref` is a
13
+ normal prop).
14
+ - **`<Context>` as its own provider** — `<ThemeContext value={…}>`, not `<ThemeContext.Provider>`.
15
+ Consume with a typed custom hook (`useTheme()`), never raw `useContext` scattered in views.
16
+ - **The React Compiler does the memoization.** With it enabled, drop hand-written `useMemo` /
17
+ `useCallback` / `React.memo` — keep them only where the compiler is off or you have a measured need.
18
+ - **Derive during render**, hold only source-of-truth state in `useState` / `useReducer`. Effects are
19
+ for synchronizing with _external_ systems, not for computing values from props/state.
20
+ - **External shared state lives in a store** (Zustand), not in a high-frequency Context. Context is for
21
+ dependency injection of stable services.
22
+ - **Strong typing**: typed props, discriminated unions for variants, `as const satisfies` for lookup
23
+ maps, branded ids.
24
+
25
+ ```tsx
26
+ const ThemeContext = createContext<Theme | null>(null);
27
+
28
+ export function useTheme(): Theme {
29
+ const theme = use(ThemeContext); // React 19 reader — callable conditionally, unlike useContext
30
+ if (!theme) throw new Error('useTheme must be used inside <ThemeContext>');
31
+ return theme;
32
+ }
33
+ ```
34
+
35
+ ## Pattern → React wiring
36
+
37
+ ### Strategy → typed lookup map, resolved by key
38
+
39
+ Stateless strategies in an `as const satisfies` record; pick by key — no `switch`.
40
+
41
+ ```ts
42
+ export interface PaymentStrategy {
43
+ pay(amount: number): Promise<void>;
44
+ }
45
+
46
+ export const paymentStrategies = {
47
+ stripe: stripeStrategy,
48
+ paypal: paypalStrategy,
49
+ } as const satisfies Record<string, PaymentStrategy>;
50
+
51
+ export type PaymentMethod = keyof typeof paymentStrategies;
52
+
53
+ const strategy = paymentStrategies[method]; // typed, exhaustive, no switch
54
+ ```
55
+
56
+ ### Registry → self-registering map (validated when dynamic)
57
+
58
+ Plugins self-register into a module-level map; no central switch. Validate runtime/external plugins.
59
+
60
+ ```ts
61
+ const registry = new Map<string, Plugin>();
62
+ export const registerPlugin = (p: Plugin): void => void registry.set(p.key, p);
63
+ export const getPlugin = (key: string): Plugin | undefined => registry.get(key);
64
+ // dynamic plugins: validate the shape with zod/valibot before registering
65
+ ```
66
+
67
+ To expose a registry to the tree, provide it once via Context and read it through a custom hook —
68
+ never reach into module singletons from components when the registry is request/tenant-scoped.
69
+
70
+ ### Factory → factory function returning an abstraction
71
+
72
+ Isolate creation logic; return an interface, not a concrete class. A custom hook can be a factory of
73
+ React-bound values.
74
+
75
+ ```ts
76
+ export const createLogger = (env: Env): Logger => (env.production ? new RemoteLogger() : new ConsoleLogger());
77
+ ```
78
+
79
+ ### Command → framework-agnostic use-case, optionally wrapped by an Action
80
+
81
+ Encapsulate one action as a plain (async) function in a framework-agnostic module. Invoke it from an
82
+ event handler, a store action, or — for form submission — a React 19 **Action**.
83
+
84
+ ```ts
85
+ // application/use-case — no React imports
86
+ export const checkoutCart = (cart: Cart): Promise<Order> => orderApi.place(cart);
87
+ ```
88
+
89
+ ```tsx
90
+ // component: wrap the use-case in an Action for pending/optimistic state
91
+ const [order, submit, isPending] = useActionState<Order | null, FormData>(
92
+ (_prev, formData) => checkoutCart(cartFromForm(formData)),
93
+ null,
94
+ );
95
+ ```
96
+
97
+ A Zustand action is **not automatically** the Command — keep the action a distinct framework-agnostic use-case (function) the store action **delegates** to. Full rule + layering: catalog → _Command_.
98
+
99
+ ### Adapter / DTO Mapping → pure `from`/`to` at the data boundary
100
+
101
+ Map DTO ↔ domain inside the api client / query function; never leak DTOs into components or JSX.
102
+
103
+ ```ts
104
+ // UserDto / User / userFromDto: see catalog -> Adapter / DTO Mapping
105
+ export const getUser = async (id: string): Promise<User> =>
106
+ userFromDto(await api.get<UserDto>(`/api/users/${id}`));
107
+ ```
108
+
109
+ ### Composition → custom hooks + children/slots
110
+
111
+ Compose behavior through custom hooks and component composition (children, slot props) — not
112
+ inheritance, not mixins, not deep HOC stacks.
113
+
114
+ ```tsx
115
+ function useTooltip(ref: RefObject<HTMLElement | null>) {
116
+ /* attach/detach behavior, returns state */
117
+ }
118
+
119
+ function Card({ media, children }: { media?: ReactNode; children: ReactNode }) {
120
+ return (
121
+ <section>
122
+ {media}
123
+ {children}
124
+ </section>
125
+ ); // behavior composed via hooks + slots, no base class
126
+ }
127
+ ```
128
+
129
+ ### Singleton / shared state → a Zustand store exposing state + actions
130
+
131
+ The common case for app-wide state. Keep the shape minimal; mutate only through actions; **select
132
+ narrowly** so components re-render on the slices they read. Use `useShallow` for object/array selections
133
+ and derive in selectors (or in actions) rather than duplicating derived state.
134
+
135
+ ```ts
136
+ import { create } from 'zustand';
137
+ import { useShallow } from 'zustand/react/shallow';
138
+
139
+ interface CartState {
140
+ items: readonly CartItem[];
141
+ add: (item: CartItem) => void;
142
+ }
143
+
144
+ export const useCartStore = create<CartState>((set) => ({
145
+ items: [],
146
+ add: (item) => set((s) => ({ items: [...s.items, item] })),
147
+ }));
148
+
149
+ // consume — narrow selector; derived value computed in the selector
150
+ const total = useCartStore((s) => s.items.reduce((sum, i) => sum + i.price, 0));
151
+ const { items, add } = useCartStore(useShallow((s) => ({ items: s.items, add: s.add })));
152
+ ```
153
+
154
+ For app-wide _services_ (not reactive state), inject via Context + a typed hook. Reserve Context for
155
+ stable dependencies; route high-frequency state through the store to avoid re-rendering the subtree.
156
+
157
+ ## Cross-cutting React practice
158
+
159
+ ### Derive during render, not in effects
160
+
161
+ Compute values from props/state inline (the compiler memoizes). Do **not** mirror props into state with
162
+ `useEffect` + `setState`.
163
+
164
+ ```tsx
165
+ // Bad: const [full, setFull] = useState(''); useEffect(() => setFull(`${first} ${last}`), [first, last]);
166
+ // Good:
167
+ const fullName = `${first} ${last}`;
168
+ ```
169
+
170
+ ### Effects only at external boundaries
171
+
172
+ `useEffect` is for synchronizing with systems outside React (subscriptions, timers, non-React widgets,
173
+ manual DOM). Always return a cleanup. Event-response logic belongs in the event handler, not an effect.
174
+
175
+ ```tsx
176
+ useEffect(() => {
177
+ const sub = socket.subscribe(onMessage);
178
+ return () => sub.unsubscribe();
179
+ }, [socket]);
180
+ ```
181
+
182
+ ### Data fetching at the edge
183
+
184
+ Read promises with `use()` under `<Suspense>`, or use a cache (TanStack Query) for client data; fetch
185
+ on the server with Server Components / server functions where the framework supports them. Map DTO→domain
186
+ in the fetcher. Don't fetch in `useEffect` for primary data in new code.
187
+
188
+ ### Let the compiler memoize
189
+
190
+ With the React Compiler enabled, do not hand-roll `useMemo` / `useCallback` / `memo`. Add them back only
191
+ when the compiler is disabled for a file or profiling proves a hot path.
192
+
193
+ ### Keep components thin
194
+
195
+ Components render UI, bind state, dispatch actions. Push logic and mapping into hooks, the store, and
196
+ pure framework-agnostic functions.
197
+
198
+ ### Forms via Actions
199
+
200
+ Prefer React 19 form **Actions** — `useActionState` for submit + result, `useFormStatus` for pending
201
+ UI, `useOptimistic` for optimistic updates. Keep validation rules in pure functions (zod/valibot) the
202
+ Action just calls. Controlled inputs remain fine for small, interactive forms.
203
+
204
+ ### `ref` is a prop
205
+
206
+ Pass `ref` directly to function components (React 19) — do **not** write `forwardRef` in new code.
207
+
208
+ ```tsx
209
+ function TextField({ ref, ...props }: { ref?: Ref<HTMLInputElement> } & InputProps) {
210
+ return <input ref={ref} {...props} />;
211
+ }
212
+ ```
213
+
214
+ ### JSX SRP
215
+
216
+ No business logic in JSX. Move ternaries, multi-condition branches, and data-transforming calls into
217
+ derived consts (or selectors).
218
+
219
+ ```tsx
220
+ // Bad: {user.isActive ? 'Active' : 'Inactive'}
221
+ const statusLabel = user.isActive ? 'Active' : 'Inactive';
222
+ // JSX: {statusLabel}
223
+ ```
224
+
225
+ ## React Decision Matrix
226
+
227
+ | Problem | Recommended Solution |
228
+ | ----------------------------- | ------------------------------------------------------------ |
229
+ | Local component state | useState / useReducer |
230
+ | Derived value | compute during render (compiler memoizes) |
231
+ | Shared app state | Zustand store + narrow selectors / useShallow |
232
+ | Dependency injection | Context + a typed custom hook (`<Context value>`) |
233
+ | Interchangeable behaviors | Strategy (`as const satisfies` lookup map) |
234
+ | Plugin / extension system | Registry (self-registering Map, zod for dynamic) |
235
+ | Object creation with logic | Factory (factory function / hook returning an abstraction) |
236
+ | Reusable action | Command (framework-agnostic use-case, store/Action delegates) |
237
+ | API ↔ domain shape mismatch | Adapter — fromDto / toDto in the fetcher |
238
+ | Cross-cutting behavior | Composition (custom hooks + children/slots) |
239
+ | Data fetching | use() + Suspense / TanStack Query / Server Components |
240
+ | Form | React 19 Actions (useActionState / useFormStatus / useOptimistic) |
241
+ | External system sync | useEffect with cleanup |
242
+ | Forwarding a ref | `ref` as a normal prop (no forwardRef) |
243
+ | Memoization | React Compiler (no manual useMemo/useCallback) |
244
+
245
+ ## Anti-Patterns to Avoid
246
+
247
+ - class components; `forwardRef` in new code
248
+ - deriving state with `useEffect` + `setState`; storing derived data in `useState`
249
+ - fetching primary data in `useEffect` (use `use()` + Suspense / a query cache / the server)
250
+ - business logic in components; DTOs leaking into JSX; god hooks/components; string magic / `switch` on a type tag instead of an `as const satisfies` lookup map
251
+ - high-frequency global state in Context (re-renders the subtree) — use a Zustand store; reserve Context for DI
252
+ - selecting the whole store object without `useShallow` (re-renders on every change)
253
+ - hand-rolled `useMemo` / `useCallback` / `memo` noise when the React Compiler is enabled
254
+ - prop-drilling shared state deep instead of composition / a store
255
+ - a Command merged into a store action when it must be reusable — keep it a distinct use-case the action delegates to
256
+ - inheritance / mixins; `any`; magic strings; mutating props or store state outside actions