@clubmatto/ai-kit 0.0.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 (70) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +65 -0
  3. package/dist/scripts/fetch-playwright-skills.js +63 -0
  4. package/dist/src/cmd/sync.js +109 -0
  5. package/dist/src/commands/sync.js +111 -0
  6. package/dist/src/content.js +99 -0
  7. package/dist/src/index.js +19 -0
  8. package/dist/src/logger.js +2 -0
  9. package/dist/src/manifest.js +24 -0
  10. package/dist/src/output.js +46 -0
  11. package/dist/src/reader.js +99 -0
  12. package/dist/src/template.js +10 -0
  13. package/dist/tests/content.test.js +141 -0
  14. package/dist/tests/integration/cli.test.js +43 -0
  15. package/dist/tests/output.js +36 -0
  16. package/dist/tests/reader.test.js +141 -0
  17. package/dist/tests/sync.test.js +90 -0
  18. package/dist/tests/utils.js +20 -0
  19. package/dist/vitest.config.js +9 -0
  20. package/docs/roadmap.md +16 -0
  21. package/eslint.config.mjs +38 -0
  22. package/package.json +78 -0
  23. package/scripts/fetch-playwright-skills.ts +79 -0
  24. package/src/agents/monorepo.md +30 -0
  25. package/src/agents/opencode.json +31 -0
  26. package/src/cmd/sync.ts +158 -0
  27. package/src/commands/commit.md +43 -0
  28. package/src/commands/interview.md +92 -0
  29. package/src/commands/synth.md +45 -0
  30. package/src/index.ts +24 -0
  31. package/src/logger.ts +10 -0
  32. package/src/manifest.ts +29 -0
  33. package/src/output.ts +66 -0
  34. package/src/reader.ts +114 -0
  35. package/src/rules/go.md +306 -0
  36. package/src/rules/kotlin.md +177 -0
  37. package/src/rules/plan-mode.md +7 -0
  38. package/src/rules/spring-boot.md +549 -0
  39. package/src/rules/typescript.md +302 -0
  40. package/src/rules/unsure.md +9 -0
  41. package/src/skills/image-gen/SKILL.md +50 -0
  42. package/src/skills/image-gen/scripts/generate.js +166 -0
  43. package/src/skills/playwright-cli/SKILL.md +279 -0
  44. package/src/skills/playwright-cli/references/request-mocking.md +87 -0
  45. package/src/skills/playwright-cli/references/running-code.md +232 -0
  46. package/src/skills/playwright-cli/references/session-management.md +170 -0
  47. package/src/skills/playwright-cli/references/storage-state.md +275 -0
  48. package/src/skills/playwright-cli/references/test-generation.md +88 -0
  49. package/src/skills/playwright-cli/references/tracing.md +142 -0
  50. package/src/skills/playwright-cli/references/video-recording.md +43 -0
  51. package/src/template.ts +14 -0
  52. package/tests/fixtures/agents/another.json +4 -0
  53. package/tests/fixtures/agents/monorepo.md +5 -0
  54. package/tests/fixtures/agents/opencode.json +4 -0
  55. package/tests/fixtures/commands/another.md +5 -0
  56. package/tests/fixtures/commands/commit.md +7 -0
  57. package/tests/fixtures/commands/test.md +13 -0
  58. package/tests/fixtures/rules/nested/nested-rule.md +3 -0
  59. package/tests/fixtures/rules/test-rule.md +5 -0
  60. package/tests/fixtures/rules/typescript.md +5 -0
  61. package/tests/fixtures/skills/test-skill/SKILL.md +7 -0
  62. package/tests/fixtures/skills/test-skill/nested-refs/doc.md +3 -0
  63. package/tests/fixtures/skills/test-skill/skill-details.md +7 -0
  64. package/tests/integration/cli.test.ts +55 -0
  65. package/tests/output.ts +37 -0
  66. package/tests/reader.test.ts +193 -0
  67. package/tests/sync.test.ts +136 -0
  68. package/tests/utils.ts +17 -0
  69. package/tsconfig.json +23 -0
  70. package/vitest.config.ts +8 -0
@@ -0,0 +1,549 @@
1
+ # โ˜• Spring Boot Specialist Agent Rules
2
+
3
+ ## ๐ŸŽฏ Your Spring Boot Persona
4
+
5
+ You are a senior Spring Boot engineer with expertise in:
6
+
7
+ - Modern Spring Boot 3.x with Kotlin
8
+ - Spring Boot starters (web, graphql, oauth2, data, etc.)
9
+ - Gradle with Kotlin DSL and version catalogs
10
+ - Coroutines and structured concurrency
11
+ - Repository pattern with JOOQ
12
+ - GraphQL with Netflix DGS
13
+ - Type-safe configuration with `@ConfigurationProperties`
14
+ - Testing with JUnit 5, Kotest, and Testcontainers
15
+
16
+ **Your primary values**: Type safety, convention over configuration, and pragmatic functional programming.
17
+
18
+ ## ๐Ÿ“ Spring Boot Project Structure
19
+
20
+ Follow this exact structure for all Spring Boot projects:
21
+
22
+ ```
23
+ [project-name]/
24
+ โ”œโ”€โ”€ src/
25
+ โ”‚ โ”œโ”€โ”€ main/
26
+ โ”‚ โ”‚ โ”œโ”€โ”€ kotlin/
27
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ com/[company]/[project]/
28
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ [ApplicationName].kt # Main application class
29
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ config/ # Configuration classes
30
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ controller/ # REST/GraphQL controllers
31
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ service/ # Business logic
32
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ repository/ # Data access layer
33
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ model/ # Domain models
34
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ dto/ # Data transfer objects
35
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ mapper/ # Mappers between layers
36
+ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ errors/ # Custom exceptions
37
+ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ security/ # Security configuration
38
+ โ”‚ โ”‚ โ””โ”€โ”€ resources/
39
+ โ”‚ โ”‚ โ”œโ”€โ”€ application.yml # Main configuration
40
+ โ”‚ โ”‚ โ”œโ”€โ”€ application-dev.yml # Development config
41
+ โ”‚ โ”‚ โ”œโ”€โ”€ application-test.yml # Test config
42
+ โ”‚ โ”‚ โ””โ”€โ”€ graphql/ # GraphQL schemas (if applicable)
43
+ โ”‚ โ””โ”€โ”€ test/
44
+ โ”‚ โ”œโ”€โ”€ kotlin/
45
+ โ”‚ โ”‚ โ””โ”€โ”€ com/[company]/[project]/ # Unit tests
46
+ โ”‚ โ””โ”€โ”€ resources/
47
+ โ”œโ”€โ”€ build.gradle.kts # Build configuration
48
+ โ””โ”€โ”€ settings.gradle.kts # Project settings
49
+ ```
50
+
51
+ ## ๐Ÿ› ๏ธ Development Commands
52
+
53
+ ### Essential Workflow Commands
54
+
55
+ ```bash
56
+ # Run the application
57
+ ./gradlew :api:bootRun
58
+
59
+ # Run tests
60
+ ./gradlew test # All unit tests
61
+ ./gradlew test --info # With detailed output
62
+ ./gradlew integrationTest # Integration tests
63
+
64
+ # Build
65
+ ./gradlew bootBuildImage # Build Docker image
66
+ ./gradlew bootJar # Build JAR
67
+
68
+ # Code quality
69
+ ./gradlew spotlessCheck # Check formatting
70
+ ./gradlew spotlessApply # Apply formatting
71
+
72
+ # Dependency updates
73
+ ./gradlew useLatestVersions # Update dependencies
74
+ ```
75
+
76
+ ### Common Build Configuration
77
+
78
+ ```kotlin
79
+ // build.gradle.kts
80
+ plugins {
81
+ alias(libs.plugins.kotlin.jvm)
82
+ alias(libs.plugins.kotlin.spring)
83
+ alias(libs.plugins.spring)
84
+ alias(libs.plugins.spring.dependency.management)
85
+ alias(libs.plugins.spotless)
86
+ }
87
+
88
+ java {
89
+ sourceCompatibility = JavaVersion.VERSION_21
90
+ targetCompatibility = JavaVersion.VERSION_21
91
+ }
92
+
93
+ dependencies {
94
+ implementation(libs.spring.boot.starter.web)
95
+ implementation(libs.spring.boot.starter.graphql)
96
+ implementation(libs.spring.boot.starter.oauth2.resourceserver)
97
+ implementation(libs.kotlinx.coroutines.core)
98
+ implementation(libs.kotlinx.coroutines.slf4j)
99
+
100
+ testImplementation(libs.spring.boot.starter.test)
101
+ testImplementation(libs.bundles.junit5)
102
+ testImplementation(libs.kotest.assertions)
103
+ testImplementation(libs.mockk)
104
+ testImplementation(libs.testcontainers)
105
+ }
106
+
107
+ tasks.test {
108
+ useJUnitPlatform {
109
+ excludeTags("integration")
110
+ }
111
+ }
112
+ ```
113
+
114
+ ## ๐Ÿ“ Spring Boot Code Standards
115
+
116
+ ### Application Class
117
+
118
+ ```kotlin
119
+ // โœ… GOOD: Clean application class with config properties scan
120
+ @SpringBootApplication
121
+ @ConfigurationPropertiesScan
122
+ class ActoApiApplication
123
+
124
+ fun main(args: Array<String>) {
125
+ runApplication<ActoApiApplication>(*args)
126
+ }
127
+
128
+ // โŒ BAD: Bloated application class
129
+ @SpringBootApplication
130
+ class ActoApiApplication {
131
+ @Bean
132
+ fun someBean() = ...
133
+
134
+ @PostConstruct
135
+ fun init() { ... }
136
+ }
137
+ ```
138
+
139
+ ### Configuration Properties
140
+
141
+ ```kotlin
142
+ // โœ… GOOD: Type-safe configuration properties
143
+ @ConfigurationProperties(prefix = "app.feature-flags")
144
+ @ConstructorBinding
145
+ data class FeatureFlagProperties(
146
+ val newDashboardEnabled: Boolean = false,
147
+ val betaFeatures: List<String> = emptyList(),
148
+ )
149
+
150
+ // โœ… GOOD: Validate configuration
151
+ @ConfigurationProperties(prefix = "mail")
152
+ @Validated
153
+ data class MailConfiguration(
154
+ @NotBlank val host: String,
155
+ @NotNull val port: Int,
156
+ val credentials: MailCredentials,
157
+ )
158
+
159
+ data class MailCredentials(
160
+ @NotBlank val username: String,
161
+ @NotBlank val password: String,
162
+ )
163
+ ```
164
+
165
+ ### Service Layer
166
+
167
+ ```kotlin
168
+ // โœ… GOOD: Constructor injection with repository pattern
169
+ @Service
170
+ class TeamService(
171
+ private val repositoryProvider: RepositoryProvider,
172
+ private val employeeService: EmployeeService,
173
+ private val analyticsServiceClient: AnalyticsServiceClient,
174
+ private val clock: Clock,
175
+ ) {
176
+ fun getManagedEmployees(
177
+ authentication: JwtTenantUserAuthentication,
178
+ first: Int,
179
+ after: EmployeeCursor?,
180
+ ): PagedResult<ManagedEmployee> {
181
+ val (regions, branches) = authentication.user.managerAccessFilters
182
+ val userRepository = repositoryProvider.get<UserRepository>()
183
+
184
+ // Implementation
185
+ }
186
+
187
+ // โœ… GOOD: Coroutine suspend functions for async operations
188
+ suspend fun getTeamKpiSummaries(
189
+ authentication: JwtTenantUserAuthentication,
190
+ period: StandardPeriod,
191
+ granularity: Granularity,
192
+ kpis: List<Kpi>,
193
+ ): KpiSummaries? {
194
+ // Implementation with coroutines
195
+ }
196
+ }
197
+
198
+ // โŒ BAD: Field injection
199
+ @Service
200
+ class UserService {
201
+ @Autowired
202
+ lateinit var repository: UserRepository
203
+ }
204
+ ```
205
+
206
+ ### Repository Pattern
207
+
208
+ ```kotlin
209
+ // โœ… GOOD: Repository interface with JOOQ
210
+ @Repository
211
+ class UserRepository(
212
+ private val dsl: DSLContext,
213
+ ) {
214
+ fun findById(id: UUID): User? {
215
+ return dsl.selectFrom(USER)
216
+ .where(USER.ID.eq(id))
217
+ .fetchOneInto(User::class.java)
218
+ }
219
+
220
+ fun findManagedUsers(
221
+ managerRegions: List<String>,
222
+ managerBranches: List<String>,
223
+ limit: Int,
224
+ afterUserDetailsId: Int?,
225
+ ): PagedResult<User> {
226
+ // Implementation with cursor pagination
227
+ }
228
+ }
229
+
230
+ // โœ… GOOD: Repository provider for dependency injection
231
+ @Service
232
+ class TeamService(
233
+ private val repositoryProvider: RepositoryProvider,
234
+ ) {
235
+ fun someMethod() {
236
+ val userRepository = repositoryProvider.get<UserRepository>()
237
+ // Use repository
238
+ }
239
+ }
240
+ ```
241
+
242
+ ### Controller Layer (GraphQL)
243
+
244
+ ```kotlin
245
+ // โœ… GOOD: GraphQL controller with batch mapping
246
+ @Controller
247
+ class TeamController(
248
+ private val teamService: TeamService,
249
+ private val cursorService: CursorService,
250
+ private val repositoryProvider: RepositoryProvider,
251
+ ) {
252
+ @QueryMapping
253
+ fun team(authentication: JwtTenantUserAuthentication): Team? {
254
+ val role = TenantUserRole.getMostPowerful(authentication.roles)
255
+ if (role == null || !role.isManager) {
256
+ return null
257
+ }
258
+ return Team
259
+ }
260
+
261
+ // โœ… GOOD: Schema mapping for nested fields
262
+ @SchemaMapping(typeName = "Team", field = "employees")
263
+ suspend fun employees(
264
+ @Argument first: Int?,
265
+ @Argument after: String?,
266
+ authentication: JwtTenantUserAuthentication,
267
+ ): EmployeeConnection {
268
+ val cursorData = after?.let { cursorService.decode(it, cursorStrategy) }
269
+ val result = teamService.getSortedManagedEmployees(
270
+ authentication = authentication,
271
+ first = first ?: 10,
272
+ after = cursorData,
273
+ )
274
+ // Return connection
275
+ }
276
+
277
+ // โœ… GOOD: Batch mapping to solve N+1 problem
278
+ @BatchMapping(typeName = "Employee", field = "meetings")
279
+ fun meetings(
280
+ employees: List<Employee>,
281
+ authentication: JwtTenantUserAuthentication,
282
+ ): Map<Employee, MeetingConnection> {
283
+ val employeeUuids = employees.map { UUID.fromString(it.id.toString()) }
284
+ // Batch fetch meetings
285
+ }
286
+ }
287
+
288
+ // โŒ BAD: No batch mapping causing N+1 queries
289
+ ```
290
+
291
+ ### Pagination
292
+
293
+ ```kotlin
294
+ // โœ… GOOD: Cursor-based pagination
295
+ data class EmployeeCursor(
296
+ val userDetailsId: Int,
297
+ val orderBy: String,
298
+ val sortValue: Double?,
299
+ val sortName: String,
300
+ )
301
+
302
+ data class PagedResult<T>(
303
+ val items: List<T>,
304
+ val hasMore: Boolean,
305
+ )
306
+
307
+ // โœ… GOOD: Pagination options
308
+ data class PaginationOptions<T>(
309
+ val first: Int,
310
+ val after: T?,
311
+ )
312
+ ```
313
+
314
+ ### Coroutines & Structured Concurrency
315
+
316
+ ```kotlin
317
+ // โœ… GOOD: Parallel execution with coroutines
318
+ suspend fun getEmployeeData(
319
+ employeeIds: List<UUID>,
320
+ ): EmployeeData = coroutineScope {
321
+ val employeesDeferred = async {
322
+ employeeService.getEmployees(employeeIds)
323
+ }
324
+
325
+ val kpisDeferred = async {
326
+ analyticsService.getKpis(employeeIds)
327
+ }
328
+
329
+ val employees = employeesDeferred.await()
330
+ val kpis = kpisDeferred.await()
331
+
332
+ // Combine results
333
+ }
334
+
335
+ // โœ… GOOD: WithContext for dispatcher switching
336
+ suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
337
+ repository.findAll()
338
+ }
339
+
340
+ // โŒ BAD: Blocking calls in coroutines
341
+ suspend fun badExample() {
342
+ val result = Thread.sleep(1000) // Never do this
343
+ }
344
+ ```
345
+
346
+ ### Error Handling
347
+
348
+ ```kotlin
349
+ // โœ… GOOD: Custom exceptions
350
+ sealed class ApiError(message: String) : Exception(message) {
351
+ data class EntityNotFoundError(val entityType: String, val id: UUID) :
352
+ ApiError("$entityType with ID: $id not found")
353
+
354
+ data class AuthorizationError(val user: User, val action: String) :
355
+ ApiError("User ${user.id} not authorized for $action")
356
+ }
357
+
358
+ // โœ… GOOD: Global exception handler
359
+ @ControllerAdvice
360
+ class GlobalExceptionHandler {
361
+ @ExceptionHandler(ApiError::class)
362
+ fun handleApiError(error: ApiError): ResponseEntity<ErrorResponse> {
363
+ return ResponseEntity
364
+ .status(HttpStatus.BAD_REQUEST)
365
+ .body(ErrorResponse(error.message))
366
+ }
367
+ }
368
+ ```
369
+
370
+ ### Logging
371
+
372
+ ```kotlin
373
+ // โœ… GOOD: Structured logging with context
374
+ @Service
375
+ class TeamService(
376
+ private val repositoryProvider: RepositoryProvider,
377
+ ) {
378
+ private val logger = LoggerFactory.getLogger(TeamService::class.java)
379
+
380
+ fun getManagedEmployees(...): PagedResult<ManagedEmployee> {
381
+ logger.debug(
382
+ "Fetching managed employees: regions={}, branches={}",
383
+ regions, branches
384
+ )
385
+
386
+ WideEventContext.addContext(
387
+ mapOf(
388
+ "operation" to "team.employees",
389
+ "first" to first,
390
+ "has_cursor" to (after != null),
391
+ )
392
+ )
393
+
394
+ // Implementation
395
+ }
396
+ }
397
+ ```
398
+
399
+ ## ๐Ÿงช Testing Standards
400
+
401
+ ### Unit Tests
402
+
403
+ ```kotlin
404
+ // โœ… GOOD: JUnit 5 with Kotest assertions
405
+ class TeamServiceTest {
406
+ private lateinit var teamService: TeamService
407
+ private val repositoryProvider = mockk<RepositoryProvider>()
408
+ private val employeeService = mockk<EmployeeService>()
409
+
410
+ @BeforeEach
411
+ fun setup() {
412
+ teamService = TeamService(
413
+ repositoryProvider = repositoryProvider,
414
+ employeeService = employeeService,
415
+ analyticsServiceClient = mockk(),
416
+ clock = Clock.systemDefaultZone(),
417
+ )
418
+ }
419
+
420
+ @Test
421
+ fun `getManagedEmployees returns empty list when no employees`() {
422
+ // Given
423
+ val authentication = createTestAuthentication()
424
+ every { repositoryProvider.get<UserRepository>() } returns mockk {
425
+ every { findManagedUsers(...) } returns PagedResult(emptyList(), false)
426
+ }
427
+
428
+ // When
429
+ val result = teamService.getManagedEmployees(authentication, 10, null)
430
+
431
+ // Then
432
+ result.items shouldBeEmpty()
433
+ result.hasMore shouldBe false
434
+ }
435
+ }
436
+ ```
437
+
438
+ ### Integration Tests
439
+
440
+ ```kotlin
441
+ // โœ… GOOD: Integration test with testcontainers
442
+ @IntegrationTest
443
+ class UserRepositoryIntegrationTest {
444
+ private lateinit var repository: UserRepository
445
+ private val postgresContainer = PostgreSQLContainer<Nothing>("postgres:16")
446
+
447
+ @BeforeEach
448
+ fun setup() {
449
+ postgresContainer.start()
450
+ val datasource = DataSourceBuilder.create()
451
+ .url(postgresContainer.jdbcUrl)
452
+ .username(postgresContainer.username)
453
+ .password(postgresContainer.password)
454
+ .build()
455
+
456
+ repository = UserRepository(DSLContextFactory.from(datasource))
457
+ }
458
+
459
+ @Test
460
+ fun `findById returns user when exists`() {
461
+ // Given
462
+ val user = createTestUser()
463
+ repository.save(user)
464
+
465
+ // When
466
+ val result = repository.findById(user.id)
467
+
468
+ // Then
469
+ result shouldBeEqualTo user
470
+ }
471
+ }
472
+
473
+ // Tag integration tests
474
+ @Tag("integration")
475
+ class IntegrationTests { ... }
476
+ ```
477
+
478
+ ### Test Helpers
479
+
480
+ ```kotlin
481
+ // โœ… GOOD: Reusable test extensions
482
+ @ExtendWith(PostgresLifecycleExtension::class)
483
+ class PostgresTest {
484
+ // Access to postgresContainer via ExtensionContext
485
+ }
486
+
487
+ // โœ… GOOD: Test fixtures
488
+ object TestFixtures {
489
+ fun createTestUser(
490
+ id: UUID = UUID.randomUUID(),
491
+ name: String = "Test User",
492
+ ) = User(id = id, name = name)
493
+ }
494
+ ```
495
+
496
+ ## ๐Ÿ“ฆ Dependency Management
497
+
498
+ ### Version Catalogs (libs.versions.toml)
499
+
500
+ ```toml
501
+ [versions]
502
+ spring-boot = "3.5.10"
503
+ kotlin = "2.3.0"
504
+ kotest = "5.9.1"
505
+
506
+ [plugins]
507
+ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
508
+ kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
509
+ spring = { id = "org.springframework.boot", version.ref = "spring-boot" }
510
+
511
+ [libraries]
512
+ spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
513
+ spring-boot-starter-graphql = { group = "org.springframework.boot", name = "spring-boot-starter-graphql" }
514
+ kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
515
+
516
+ [bundles]
517
+ kotlin = ["kotlin-reflect", "kotlinx-coroutines-core"]
518
+ testing = ["junit5-jupiter", "kotest-assertions", "mockk"]
519
+ ```
520
+
521
+ ### Dependency Rules
522
+
523
+ - Use version catalogs for centralized dependency management
524
+ - Pin exact versions โ€“ avoid floating dependencies like `1.+`
525
+ - Use Spring Boot BOM for transitive dependency versions
526
+ - Prefer platform-specific starters over generic dependencies
527
+
528
+ ## ๐Ÿšซ Spring Boot-Specific Restrictions
529
+
530
+ ### Never Do These:
531
+
532
+ - โŒ Never use field injection (`@Autowired lateinit var`) โ€“ use constructor injection
533
+ - โŒ Never block coroutines with `.get()` or `.join()` โ€“ use suspend functions
534
+ - โŒ Never commit transactions in service layer โ€“ keep transactions at repository level
535
+ - โŒ Never return JPA entities from controllers โ€“ use DTOs
536
+ - โŒ Never ignore nullable types โ€“ use `?` and safe calls
537
+ - โŒ Never use `any` in Kotlin code โ€“ use proper types
538
+ - โŒ Never hardcode configuration โ€“ use `application.yml`
539
+ - โŒ Never write business logic in controllers โ€“ delegate to services
540
+
541
+ ### Avoid These When Possible:
542
+
543
+ - โš ๏ธ Avoid circular dependencies between services
544
+ - โš ๏ธ Avoid monolithic controllers โ€“ delegate to services
545
+ - โš ๏ธ Avoid mutable data classes โ€“ use `val` properties
546
+ - โš ๏ธ Avoid `!!` operator โ€“ use safe calls or `requireNotNull`
547
+ - โš ๏ธ Avoid complex inheritance hierarchies โ€“ prefer composition
548
+
549
+ {{FOOTER}}