@c0x12c/ai-toolkit 1.15.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.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +12 -0
- package/README.md +439 -0
- package/VERSION +1 -0
- package/agents/design-critic.md +127 -0
- package/agents/idea-killer.md +72 -0
- package/agents/infrastructure-expert.md +49 -0
- package/agents/micronaut-backend-expert.md +45 -0
- package/agents/phase-reviewer.md +150 -0
- package/agents/research-planner.md +70 -0
- package/agents/solution-architect-cto.md +49 -0
- package/agents/sre-architect.md +49 -0
- package/agents/team-coordinator.md +111 -0
- package/bin/cli.js +780 -0
- package/claude-md/00-header.md +39 -0
- package/claude-md/01-core.md +105 -0
- package/claude-md/05-database.md +20 -0
- package/claude-md/11-backend-micronaut.md +19 -0
- package/claude-md/20-frontend-react.md +44 -0
- package/claude-md/25-ux-design.md +56 -0
- package/claude-md/30-infrastructure.md +24 -0
- package/claude-md/30-project-mgmt.md +119 -0
- package/claude-md/40-product.md +39 -0
- package/claude-md/50-ops.md +34 -0
- package/claude-md/60-research.md +27 -0
- package/claude-md/90-footer.md +21 -0
- package/commands/spartan/brainstorm.md +134 -0
- package/commands/spartan/brownfield.md +157 -0
- package/commands/spartan/build.md +435 -0
- package/commands/spartan/careful.md +94 -0
- package/commands/spartan/commit-message.md +112 -0
- package/commands/spartan/content.md +17 -0
- package/commands/spartan/context-save.md +161 -0
- package/commands/spartan/contribute.md +140 -0
- package/commands/spartan/daily.md +42 -0
- package/commands/spartan/debug.md +308 -0
- package/commands/spartan/deep-dive.md +55 -0
- package/commands/spartan/deploy.md +207 -0
- package/commands/spartan/e2e.md +264 -0
- package/commands/spartan/env-setup.md +166 -0
- package/commands/spartan/epic.md +199 -0
- package/commands/spartan/fe-review.md +181 -0
- package/commands/spartan/figma-to-code.md +260 -0
- package/commands/spartan/forensics.md +46 -0
- package/commands/spartan/freeze.md +84 -0
- package/commands/spartan/fundraise.md +53 -0
- package/commands/spartan/gate-review.md +229 -0
- package/commands/spartan/gsd-upgrade.md +376 -0
- package/commands/spartan/guard.md +42 -0
- package/commands/spartan/init-project.md +178 -0
- package/commands/spartan/init-rules.md +298 -0
- package/commands/spartan/interview.md +154 -0
- package/commands/spartan/kickoff.md +73 -0
- package/commands/spartan/kotlin-service.md +109 -0
- package/commands/spartan/lean-canvas.md +222 -0
- package/commands/spartan/lint-rules.md +122 -0
- package/commands/spartan/map-codebase.md +124 -0
- package/commands/spartan/migration.md +82 -0
- package/commands/spartan/next-app.md +317 -0
- package/commands/spartan/next-feature.md +212 -0
- package/commands/spartan/onboard.md +326 -0
- package/commands/spartan/outreach.md +16 -0
- package/commands/spartan/phase.md +142 -0
- package/commands/spartan/pitch.md +18 -0
- package/commands/spartan/plan.md +210 -0
- package/commands/spartan/pr-ready.md +202 -0
- package/commands/spartan/project.md +106 -0
- package/commands/spartan/qa.md +222 -0
- package/commands/spartan/research.md +254 -0
- package/commands/spartan/review.md +132 -0
- package/commands/spartan/scan-rules.md +173 -0
- package/commands/spartan/sessions.md +143 -0
- package/commands/spartan/spec.md +131 -0
- package/commands/spartan/startup.md +257 -0
- package/commands/spartan/team.md +570 -0
- package/commands/spartan/teardown.md +161 -0
- package/commands/spartan/testcontainer.md +97 -0
- package/commands/spartan/tf-cost.md +123 -0
- package/commands/spartan/tf-deploy.md +116 -0
- package/commands/spartan/tf-drift.md +100 -0
- package/commands/spartan/tf-import.md +107 -0
- package/commands/spartan/tf-module.md +121 -0
- package/commands/spartan/tf-plan.md +100 -0
- package/commands/spartan/tf-review.md +106 -0
- package/commands/spartan/tf-scaffold.md +109 -0
- package/commands/spartan/tf-security.md +147 -0
- package/commands/spartan/think.md +221 -0
- package/commands/spartan/unfreeze.md +13 -0
- package/commands/spartan/update.md +134 -0
- package/commands/spartan/ux.md +1233 -0
- package/commands/spartan/validate.md +193 -0
- package/commands/spartan/web-to-prd.md +706 -0
- package/commands/spartan/workstreams.md +109 -0
- package/commands/spartan/write.md +16 -0
- package/commands/spartan.md +386 -0
- package/frameworks/00-framework-comparison-guide.md +317 -0
- package/frameworks/01-lean-canvas.md +196 -0
- package/frameworks/02-design-sprint.md +304 -0
- package/frameworks/03-foundation-sprint.md +337 -0
- package/frameworks/04-business-model-canvas.md +391 -0
- package/frameworks/05-customer-development.md +426 -0
- package/frameworks/06-jobs-to-be-done.md +358 -0
- package/frameworks/07-mom-test.md +392 -0
- package/frameworks/08-value-proposition-canvas.md +488 -0
- package/frameworks/09-javelin-board.md +428 -0
- package/frameworks/10-build-measure-learn.md +467 -0
- package/frameworks/11-mvp-approaches.md +533 -0
- package/frameworks/think-before-build.md +593 -0
- package/lib/assembler.js +197 -0
- package/lib/assembler.test.js +159 -0
- package/lib/detector.js +166 -0
- package/lib/detector.test.js +221 -0
- package/lib/packs.js +16 -0
- package/lib/resolver.js +272 -0
- package/lib/resolver.test.js +298 -0
- package/lib/worktree.sh +104 -0
- package/package.json +50 -0
- package/packs/backend-micronaut.yaml +35 -0
- package/packs/backend-nodejs.yaml +15 -0
- package/packs/backend-python.yaml +15 -0
- package/packs/core.yaml +37 -0
- package/packs/database.yaml +21 -0
- package/packs/frontend-react.yaml +24 -0
- package/packs/infrastructure.yaml +40 -0
- package/packs/ops.yaml +16 -0
- package/packs/packs.compiled.json +371 -0
- package/packs/product.yaml +22 -0
- package/packs/project-mgmt.yaml +24 -0
- package/packs/research.yaml +39 -0
- package/packs/shared-backend.yaml +14 -0
- package/packs/ux-design.yaml +21 -0
- package/rules/backend-micronaut/API_DESIGN.md +313 -0
- package/rules/backend-micronaut/BATCH_PROCESSING.md +92 -0
- package/rules/backend-micronaut/CONTROLLERS.md +388 -0
- package/rules/backend-micronaut/KOTLIN.md +414 -0
- package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +290 -0
- package/rules/backend-micronaut/SERVICES_AND_BEANS.md +325 -0
- package/rules/core/NAMING_CONVENTIONS.md +208 -0
- package/rules/core/SKILL_AUTHORING.md +174 -0
- package/rules/core/TIMEZONE.md +316 -0
- package/rules/database/ORM_AND_REPO.md +289 -0
- package/rules/database/SCHEMA.md +146 -0
- package/rules/database/TRANSACTIONS.md +311 -0
- package/rules/frontend-react/FRONTEND.md +344 -0
- package/rules/infrastructure/MODULES.md +260 -0
- package/rules/infrastructure/NAMING.md +196 -0
- package/rules/infrastructure/PROVIDERS.md +309 -0
- package/rules/infrastructure/SECURITY.md +310 -0
- package/rules/infrastructure/STATE_AND_BACKEND.md +237 -0
- package/rules/infrastructure/STRUCTURE.md +234 -0
- package/rules/infrastructure/VARIABLES.md +285 -0
- package/rules/shared-backend/ARCHITECTURE.md +46 -0
- package/rules/ux-design/DESIGN_PROCESS.md +176 -0
- package/skills/api-endpoint-creator/SKILL.md +455 -0
- package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
- package/skills/api-endpoint-creator/examples.md +522 -0
- package/skills/api-endpoint-creator/testing-patterns.md +302 -0
- package/skills/article-writing/SKILL.md +109 -0
- package/skills/article-writing/examples.md +59 -0
- package/skills/backend-api-design/SKILL.md +84 -0
- package/skills/backend-api-design/code-patterns.md +138 -0
- package/skills/brainstorm/SKILL.md +95 -0
- package/skills/browser-qa/SKILL.md +87 -0
- package/skills/browser-qa/playwright-snippets.md +110 -0
- package/skills/ci-cd-patterns/SKILL.md +108 -0
- package/skills/ci-cd-patterns/workflows.md +149 -0
- package/skills/competitive-teardown/SKILL.md +93 -0
- package/skills/competitive-teardown/example-analysis.md +50 -0
- package/skills/content-engine/SKILL.md +131 -0
- package/skills/content-engine/examples.md +72 -0
- package/skills/database-patterns/SKILL.md +72 -0
- package/skills/database-patterns/code-templates.md +114 -0
- package/skills/database-table-creator/SKILL.md +141 -0
- package/skills/database-table-creator/examples.md +552 -0
- package/skills/database-table-creator/kotlin-templates.md +400 -0
- package/skills/database-table-creator/migration-template.sql +68 -0
- package/skills/database-table-creator/validation-checklist.md +337 -0
- package/skills/deep-research/SKILL.md +80 -0
- package/skills/design-intelligence/SKILL.md +268 -0
- package/skills/design-workflow/SKILL.md +127 -0
- package/skills/design-workflow/checklists.md +45 -0
- package/skills/idea-validation/SKILL.md +129 -0
- package/skills/idea-validation/example-report.md +50 -0
- package/skills/investor-materials/SKILL.md +122 -0
- package/skills/investor-materials/example-outline.md +70 -0
- package/skills/investor-outreach/SKILL.md +112 -0
- package/skills/investor-outreach/examples.md +76 -0
- package/skills/kotlin-best-practices/SKILL.md +58 -0
- package/skills/kotlin-best-practices/code-patterns.md +132 -0
- package/skills/market-research/SKILL.md +99 -0
- package/skills/security-checklist/SKILL.md +65 -0
- package/skills/security-checklist/audit-reference.md +95 -0
- package/skills/service-debugging/SKILL.md +116 -0
- package/skills/service-debugging/common-issues.md +65 -0
- package/skills/startup-pipeline/SKILL.md +152 -0
- package/skills/terraform-best-practices/SKILL.md +244 -0
- package/skills/terraform-module-creator/SKILL.md +284 -0
- package/skills/terraform-review/SKILL.md +222 -0
- package/skills/terraform-security-audit/SKILL.md +280 -0
- package/skills/terraform-service-scaffold/SKILL.md +574 -0
- package/skills/testing-strategies/SKILL.md +116 -0
- package/skills/testing-strategies/examples.md +103 -0
- package/skills/testing-strategies/integration-test-setup.md +71 -0
- package/skills/ui-ux-pro-max/SKILL.md +238 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/python-setup.md +146 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/web-to-prd/SKILL.md +478 -0
- package/templates/build-config.yaml +44 -0
- package/templates/commands-config.yaml +55 -0
- package/templates/competitor-analysis.md +60 -0
- package/templates/content/AGENT_TEMPLATE.md +47 -0
- package/templates/content/COMMAND_TEMPLATE.md +27 -0
- package/templates/content/RULE_TEMPLATE.md +40 -0
- package/templates/content/SKILL_TEMPLATE.md +41 -0
- package/templates/design-config.md +105 -0
- package/templates/design-doc.md +207 -0
- package/templates/epic.md +100 -0
- package/templates/feature-spec.md +181 -0
- package/templates/idea-canvas.md +47 -0
- package/templates/implementation-plan.md +159 -0
- package/templates/prd-template.md +86 -0
- package/templates/preamble.md +89 -0
- package/templates/project-readme.md +35 -0
- package/templates/quality-gates.md +230 -0
- package/templates/spartan-config.yaml +164 -0
- package/templates/user-interview.md +69 -0
- package/templates/validation-checklist.md +108 -0
- package/templates/workflow-backend-micronaut.md +409 -0
- package/templates/workflow-frontend-react.md +233 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Kotlin Rules
|
|
2
|
+
|
|
3
|
+
> Full guide: use `/kotlin-best-practices` skill
|
|
4
|
+
|
|
5
|
+
## Null Safety (CRITICAL)
|
|
6
|
+
|
|
7
|
+
**The force unwrap operator `!!` is FORBIDDEN in this codebase.**
|
|
8
|
+
|
|
9
|
+
It causes runtime crashes, makes code unpredictable, and goes against Kotlin's null safety. There are no exceptions.
|
|
10
|
+
|
|
11
|
+
### What to Use Instead
|
|
12
|
+
|
|
13
|
+
```kotlin
|
|
14
|
+
// Safe call + elvis
|
|
15
|
+
val email = decodedToken.email ?: return AuthError.INVALID_CREDENTIALS.asException().left()
|
|
16
|
+
|
|
17
|
+
// Null check (enables smart cast)
|
|
18
|
+
if (user == null) {
|
|
19
|
+
return AuthError.AUTHENTICATION_FAILED.asException().left()
|
|
20
|
+
}
|
|
21
|
+
generateTokensAndResponse(user, provider.value) // user is non-null here
|
|
22
|
+
|
|
23
|
+
// requireNotNull (only when null = programming error)
|
|
24
|
+
val validUser = requireNotNull(user) { "User must not be null here" }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## No Workarounds (CRITICAL)
|
|
30
|
+
|
|
31
|
+
**NEVER use workarounds. Always fix the root cause. This rule has no exceptions.**
|
|
32
|
+
|
|
33
|
+
**Forbidden patterns:**
|
|
34
|
+
|
|
35
|
+
| Pattern | Instead |
|
|
36
|
+
|---------|---------|
|
|
37
|
+
| `@Suppress("UNUSED_PARAMETER")` | Remove the parameter, update call sites |
|
|
38
|
+
| `@Suppress("DEPRECATION")` | Use the non-deprecated replacement |
|
|
39
|
+
| `// TODO: Fix this later` + `!!` | Fix it now with proper null handling |
|
|
40
|
+
| `// HACK:` comments | Fix the root cause using proper layers |
|
|
41
|
+
| Placeholder parameters `reserved: String = ""` | Only include parameters you use |
|
|
42
|
+
|
|
43
|
+
```kotlin
|
|
44
|
+
// WRONG
|
|
45
|
+
@Suppress("UNUSED_PARAMETER")
|
|
46
|
+
private fun createCommit(employeeId: UUID, authorUsername: String)
|
|
47
|
+
|
|
48
|
+
// CORRECT — remove unused param, update all call sites
|
|
49
|
+
private fun createCommit(authorUsername: String)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Always**: understand root cause, fix actual problem, update all related code (call sites, tests, docs).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Error Handling
|
|
57
|
+
|
|
58
|
+
### Use Either for All Fallible Operations
|
|
59
|
+
|
|
60
|
+
Return `Either<ClientException, T>` from managers. Never throw exceptions in business logic.
|
|
61
|
+
|
|
62
|
+
```kotlin
|
|
63
|
+
suspend fun loginUser(email: String): Either<ClientException, User> {
|
|
64
|
+
val user = userRepository.byEmail(email)
|
|
65
|
+
?: return ClientError.USER_NOT_FOUND.asException().left()
|
|
66
|
+
return user.right()
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```kotlin
|
|
71
|
+
// Good - return error type
|
|
72
|
+
fun validateEmail(email: String): Either<ValidationError, String> {
|
|
73
|
+
return if (email.contains("@")) {
|
|
74
|
+
email.right()
|
|
75
|
+
} else {
|
|
76
|
+
ValidationError.INVALID_EMAIL.left()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Bad - throwing exceptions
|
|
81
|
+
fun validateEmail(email: String): String {
|
|
82
|
+
if (!email.contains("@")) {
|
|
83
|
+
throw IllegalArgumentException("Invalid email")
|
|
84
|
+
}
|
|
85
|
+
return email
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Exhaustive When Expressions
|
|
90
|
+
|
|
91
|
+
Use exhaustive `when` on enums (no `else` needed if all cases covered):
|
|
92
|
+
```kotlin
|
|
93
|
+
when (provider) {
|
|
94
|
+
OAuthProvider.GOOGLE -> handleGoogle()
|
|
95
|
+
OAuthProvider.GITHUB -> handleGithub()
|
|
96
|
+
OAuthProvider.TWITTER -> handleTwitter()
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Transaction Safety
|
|
101
|
+
|
|
102
|
+
Always return from transaction blocks and handle nullable results:
|
|
103
|
+
```kotlin
|
|
104
|
+
transaction(db.primary) {
|
|
105
|
+
val user = userRepository.byId(userId)
|
|
106
|
+
if (user == null) {
|
|
107
|
+
return@transaction AuthError.USER_NOT_FOUND.asException().left()
|
|
108
|
+
}
|
|
109
|
+
return@transaction Success(user).right()
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Enum Usage
|
|
116
|
+
|
|
117
|
+
**NEVER hardcode strings when an enum exists.** Use `EnumName.VALUE.value` everywhere: return values, fallback values, comparisons, default values.
|
|
118
|
+
|
|
119
|
+
```kotlin
|
|
120
|
+
// WRONG - hardcoded string
|
|
121
|
+
ifLeft = { "critical" }
|
|
122
|
+
val status = "healthy"
|
|
123
|
+
if (value == "at_risk") { ... }
|
|
124
|
+
|
|
125
|
+
// CORRECT - use the enum
|
|
126
|
+
ifLeft = { HealthStatus.CRITICAL.value }
|
|
127
|
+
val status = HealthStatus.HEALTHY.value
|
|
128
|
+
if (value == HealthStatus.AT_RISK.value) { ... }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Why**: Hardcoded strings break silently when enum values change. The compiler can't catch typos in strings.
|
|
132
|
+
|
|
133
|
+
**Known enums**: `HealthStatus`, `ReportType`, `EvaluationMode`, `ProjectRole`, `ProjectStatus`.
|
|
134
|
+
|
|
135
|
+
Convert String -> enum at boundaries (controllers). Use enum references everywhere else.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Conversions
|
|
140
|
+
|
|
141
|
+
Put `companion object { fun from(entity) }` inside Response DTOs. **Never** create separate mapper files or private extension functions.
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
data class UserResponse(val id: UUID, val email: String, val displayName: String) {
|
|
145
|
+
companion object {
|
|
146
|
+
fun from(entity: UserEntity) = UserResponse(
|
|
147
|
+
id = entity.id,
|
|
148
|
+
email = entity.email,
|
|
149
|
+
displayName = entity.displayName ?: entity.email
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When extra data needed, pass as additional params: `fun from(entity, extraData, count)`.
|
|
156
|
+
When extra data needs fetching, use a private manager helper that calls `Response.from()`.
|
|
157
|
+
|
|
158
|
+
**Never do:** separate mapper files, private extension functions in managers, scattered inline mapping.
|
|
159
|
+
|
|
160
|
+
**Location:** Response DTOs go in `module-client/response/`. `module-client` depends on `module-repository`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Style
|
|
165
|
+
|
|
166
|
+
### No Magic Numbers
|
|
167
|
+
|
|
168
|
+
Never hardcode durations, timeouts, limits. Put them in `application.yml` and inject via config class.
|
|
169
|
+
|
|
170
|
+
```kotlin
|
|
171
|
+
// WRONG
|
|
172
|
+
val expiresAt = Instant.now().plus(7, ChronoUnit.DAYS)
|
|
173
|
+
val maxRetries = 3
|
|
174
|
+
|
|
175
|
+
// CORRECT
|
|
176
|
+
val expiresAt = Instant.now().plusSeconds(tokenConfig.refreshTokenExpirationSeconds)
|
|
177
|
+
val maxRetries = appConfig.maxRetries
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### No Inline Fully-Qualified Imports
|
|
181
|
+
|
|
182
|
+
Always use `import` at the top. Never write fully-qualified class names inline.
|
|
183
|
+
|
|
184
|
+
```kotlin
|
|
185
|
+
// WRONG
|
|
186
|
+
val token = java.util.UUID.randomUUID().toString()
|
|
187
|
+
|
|
188
|
+
// CORRECT
|
|
189
|
+
import java.util.UUID
|
|
190
|
+
val token = UUID.randomUUID().toString()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Config Objects Over Individual Fields
|
|
194
|
+
|
|
195
|
+
Inject the config object. Don't unpack fields into separate constructor params.
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// WRONG — too many params, fragile
|
|
199
|
+
class AuthManager(
|
|
200
|
+
private val refreshTokenExpirationSeconds: Long,
|
|
201
|
+
private val passwordResetExpirationSeconds: Long,
|
|
202
|
+
private val emailVerificationExpirationSeconds: Long
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// CORRECT — one config object
|
|
206
|
+
class AuthManager(
|
|
207
|
+
private val tokenExpirationConfig: TokenExpirationConfig
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### JSON Naming
|
|
212
|
+
|
|
213
|
+
Backend uses `snake_case` for all JSON. Jackson does this automatically.
|
|
214
|
+
Kotlin code stays `camelCase` — Jackson converts at serialization time.
|
|
215
|
+
|
|
216
|
+
### Named Arguments
|
|
217
|
+
Use named arguments for 2+ parameters:
|
|
218
|
+
```kotlin
|
|
219
|
+
fn(a = x, b = y)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Comments
|
|
223
|
+
Comments explain **WHY**, not **WHAT**. No KDoc that restates the function name.
|
|
224
|
+
|
|
225
|
+
```kotlin
|
|
226
|
+
// WRONG - useless comments
|
|
227
|
+
/**
|
|
228
|
+
* Default implementation of [UserManager].
|
|
229
|
+
* Provides CRUD operations for users with validation.
|
|
230
|
+
*/
|
|
231
|
+
class DefaultUserManager(...) : UserManager
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validates a configuration value.
|
|
235
|
+
* @param configKey The configuration key
|
|
236
|
+
* @param value The value to validate
|
|
237
|
+
* @return ValidationResult
|
|
238
|
+
*/
|
|
239
|
+
fun validate(configKey: String, value: JsonNode): ValidationResult
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```kotlin
|
|
243
|
+
// CORRECT - no comments needed, code is self-explanatory
|
|
244
|
+
class DefaultUserManager(...) : UserManager
|
|
245
|
+
|
|
246
|
+
fun validate(configKey: String, value: JsonNode): ValidationResult
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```kotlin
|
|
250
|
+
// CORRECT - comments that explain WHY
|
|
251
|
+
// Twilio requires E.164 format, strip any formatting
|
|
252
|
+
val normalized = phoneNumber.replace(Regex("[^+\\d]"), "")
|
|
253
|
+
|
|
254
|
+
// Cache for 5 minutes to reduce DB load during peak hours
|
|
255
|
+
val CACHE_TTL = 5.minutes
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Configuration URLs
|
|
259
|
+
Wrap default URL values in backticks in YAML:
|
|
260
|
+
```yaml
|
|
261
|
+
# Good
|
|
262
|
+
base-url: ${VENDOR_BASE_URL:`https://api.example.com`}
|
|
263
|
+
|
|
264
|
+
# Bad - may cause build errors
|
|
265
|
+
base-url: ${VENDOR_BASE_URL:https://api.example.com}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Formatting
|
|
269
|
+
- **2-space indentation**, no tabs
|
|
270
|
+
- **LF line endings** (Unix style)
|
|
271
|
+
- **120 char** line length guide, 1000 hard limit
|
|
272
|
+
- UTF-8 encoding, final newline always
|
|
273
|
+
- No star imports (wildcard imports disabled)
|
|
274
|
+
- Import order: `*,java.*,javax.*,kotlin.*`
|
|
275
|
+
|
|
276
|
+
```kotlin
|
|
277
|
+
// Braces on same line
|
|
278
|
+
class MyClass {
|
|
279
|
+
fun myFunction() {
|
|
280
|
+
// code
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Multiline parameters - aligned
|
|
285
|
+
fun longFunctionName(
|
|
286
|
+
firstParameter: String,
|
|
287
|
+
secondParameter: Int,
|
|
288
|
+
thirdParameter: Boolean
|
|
289
|
+
) {
|
|
290
|
+
// body
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Enforcement
|
|
295
|
+
```bash
|
|
296
|
+
./gradlew ktlintCheck # Check style
|
|
297
|
+
./gradlew ktlintFormat # Auto-fix style
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Constants Registry
|
|
303
|
+
|
|
304
|
+
Use the right pattern for the right kind of constant:
|
|
305
|
+
|
|
306
|
+
| Use Case | Pattern |
|
|
307
|
+
|----------|---------|
|
|
308
|
+
| Finite, known values (status, role) | `enum class` |
|
|
309
|
+
| Open-ended, growing list (event names, metrics) | `object` with `const val` |
|
|
310
|
+
| Structured paths (S3 keys, URLs) | Type-safe builder class |
|
|
311
|
+
|
|
312
|
+
```kotlin
|
|
313
|
+
// Enums for BOUNDED sets
|
|
314
|
+
enum class ProjectStatus(val value: String) {
|
|
315
|
+
ACTIVE("active"), ARCHIVED("archived")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Object registries for OPEN-ENDED sets
|
|
319
|
+
object ActivityNames {
|
|
320
|
+
const val AGENT_CONVERSATION = "AgentConversation"
|
|
321
|
+
const val PROCESS_DOCUMENT = "ProcessDocument"
|
|
322
|
+
const val SYNC_USER_PROFILE = "SyncUserProfile"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Type-safe builders for STRUCTURED paths
|
|
326
|
+
object StorageKey {
|
|
327
|
+
fun user(userId: UUID) = UserStorageKey(userId)
|
|
328
|
+
|
|
329
|
+
class UserStorageKey(private val userId: UUID) {
|
|
330
|
+
fun avatar() = "user/$userId/avatar"
|
|
331
|
+
fun document(docId: UUID) = "user/$userId/documents/$docId"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Once a registry passes ~50 constants, split by domain with nested objects:
|
|
337
|
+
```kotlin
|
|
338
|
+
object ActivityNames {
|
|
339
|
+
object Agent {
|
|
340
|
+
const val CONVERSATION = "AgentConversation"
|
|
341
|
+
}
|
|
342
|
+
object Document {
|
|
343
|
+
const val PROCESS = "ProcessDocument"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Usage: ActivityNames.Agent.CONVERSATION
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Nested Input/Output Data Classes
|
|
352
|
+
|
|
353
|
+
For internal service contracts, nest `Input`/`Output` inside the class that uses them:
|
|
354
|
+
|
|
355
|
+
```kotlin
|
|
356
|
+
class ProcessDocumentService {
|
|
357
|
+
data class Input(
|
|
358
|
+
val documentId: UUID,
|
|
359
|
+
val userId: UUID,
|
|
360
|
+
val options: ProcessOptions = ProcessOptions()
|
|
361
|
+
)
|
|
362
|
+
data class Output(
|
|
363
|
+
val resultId: UUID,
|
|
364
|
+
val pageCount: Int
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
suspend fun run(input: Input): Either<ClientException, Output> { ... }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Usage is self-documenting:
|
|
371
|
+
val result = service.run(ProcessDocumentService.Input(documentId = id, userId = userId))
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**When to use:** Internal service-to-service contracts, background jobs, batch operations.
|
|
375
|
+
**When NOT to use:** API request/response models — those go in `module-client`.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## No Silent Try-Catch
|
|
380
|
+
|
|
381
|
+
Never catch and swallow exceptions. If you need a fallback, log the failure first.
|
|
382
|
+
|
|
383
|
+
```kotlin
|
|
384
|
+
// WRONG — silent swallow, hides bugs
|
|
385
|
+
val result = try {
|
|
386
|
+
expensiveOperation()
|
|
387
|
+
} catch (e: Exception) {
|
|
388
|
+
fallbackValue // No logging — bug disappears silently
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// CORRECT for non-suspend — log then fallback
|
|
392
|
+
val result = runCatching { blockingOperation() }
|
|
393
|
+
.onFailure { logger.warn("Operation failed, using fallback", it) }
|
|
394
|
+
.getOrDefault(fallbackValue)
|
|
395
|
+
|
|
396
|
+
// CAREFUL — runCatching catches ALL Throwables including CancellationException.
|
|
397
|
+
// In suspend code, this breaks cancellation silently:
|
|
398
|
+
val result = runCatching { suspendFunction() } // WRONG — swallows CancellationException
|
|
399
|
+
.getOrDefault(fallback)
|
|
400
|
+
|
|
401
|
+
// CORRECT for suspend — rethrow CancellationException
|
|
402
|
+
val result = runCatching { suspendFunction() }
|
|
403
|
+
.onFailure { if (it is CancellationException) throw it }
|
|
404
|
+
.onFailure { logger.warn("Operation failed", it) }
|
|
405
|
+
.getOrDefault(fallback)
|
|
406
|
+
|
|
407
|
+
// BEST — use Either, no exceptions at all
|
|
408
|
+
suspend fun doWork(): Either<ClientException, Result> { ... }
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Rules:**
|
|
412
|
+
- Never catch-and-swallow. Log the failure if you need a fallback.
|
|
413
|
+
- `runCatching` is fine for non-suspend code. For suspend functions, always rethrow `CancellationException` or use Either.
|
|
414
|
+
- If you don't need a fallback, use Either and let the caller decide.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Retrofit Client Placement Rules
|
|
2
|
+
|
|
3
|
+
> Full guide: use `/kotlin-best-practices` skill
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Rule
|
|
8
|
+
|
|
9
|
+
**NEVER place Retrofit client interfaces in modules that have kapt enabled for Micronaut.**
|
|
10
|
+
|
|
11
|
+
### Correct Pattern
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
module-client/ # NO kapt
|
|
15
|
+
├── src/main/kotlin/
|
|
16
|
+
│ └── com/yourcompany/client/
|
|
17
|
+
│ ├── TemplateServiceClient.kt ✅ Retrofit @GET/@POST
|
|
18
|
+
│ ├── TerraformServiceClient.kt ✅ Retrofit interfaces
|
|
19
|
+
│ └── dto/
|
|
20
|
+
│ ├── template/
|
|
21
|
+
│ │ └── TemplateDtos.kt ✅ Plain data classes
|
|
22
|
+
│ └── terraform/
|
|
23
|
+
│ └── TerraformDtos.kt ✅ No Micronaut annotations
|
|
24
|
+
└── build.gradle
|
|
25
|
+
# NO kotlin-kapt plugin ✅
|
|
26
|
+
dependencies {
|
|
27
|
+
implementation(libs.networking.retrofit)
|
|
28
|
+
implementation(libs.networking.okhttp)
|
|
29
|
+
implementation(libs.arrow.core)
|
|
30
|
+
implementation(libs.jackson.*)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Incorrect Pattern
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
module-auth/ # HAS kapt enabled
|
|
38
|
+
├── src/main/kotlin/
|
|
39
|
+
│ └── com/yourcompany/auth/
|
|
40
|
+
│ ├── client/
|
|
41
|
+
│ │ ├── TemplateServiceClient.kt ❌ WILL FAIL
|
|
42
|
+
│ │ └── TerraformServiceClient.kt ❌ kapt can't process
|
|
43
|
+
│ └── dto/
|
|
44
|
+
│ └── TemplateDtos.kt ❌ @Serdeable conflicts
|
|
45
|
+
└── build.gradle
|
|
46
|
+
plugins {
|
|
47
|
+
id("kotlin-kapt") ❌ PROBLEM
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Why This Fails
|
|
54
|
+
|
|
55
|
+
### Kapt Limitations
|
|
56
|
+
|
|
57
|
+
1. **Type Alias Issues**
|
|
58
|
+
```kotlin
|
|
59
|
+
// Kapt can't handle this in stub generation:
|
|
60
|
+
typealias EitherCall<R> = Call<Either<ErrorResponse, R>>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. **Complex Generic Types**
|
|
64
|
+
```kotlin
|
|
65
|
+
// Kapt fails to resolve nested generics:
|
|
66
|
+
fun getStatus(): Call<Either<ErrorResponse, TerraformStatusDto>>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. **Annotation Processor Conflicts**
|
|
70
|
+
- Micronaut processors try to process Retrofit interfaces
|
|
71
|
+
- Results in `@error.NonExistentClass()` in generated stubs
|
|
72
|
+
- Build fails with "incompatible types" errors
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Implementation Checklist
|
|
77
|
+
|
|
78
|
+
When adding new external service clients:
|
|
79
|
+
|
|
80
|
+
### 1. Create Client Interface in module-client
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
package com.yourcompany.client
|
|
84
|
+
|
|
85
|
+
import arrow.core.Either
|
|
86
|
+
import com.yourcompany.retrofit.ErrorResponse
|
|
87
|
+
import retrofit2.Call
|
|
88
|
+
import retrofit2.http.*
|
|
89
|
+
|
|
90
|
+
interface MyServiceClient {
|
|
91
|
+
@GET("/api/resource")
|
|
92
|
+
fun getResource(): Call<Either<ErrorResponse, ResourceDto>>
|
|
93
|
+
|
|
94
|
+
@POST("/api/resource")
|
|
95
|
+
fun createResource(@Body request: CreateRequestDto): Call<Either<ErrorResponse, ResourceDto>>
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Key Points:**
|
|
100
|
+
- ✅ Package: `com.yourcompany.client` (NOT `com.yourcompany.auth.client`)
|
|
101
|
+
- ✅ Use full type: `Call<Either<ErrorResponse, T>>` (NOT `EitherCall<T>`)
|
|
102
|
+
- ✅ Explicit imports (NOT wildcard `import com.yourcompany.client.dto.*`)
|
|
103
|
+
|
|
104
|
+
### 2. Create DTOs in module-client
|
|
105
|
+
|
|
106
|
+
```kotlin
|
|
107
|
+
package com.yourcompany.client.dto.myservice
|
|
108
|
+
|
|
109
|
+
// NO Micronaut annotations! ❌ @Serdeable
|
|
110
|
+
data class ResourceDto(
|
|
111
|
+
val id: String,
|
|
112
|
+
val name: String,
|
|
113
|
+
val createdAt: Instant
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
data class CreateRequestDto(
|
|
117
|
+
val name: String,
|
|
118
|
+
val options: Map<String, Any>
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Key Points:**
|
|
123
|
+
- ✅ Plain Kotlin data classes
|
|
124
|
+
- ✅ NO `@Serdeable` annotation
|
|
125
|
+
- ✅ Jackson handles serialization automatically
|
|
126
|
+
- ✅ Package: `com.yourcompany.client.dto.*`
|
|
127
|
+
|
|
128
|
+
### 3. Create Client Factory in module-auth
|
|
129
|
+
|
|
130
|
+
```kotlin
|
|
131
|
+
package com.yourcompany.auth.config
|
|
132
|
+
|
|
133
|
+
import com.yourcompany.client.MyServiceClient
|
|
134
|
+
import io.micronaut.context.annotation.Factory
|
|
135
|
+
import jakarta.inject.Singleton
|
|
136
|
+
|
|
137
|
+
@Factory
|
|
138
|
+
class MyServiceConfig(
|
|
139
|
+
@Value("\${app.myservice.url}")
|
|
140
|
+
private val serviceUrl: String,
|
|
141
|
+
private val objectMapper: ObjectMapper
|
|
142
|
+
) {
|
|
143
|
+
|
|
144
|
+
@Singleton
|
|
145
|
+
fun myServiceClient(): MyServiceClient {
|
|
146
|
+
val retrofit = Retrofits.createRetrofit(
|
|
147
|
+
baseUrl = serviceUrl.toHttpUrl(),
|
|
148
|
+
mapper = objectMapper
|
|
149
|
+
)
|
|
150
|
+
return retrofit.create(MyServiceClient::class.java)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Key Points:**
|
|
156
|
+
- ✅ Factory is in module-auth (has kapt for `@Factory`)
|
|
157
|
+
- ✅ Client interface is in module-client (no kapt)
|
|
158
|
+
- ✅ Clean separation of concerns
|
|
159
|
+
|
|
160
|
+
### 4. Update module-auth Dependencies
|
|
161
|
+
|
|
162
|
+
```groovy
|
|
163
|
+
// module-auth/build.gradle
|
|
164
|
+
|
|
165
|
+
dependencies {
|
|
166
|
+
// Client module (contains Retrofit interfaces)
|
|
167
|
+
implementation(project(":module-client"))
|
|
168
|
+
|
|
169
|
+
// Retrofit runtime (needed for creating clients in factories)
|
|
170
|
+
implementation(libs.networking.retrofit)
|
|
171
|
+
implementation(libs.networking.retrofit.jackson)
|
|
172
|
+
implementation(libs.networking.okhttp)
|
|
173
|
+
implementation(libs.networking.okhttp.logging)
|
|
174
|
+
|
|
175
|
+
// Arrow (for Either type)
|
|
176
|
+
implementation(libs.arrow.core)
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Troubleshooting
|
|
183
|
+
|
|
184
|
+
### Error: `@error.NonExistentClass()`
|
|
185
|
+
|
|
186
|
+
**Cause:** Retrofit client interface in a module with kapt enabled
|
|
187
|
+
|
|
188
|
+
**Solution:** Move client to `module-client`
|
|
189
|
+
|
|
190
|
+
### Error: `Unresolved reference 'Serdeable'`
|
|
191
|
+
|
|
192
|
+
**Cause:** DTOs use Micronaut annotation but module lacks dependency
|
|
193
|
+
|
|
194
|
+
**Solution:** Remove `@Serdeable` - Jackson handles it automatically
|
|
195
|
+
|
|
196
|
+
### Error: `when expression must be exhaustive`
|
|
197
|
+
|
|
198
|
+
**Cause:** Nullable enum in when expression
|
|
199
|
+
|
|
200
|
+
**Solution:**
|
|
201
|
+
```kotlin
|
|
202
|
+
// ❌ Before:
|
|
203
|
+
when (task.automationType) { ... }
|
|
204
|
+
|
|
205
|
+
// ✅ After:
|
|
206
|
+
val type = task.automationType ?: return ...
|
|
207
|
+
when (type) { ... }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Module Architecture
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
backend/
|
|
216
|
+
├── app/
|
|
217
|
+
│ ├── module-auth/ # Micronaut controllers/services
|
|
218
|
+
│ │ ├── build.gradle # HAS kapt
|
|
219
|
+
│ │ ├── controller/ # @Controller, @Get, @Post
|
|
220
|
+
│ │ ├── service/ # @Singleton services
|
|
221
|
+
│ │ └── config/ # @Factory for clients
|
|
222
|
+
│ │
|
|
223
|
+
│ └── module-client/ # Retrofit clients ONLY
|
|
224
|
+
│ ├── build.gradle # NO kapt ✅
|
|
225
|
+
│ └── src/.../client/ # Retrofit interfaces
|
|
226
|
+
│
|
|
227
|
+
└── core/
|
|
228
|
+
└── module-retrofit/ # Shared Retrofit utilities
|
|
229
|
+
└── EitherCall.kt # Type aliases, adapters
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Benefits
|
|
235
|
+
|
|
236
|
+
1. **Clean Compilation**
|
|
237
|
+
- Kapt only processes Micronaut beans
|
|
238
|
+
- Retrofit clients compile with standard Kotlin compiler
|
|
239
|
+
|
|
240
|
+
2. **Clear Separation**
|
|
241
|
+
- Micronaut infrastructure ↔ External API clients
|
|
242
|
+
- Easy to test and mock
|
|
243
|
+
|
|
244
|
+
3. **Reusability**
|
|
245
|
+
- Clients can be used in other modules
|
|
246
|
+
- No Micronaut dependency pollution
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Related Rules
|
|
251
|
+
|
|
252
|
+
- `KOTLIN.md` — Null safety, no force unwraps
|
|
253
|
+
- `NAMING_CONVENTIONS.md` — Package naming
|
|
254
|
+
- `SCHEMA.md` — Repository patterns
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Always Retrofit Over Raw OkHttp
|
|
261
|
+
|
|
262
|
+
Always prefer Retrofit interfaces over raw OkHttp for HTTP clients. ~80% less boilerplate.
|
|
263
|
+
|
|
264
|
+
```kotlin
|
|
265
|
+
// WRONG — 198 lines of OkHttp boilerplate per client
|
|
266
|
+
class DefaultSearchClient(private val okHttpClient: OkHttpClient) {
|
|
267
|
+
fun search(query: String): SearchResult {
|
|
268
|
+
val request = Request.Builder()
|
|
269
|
+
.url("$baseUrl/search")
|
|
270
|
+
.post(objectMapper.writeValueAsString(body).toRequestBody())
|
|
271
|
+
.addHeader("Authorization", "Bearer $apiKey")
|
|
272
|
+
.build()
|
|
273
|
+
val response = okHttpClient.newCall(request).execute()
|
|
274
|
+
// 40+ lines of response parsing, error handling...
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// CORRECT — 25 lines, Retrofit handles the plumbing
|
|
279
|
+
interface SearchClient {
|
|
280
|
+
@POST("/search")
|
|
281
|
+
suspend fun search(
|
|
282
|
+
@Header("Authorization") auth: String,
|
|
283
|
+
@Body request: SearchRequest
|
|
284
|
+
): Response<SearchResult>
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
**TL;DR:** Retrofit clients → `module-client` (no kapt). Micronaut beans → `module-auth` (has kapt). Never mix. Always Retrofit, never raw OkHttp.
|