cc-codeconductor 0.2.1

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 (93) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +319 -0
  3. package/dist/index.js +12741 -0
  4. package/package.json +51 -0
  5. package/policy.yml +124 -0
  6. package/presets/claude/CLAUDE.md +598 -0
  7. package/presets/claude/commands/cc/feature.md +115 -0
  8. package/presets/claude/commands/cc/fix.md +121 -0
  9. package/presets/claude/commands/cc/refactor.md +148 -0
  10. package/presets/claude/commands/cc/review.md +126 -0
  11. package/presets/claude/commands/cc/tdd-cycle.md +226 -0
  12. package/presets/claude/commands/cc/test-plan.md +138 -0
  13. package/presets/claude/settings.json +37 -0
  14. package/presets/claude/skills/api-versioning/SKILL.md +389 -0
  15. package/presets/claude/skills/django-orm/SKILL.md +455 -0
  16. package/presets/claude/skills/django-testing/SKILL.md +409 -0
  17. package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
  18. package/presets/claude/skills/python/SKILL.md +606 -0
  19. package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
  20. package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
  21. package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
  22. package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
  23. package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
  24. package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
  25. package/presets/codex/AGENTS.md +883 -0
  26. package/presets/codex/README.md +102 -0
  27. package/presets/codex/skills/api-versioning/SKILL.md +389 -0
  28. package/presets/codex/skills/django-orm/SKILL.md +455 -0
  29. package/presets/codex/skills/django-testing/SKILL.md +409 -0
  30. package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
  31. package/presets/codex/skills/python/SKILL.md +606 -0
  32. package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
  33. package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
  34. package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
  35. package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
  36. package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
  37. package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
  38. package/presets/opencode/README.md +191 -0
  39. package/presets/opencode/agents/architect.md +133 -0
  40. package/presets/opencode/agents/docs.md +113 -0
  41. package/presets/opencode/agents/implementer.md +116 -0
  42. package/presets/opencode/agents/orchestrator.md +248 -0
  43. package/presets/opencode/agents/repo-explorer.md +119 -0
  44. package/presets/opencode/agents/reviewer.md +130 -0
  45. package/presets/opencode/agents/task-coach.md +103 -0
  46. package/presets/opencode/agents/tester.md +269 -0
  47. package/presets/opencode/commands/cc-feature.md +114 -0
  48. package/presets/opencode/commands/cc-fix.md +123 -0
  49. package/presets/opencode/commands/cc-refactor.md +148 -0
  50. package/presets/opencode/commands/cc-review.md +141 -0
  51. package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
  52. package/presets/opencode/commands/cc-test-plan.md +144 -0
  53. package/presets/opencode/opencode.jsonc +89 -0
  54. package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
  55. package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
  56. package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
  57. package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
  58. package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
  59. package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
  60. package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
  61. package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
  62. package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
  63. package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
  64. package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
  65. package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
  66. package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
  67. package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
  68. package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
  69. package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
  70. package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
  71. package/presets/opencode/skills/astro/SKILL.md +313 -0
  72. package/presets/opencode/skills/code-review/SKILL.md +202 -0
  73. package/presets/opencode/skills/django-orm/SKILL.md +455 -0
  74. package/presets/opencode/skills/django-testing/SKILL.md +408 -0
  75. package/presets/opencode/skills/django-uv/SKILL.md +400 -0
  76. package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
  77. package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
  78. package/presets/opencode/skills/python/SKILL.md +605 -0
  79. package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
  80. package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
  81. package/presets/opencode/skills/security/SKILL.md +376 -0
  82. package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
  83. package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
  84. package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
  85. package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
  86. package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
  87. package/src/presets/council/council.yml +59 -0
  88. package/src/presets/manifests/claude.yml +23 -0
  89. package/src/presets/manifests/codex.yml +12 -0
  90. package/src/presets/manifests/opencode.yml +18 -0
  91. package/src/presets/models/claude.yml +37 -0
  92. package/src/presets/models/codex.yml +38 -0
  93. package/src/presets/models/opencode.yml +37 -0
@@ -0,0 +1,558 @@
1
+ ---
2
+ id: spring-boot-feature
3
+ version: 1.0.0
4
+ name: Spring Boot Feature Creation
5
+ description:
6
+ Guides the creation of complete Spring Boot features following a structured,
7
+ layer-by-layer workflow entity, repository, service, controller, and tests.
8
+ Applies Kotlin idioms, Bean Validation, and MockK test patterns throughout.
9
+
10
+ compatibility:
11
+ tools: [claude, codex, opencode]
12
+ stacks:
13
+ languages: [kotlin]
14
+ frameworks: [spring-boot, spring-mvc, spring-data-jpa, mockk, junit5]
15
+ databases: [postgresql, h2]
16
+
17
+ risk:
18
+ level: medium
19
+ can_execute_shell: false
20
+ can_modify_files: true
21
+ requires_network: false
22
+
23
+ inputs:
24
+ - feature description
25
+ - existing domain context (if any)
26
+ - build.gradle.kts
27
+ - application.yml
28
+
29
+ outputs:
30
+ - domain entity
31
+ - JPA repository interface
32
+ - service class with business logic
33
+ - REST controller with validations
34
+ - request/response DTOs
35
+ - unit tests (MockK)
36
+ - controller integration tests (MockMvc)
37
+
38
+ quality:
39
+ reviewed_by: codeconductor-core
40
+ version: 0.1.0
41
+ ---
42
+
43
+ # Spring Boot Feature Creation
44
+
45
+ When asked to create a feature, follow these steps **in order**. Do not skip
46
+ steps or combine layers. Each layer has a single responsibility.
47
+
48
+ **When to ask questions:** Only ask when there is genuine ambiguity in business
49
+ logic — for example, what happens when a duplicate is found, or whether soft
50
+ delete is required. Do not ask about technical choices (naming, package
51
+ structure, framework configuration) — apply the conventions in this skill.
52
+
53
+ ---
54
+
55
+ ## Step 1 — Entity + Repository
56
+
57
+ Create the JPA entity and its repository before any other layer.
58
+
59
+ ### Entity
60
+
61
+ ```kotlin
62
+ @Entity
63
+ @Table(
64
+ name = "orders",
65
+ indexes = [Index(columnList = "customer_id")]
66
+ )
67
+ class Order(
68
+ @Id
69
+ @GeneratedValue(strategy = GenerationType.UUID)
70
+ val id: UUID = UUID.randomUUID(),
71
+
72
+ @Column(name = "customer_id", nullable = false)
73
+ val customerId: UUID,
74
+
75
+ @Column(name = "status", nullable = false)
76
+ @Enumerated(EnumType.STRING)
77
+ var status: OrderStatus = OrderStatus.PENDING,
78
+
79
+ @Column(name = "total_amount", nullable = false)
80
+ var totalAmount: BigDecimal,
81
+
82
+ @Column(name = "created_at", nullable = false, updatable = false)
83
+ @CreatedDate
84
+ val createdAt: Instant = Instant.now(),
85
+
86
+ @Column(name = "updated_at", nullable = false)
87
+ @LastModifiedDate
88
+ var updatedAt: Instant = Instant.now()
89
+ )
90
+
91
+ enum class OrderStatus { PENDING, CONFIRMED, CANCELLED }
92
+ ```
93
+
94
+ Rules:
95
+
96
+ - `@Table` with explicit `name` — never rely on inferred table names
97
+ - `@Column(nullable = false)` always explicit for non-nullable fields
98
+ - UUID primary keys with `GenerationType.UUID`
99
+ - Enums stored as `STRING`, not `ORDINAL`
100
+ - Include `createdAt` and `updatedAt` on every entity
101
+ - Never use `data class` for JPA entities — Hibernate requires a no-arg
102
+ constructor and mutable state; use `class`
103
+ - `equals`/`hashCode` based on `id` only, or omit (reference equality is safe
104
+ for JPA entities when managed by the same `EntityManager`)
105
+
106
+ ### Repository
107
+
108
+ ```kotlin
109
+ @Repository
110
+ interface OrderRepository : JpaRepository<Order, UUID> {
111
+ fun findByCustomerId(customerId: UUID): List<Order>
112
+ fun findByCustomerIdAndStatus(customerId: UUID, status: OrderStatus): List<Order>
113
+ fun existsByCustomerIdAndStatus(customerId: UUID, status: OrderStatus): Boolean
114
+
115
+ @Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt < :before")
116
+ fun findExpiredPending(
117
+ @Param("status") status: OrderStatus,
118
+ @Param("before") before: Instant
119
+ ): List<Order>
120
+ }
121
+ ```
122
+
123
+ Rules:
124
+
125
+ - Extend `JpaRepository<Entity, IdType>`
126
+ - Use derived query method names for simple conditions
127
+ - Use `@Query` with JPQL for joins, multi-condition queries, or aggregations
128
+ - Use native SQL only when JPQL cannot express it — add `nativeQuery = true`
129
+ - Never add `@Transactional` to repository methods — Spring Data handles it
130
+
131
+ ---
132
+
133
+ ## Step 2 — Service
134
+
135
+ The service is the single owner of all business logic. No logic in controllers,
136
+ no logic in repositories.
137
+
138
+ ```kotlin
139
+ @Service
140
+ @Transactional
141
+ class OrderService(
142
+ private val orderRepository: OrderRepository,
143
+ private val customerRepository: CustomerRepository
144
+ ) {
145
+
146
+ @Transactional(readOnly = true)
147
+ fun getById(id: UUID): Order =
148
+ orderRepository.findById(id).orElseThrow { OrderNotFoundException(id) }
149
+
150
+ @Transactional(readOnly = true)
151
+ fun listByCustomer(customerId: UUID): List<Order> =
152
+ orderRepository.findByCustomerId(customerId)
153
+
154
+ fun create(customerId: UUID, request: CreateOrderRequest): Order {
155
+ if (!customerRepository.existsById(customerId)) {
156
+ throw CustomerNotFoundException(customerId)
157
+ }
158
+ val order = Order(
159
+ customerId = customerId,
160
+ totalAmount = request.totalAmount
161
+ )
162
+ return orderRepository.save(order)
163
+ }
164
+
165
+ fun cancel(id: UUID): Order {
166
+ val order = getById(id)
167
+ if (order.status == OrderStatus.CANCELLED) {
168
+ throw OrderAlreadyCancelledException(id)
169
+ }
170
+ order.status = OrderStatus.CANCELLED
171
+ return orderRepository.save(order)
172
+ }
173
+ }
174
+ ```
175
+
176
+ Rules:
177
+
178
+ - `@Transactional` at class level — applies to all public methods
179
+ - `readOnly = true` on read-only methods — prevents dirty checking, faster
180
+ - Never put `@Transactional` on private methods — Spring proxies cannot
181
+ intercept them
182
+ - Throw typed domain exceptions — never `RuntimeException` or `Exception`
183
+ directly
184
+ - Never inject repositories into controllers — only services cross that boundary
185
+
186
+ ### Domain Exceptions
187
+
188
+ Define typed exceptions per feature:
189
+
190
+ ```kotlin
191
+ class OrderNotFoundException(id: UUID) :
192
+ RuntimeException("Order not found: $id")
193
+
194
+ class OrderAlreadyCancelledException(id: UUID) :
195
+ RuntimeException("Order already cancelled: $id")
196
+
197
+ class CustomerNotFoundException(id: UUID) :
198
+ RuntimeException("Customer not found: $id")
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Step 3 — Controller + DTOs
204
+
205
+ The controller translates HTTP into service calls. No business logic here.
206
+
207
+ ### DTOs
208
+
209
+ Define request and response data classes with Bean Validation annotations:
210
+
211
+ ```kotlin
212
+ data class CreateOrderRequest(
213
+ @field:NotNull(message = "totalAmount is required")
214
+ @field:DecimalMin(value = "0.01", message = "totalAmount must be greater than zero")
215
+ val totalAmount: BigDecimal
216
+ )
217
+
218
+ data class OrderResponse(
219
+ val id: UUID,
220
+ val customerId: UUID,
221
+ val status: String,
222
+ val totalAmount: BigDecimal,
223
+ val createdAt: Instant
224
+ )
225
+
226
+ fun Order.toResponse(): OrderResponse = OrderResponse(
227
+ id = id,
228
+ customerId = customerId,
229
+ status = status.name,
230
+ totalAmount = totalAmount,
231
+ createdAt = createdAt
232
+ )
233
+ ```
234
+
235
+ Rules for DTOs:
236
+
237
+ - Use `data class` — immutable, no `@Entity`
238
+ - Use `@field:` prefix on validation annotations — Kotlin applies annotations to
239
+ the property by default, not the backing field; Spring reads the field
240
+ - Define a `toResponse()` extension function on the entity — do not expose
241
+ entities directly in responses
242
+ - Never return `@Entity` from a controller method
243
+
244
+ ### Controller
245
+
246
+ ```kotlin
247
+ @RestController
248
+ @RequestMapping("/api/v1/customers/{customerId}/orders")
249
+ class OrderController(private val orderService: OrderService) {
250
+
251
+ @GetMapping
252
+ fun list(@PathVariable customerId: UUID): ResponseEntity<List<OrderResponse>> =
253
+ ResponseEntity.ok(orderService.listByCustomer(customerId).map { it.toResponse() })
254
+
255
+ @GetMapping("/{id}")
256
+ fun get(
257
+ @PathVariable customerId: UUID,
258
+ @PathVariable id: UUID
259
+ ): ResponseEntity<OrderResponse> =
260
+ ResponseEntity.ok(orderService.getById(id).toResponse())
261
+
262
+ @PostMapping
263
+ fun create(
264
+ @PathVariable customerId: UUID,
265
+ @Valid @RequestBody request: CreateOrderRequest
266
+ ): ResponseEntity<OrderResponse> {
267
+ val order = orderService.create(customerId, request)
268
+ return ResponseEntity.status(HttpStatus.CREATED).body(order.toResponse())
269
+ }
270
+
271
+ @DeleteMapping("/{id}/cancel")
272
+ fun cancel(
273
+ @PathVariable customerId: UUID,
274
+ @PathVariable id: UUID
275
+ ): ResponseEntity<OrderResponse> =
276
+ ResponseEntity.ok(orderService.cancel(id).toResponse())
277
+ }
278
+ ```
279
+
280
+ Rules:
281
+
282
+ - One controller per feature
283
+ - `@Valid` on every `@RequestBody` parameter
284
+ - Return `ResponseEntity<T>` — explicit HTTP status control
285
+ - No try/catch in controllers — use `GlobalExceptionHandler`
286
+ - Map entities to DTOs before returning — never return the entity itself
287
+
288
+ ### Global Exception Handler
289
+
290
+ Add new exception mappings to the existing `GlobalExceptionHandler`. Do not
291
+ create a second one.
292
+
293
+ ```kotlin
294
+ @ControllerAdvice
295
+ class GlobalExceptionHandler {
296
+
297
+ @ExceptionHandler(OrderNotFoundException::class)
298
+ fun handleOrderNotFound(ex: OrderNotFoundException): ResponseEntity<ErrorResponse> =
299
+ ResponseEntity.status(HttpStatus.NOT_FOUND)
300
+ .body(ErrorResponse(error = ex.message ?: "Order not found", code = "ORDER_NOT_FOUND"))
301
+
302
+ @ExceptionHandler(OrderAlreadyCancelledException::class)
303
+ fun handleAlreadyCancelled(ex: OrderAlreadyCancelledException): ResponseEntity<ErrorResponse> =
304
+ ResponseEntity.status(HttpStatus.CONFLICT)
305
+ .body(ErrorResponse(error = ex.message ?: "Order already cancelled", code = "ORDER_ALREADY_CANCELLED"))
306
+
307
+ @ExceptionHandler(MethodArgumentNotValidException::class)
308
+ fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
309
+ val message = ex.bindingResult.fieldErrors
310
+ .joinToString(", ") { "${it.field}: ${it.defaultMessage}" }
311
+ return ResponseEntity.badRequest()
312
+ .body(ErrorResponse(error = message, code = "VALIDATION_ERROR"))
313
+ }
314
+ }
315
+
316
+ data class ErrorResponse(val error: String, val code: String)
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Step 4 — Tests with MockK
322
+
323
+ Write three test classes per feature: unit tests for the service, controller
324
+ tests with MockMvc, and repository tests with `@DataJpaTest`.
325
+
326
+ ### Service Unit Tests (MockK)
327
+
328
+ ```kotlin
329
+ @ExtendWith(MockKExtension::class)
330
+ class OrderServiceTest {
331
+
332
+ @MockK
333
+ private lateinit var orderRepository: OrderRepository
334
+
335
+ @MockK
336
+ private lateinit var customerRepository: CustomerRepository
337
+
338
+ private lateinit var orderService: OrderService
339
+
340
+ @BeforeEach
341
+ fun setUp() {
342
+ orderService = OrderService(orderRepository, customerRepository)
343
+ }
344
+
345
+ @Nested
346
+ inner class Create {
347
+
348
+ @Test
349
+ fun `should create order when customer exists`() {
350
+ // Arrange
351
+ val customerId = UUID.randomUUID()
352
+ val request = CreateOrderRequest(totalAmount = BigDecimal("99.99"))
353
+ val savedOrder = Order(customerId = customerId, totalAmount = request.totalAmount)
354
+ every { customerRepository.existsById(customerId) } returns true
355
+ every { orderRepository.save(any()) } returns savedOrder
356
+
357
+ // Act
358
+ val result = orderService.create(customerId, request)
359
+
360
+ // Assert
361
+ assertThat(result.customerId).isEqualTo(customerId)
362
+ assertThat(result.totalAmount).isEqualByComparingTo(BigDecimal("99.99"))
363
+ verify(exactly = 1) { orderRepository.save(any()) }
364
+ }
365
+
366
+ @Test
367
+ fun `should throw CustomerNotFoundException when customer does not exist`() {
368
+ // Arrange
369
+ val customerId = UUID.randomUUID()
370
+ every { customerRepository.existsById(customerId) } returns false
371
+
372
+ // Act + Assert
373
+ assertThrows<CustomerNotFoundException> {
374
+ orderService.create(customerId, CreateOrderRequest(totalAmount = BigDecimal("10.00")))
375
+ }
376
+ verify(exactly = 0) { orderRepository.save(any()) }
377
+ }
378
+ }
379
+
380
+ @Nested
381
+ inner class Cancel {
382
+
383
+ @Test
384
+ fun `should cancel order when status is PENDING`() {
385
+ // Arrange
386
+ val order = Order(customerId = UUID.randomUUID(), totalAmount = BigDecimal("50.00"))
387
+ every { orderRepository.findById(order.id) } returns Optional.of(order)
388
+ every { orderRepository.save(any()) } answers { firstArg() }
389
+
390
+ // Act
391
+ val result = orderService.cancel(order.id)
392
+
393
+ // Assert
394
+ assertThat(result.status).isEqualTo(OrderStatus.CANCELLED)
395
+ }
396
+
397
+ @Test
398
+ fun `should throw OrderAlreadyCancelledException when order is already cancelled`() {
399
+ // Arrange
400
+ val order = Order(
401
+ customerId = UUID.randomUUID(),
402
+ totalAmount = BigDecimal("50.00"),
403
+ status = OrderStatus.CANCELLED
404
+ )
405
+ every { orderRepository.findById(order.id) } returns Optional.of(order)
406
+
407
+ // Act + Assert
408
+ assertThrows<OrderAlreadyCancelledException> {
409
+ orderService.cancel(order.id)
410
+ }
411
+ }
412
+ }
413
+ }
414
+ ```
415
+
416
+ ### Controller Tests (MockMvc)
417
+
418
+ ```kotlin
419
+ @SpringBootTest
420
+ @AutoConfigureMockMvc
421
+ class OrderControllerTest {
422
+
423
+ @Autowired
424
+ private lateinit var mockMvc: MockMvc
425
+
426
+ @Autowired
427
+ private lateinit var objectMapper: ObjectMapper
428
+
429
+ @MockkBean
430
+ private lateinit var orderService: OrderService
431
+
432
+ @Test
433
+ fun `should return 201 and created order when request is valid`() {
434
+ // Arrange
435
+ val customerId = UUID.randomUUID()
436
+ val order = Order(customerId = customerId, totalAmount = BigDecimal("99.99"))
437
+ every { orderService.create(customerId, any()) } returns order
438
+
439
+ val body = mapOf("totalAmount" to "99.99")
440
+
441
+ // Act + Assert
442
+ mockMvc.perform(
443
+ post("/api/v1/customers/$customerId/orders")
444
+ .contentType(MediaType.APPLICATION_JSON)
445
+ .content(objectMapper.writeValueAsString(body))
446
+ )
447
+ .andExpect(status().isCreated)
448
+ .andExpect(jsonPath("$.customerId").value(customerId.toString()))
449
+ .andExpect(jsonPath("$.status").value("PENDING"))
450
+ }
451
+
452
+ @Test
453
+ fun `should return 400 when totalAmount is missing`() {
454
+ mockMvc.perform(
455
+ post("/api/v1/customers/${UUID.randomUUID()}/orders")
456
+ .contentType(MediaType.APPLICATION_JSON)
457
+ .content("{}")
458
+ )
459
+ .andExpect(status().isBadRequest)
460
+ .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
461
+ }
462
+
463
+ @Test
464
+ fun `should return 404 when order is not found`() {
465
+ val customerId = UUID.randomUUID()
466
+ val orderId = UUID.randomUUID()
467
+ every { orderService.getById(orderId) } throws OrderNotFoundException(orderId)
468
+
469
+ mockMvc.perform(get("/api/v1/customers/$customerId/orders/$orderId"))
470
+ .andExpect(status().isNotFound)
471
+ .andExpect(jsonPath("$.code").value("ORDER_NOT_FOUND"))
472
+ }
473
+ }
474
+ ```
475
+
476
+ ### Repository Tests
477
+
478
+ ```kotlin
479
+ @DataJpaTest
480
+ class OrderRepositoryTest {
481
+
482
+ @Autowired
483
+ private lateinit var orderRepository: OrderRepository
484
+
485
+ @Test
486
+ fun `should find orders by customer id`() {
487
+ // Arrange
488
+ val customerId = UUID.randomUUID()
489
+ orderRepository.save(Order(customerId = customerId, totalAmount = BigDecimal("10.00")))
490
+ orderRepository.save(Order(customerId = UUID.randomUUID(), totalAmount = BigDecimal("20.00")))
491
+
492
+ // Act
493
+ val result = orderRepository.findByCustomerId(customerId)
494
+
495
+ // Assert
496
+ assertThat(result).hasSize(1)
497
+ assertThat(result.first().customerId).isEqualTo(customerId)
498
+ }
499
+ }
500
+ ```
501
+
502
+ MockK rules:
503
+
504
+ - Use `@ExtendWith(MockKExtension::class)` — not `@MockBean` for unit tests
505
+ - Use `@MockkBean` (MockK Spring integration) only in `@SpringBootTest` context
506
+ - `every { }` for stubbing, `verify { }` for verification
507
+ - `answers { firstArg() }` to return the argument passed to `save()`
508
+ - `verify(exactly = 0) { }` to assert a method was never called
509
+ - Never use `relaxed = true` unless you genuinely do not care about any return
510
+ value
511
+ - Test names: `should [expected outcome] when [condition]`
512
+ - Test structure: Arrange / Act / Assert, separated by blank lines
513
+
514
+ ---
515
+
516
+ ## Step 5 — When to Ask
517
+
518
+ Only pause to ask when there is genuine ambiguity in business logic. Do not ask
519
+ about technical choices — apply the conventions in this skill.
520
+
521
+ **Ask when:**
522
+
523
+ - The feature description does not specify what happens on conflict (duplicate,
524
+ constraint violation, concurrent modification)
525
+ - It is unclear whether delete means hard delete or soft delete
526
+ - Authorization rules are not specified and cannot be inferred from context
527
+ - A domain invariant is referenced but not defined (e.g., "validate the order"
528
+ without specifying which fields or rules)
529
+
530
+ **Do not ask about:**
531
+
532
+ - Package structure — always feature-oriented MVC
533
+ - Naming conventions — follow the existing codebase
534
+ - Whether to use MockK or Mockito — always MockK
535
+ - Whether to add `@Transactional` — always at the service class level
536
+ - Whether to validate at controller or service — always at DTO layer
537
+
538
+ ---
539
+
540
+ ## Package Structure
541
+
542
+ Always feature-oriented. One package per feature, not one package per layer.
543
+
544
+ ```
545
+ src/main/kotlin/{base-package}/{feature}/
546
+ controller/ # HTTP only — OrderController, DTOs, toResponse() extensions
547
+ service/ # Business logic — OrderService, domain exceptions
548
+ repository/ # Data access — OrderRepository
549
+ domain/ # JPA entities — Order, OrderStatus
550
+
551
+ src/test/kotlin/{base-package}/{feature}/
552
+ controller/ # OrderControllerTest (MockMvc)
553
+ service/ # OrderServiceTest (MockK)
554
+ repository/ # OrderRepositoryTest (@DataJpaTest)
555
+ ```
556
+
557
+ Never create `src/main/kotlin/controllers/`, `src/main/kotlin/services/`, etc.
558
+ Layer-first structure does not scale and breaks feature cohesion.