@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.
Files changed (255) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/README.md +439 -0
  4. package/VERSION +1 -0
  5. package/agents/design-critic.md +127 -0
  6. package/agents/idea-killer.md +72 -0
  7. package/agents/infrastructure-expert.md +49 -0
  8. package/agents/micronaut-backend-expert.md +45 -0
  9. package/agents/phase-reviewer.md +150 -0
  10. package/agents/research-planner.md +70 -0
  11. package/agents/solution-architect-cto.md +49 -0
  12. package/agents/sre-architect.md +49 -0
  13. package/agents/team-coordinator.md +111 -0
  14. package/bin/cli.js +780 -0
  15. package/claude-md/00-header.md +39 -0
  16. package/claude-md/01-core.md +105 -0
  17. package/claude-md/05-database.md +20 -0
  18. package/claude-md/11-backend-micronaut.md +19 -0
  19. package/claude-md/20-frontend-react.md +44 -0
  20. package/claude-md/25-ux-design.md +56 -0
  21. package/claude-md/30-infrastructure.md +24 -0
  22. package/claude-md/30-project-mgmt.md +119 -0
  23. package/claude-md/40-product.md +39 -0
  24. package/claude-md/50-ops.md +34 -0
  25. package/claude-md/60-research.md +27 -0
  26. package/claude-md/90-footer.md +21 -0
  27. package/commands/spartan/brainstorm.md +134 -0
  28. package/commands/spartan/brownfield.md +157 -0
  29. package/commands/spartan/build.md +435 -0
  30. package/commands/spartan/careful.md +94 -0
  31. package/commands/spartan/commit-message.md +112 -0
  32. package/commands/spartan/content.md +17 -0
  33. package/commands/spartan/context-save.md +161 -0
  34. package/commands/spartan/contribute.md +140 -0
  35. package/commands/spartan/daily.md +42 -0
  36. package/commands/spartan/debug.md +308 -0
  37. package/commands/spartan/deep-dive.md +55 -0
  38. package/commands/spartan/deploy.md +207 -0
  39. package/commands/spartan/e2e.md +264 -0
  40. package/commands/spartan/env-setup.md +166 -0
  41. package/commands/spartan/epic.md +199 -0
  42. package/commands/spartan/fe-review.md +181 -0
  43. package/commands/spartan/figma-to-code.md +260 -0
  44. package/commands/spartan/forensics.md +46 -0
  45. package/commands/spartan/freeze.md +84 -0
  46. package/commands/spartan/fundraise.md +53 -0
  47. package/commands/spartan/gate-review.md +229 -0
  48. package/commands/spartan/gsd-upgrade.md +376 -0
  49. package/commands/spartan/guard.md +42 -0
  50. package/commands/spartan/init-project.md +178 -0
  51. package/commands/spartan/init-rules.md +298 -0
  52. package/commands/spartan/interview.md +154 -0
  53. package/commands/spartan/kickoff.md +73 -0
  54. package/commands/spartan/kotlin-service.md +109 -0
  55. package/commands/spartan/lean-canvas.md +222 -0
  56. package/commands/spartan/lint-rules.md +122 -0
  57. package/commands/spartan/map-codebase.md +124 -0
  58. package/commands/spartan/migration.md +82 -0
  59. package/commands/spartan/next-app.md +317 -0
  60. package/commands/spartan/next-feature.md +212 -0
  61. package/commands/spartan/onboard.md +326 -0
  62. package/commands/spartan/outreach.md +16 -0
  63. package/commands/spartan/phase.md +142 -0
  64. package/commands/spartan/pitch.md +18 -0
  65. package/commands/spartan/plan.md +210 -0
  66. package/commands/spartan/pr-ready.md +202 -0
  67. package/commands/spartan/project.md +106 -0
  68. package/commands/spartan/qa.md +222 -0
  69. package/commands/spartan/research.md +254 -0
  70. package/commands/spartan/review.md +132 -0
  71. package/commands/spartan/scan-rules.md +173 -0
  72. package/commands/spartan/sessions.md +143 -0
  73. package/commands/spartan/spec.md +131 -0
  74. package/commands/spartan/startup.md +257 -0
  75. package/commands/spartan/team.md +570 -0
  76. package/commands/spartan/teardown.md +161 -0
  77. package/commands/spartan/testcontainer.md +97 -0
  78. package/commands/spartan/tf-cost.md +123 -0
  79. package/commands/spartan/tf-deploy.md +116 -0
  80. package/commands/spartan/tf-drift.md +100 -0
  81. package/commands/spartan/tf-import.md +107 -0
  82. package/commands/spartan/tf-module.md +121 -0
  83. package/commands/spartan/tf-plan.md +100 -0
  84. package/commands/spartan/tf-review.md +106 -0
  85. package/commands/spartan/tf-scaffold.md +109 -0
  86. package/commands/spartan/tf-security.md +147 -0
  87. package/commands/spartan/think.md +221 -0
  88. package/commands/spartan/unfreeze.md +13 -0
  89. package/commands/spartan/update.md +134 -0
  90. package/commands/spartan/ux.md +1233 -0
  91. package/commands/spartan/validate.md +193 -0
  92. package/commands/spartan/web-to-prd.md +706 -0
  93. package/commands/spartan/workstreams.md +109 -0
  94. package/commands/spartan/write.md +16 -0
  95. package/commands/spartan.md +386 -0
  96. package/frameworks/00-framework-comparison-guide.md +317 -0
  97. package/frameworks/01-lean-canvas.md +196 -0
  98. package/frameworks/02-design-sprint.md +304 -0
  99. package/frameworks/03-foundation-sprint.md +337 -0
  100. package/frameworks/04-business-model-canvas.md +391 -0
  101. package/frameworks/05-customer-development.md +426 -0
  102. package/frameworks/06-jobs-to-be-done.md +358 -0
  103. package/frameworks/07-mom-test.md +392 -0
  104. package/frameworks/08-value-proposition-canvas.md +488 -0
  105. package/frameworks/09-javelin-board.md +428 -0
  106. package/frameworks/10-build-measure-learn.md +467 -0
  107. package/frameworks/11-mvp-approaches.md +533 -0
  108. package/frameworks/think-before-build.md +593 -0
  109. package/lib/assembler.js +197 -0
  110. package/lib/assembler.test.js +159 -0
  111. package/lib/detector.js +166 -0
  112. package/lib/detector.test.js +221 -0
  113. package/lib/packs.js +16 -0
  114. package/lib/resolver.js +272 -0
  115. package/lib/resolver.test.js +298 -0
  116. package/lib/worktree.sh +104 -0
  117. package/package.json +50 -0
  118. package/packs/backend-micronaut.yaml +35 -0
  119. package/packs/backend-nodejs.yaml +15 -0
  120. package/packs/backend-python.yaml +15 -0
  121. package/packs/core.yaml +37 -0
  122. package/packs/database.yaml +21 -0
  123. package/packs/frontend-react.yaml +24 -0
  124. package/packs/infrastructure.yaml +40 -0
  125. package/packs/ops.yaml +16 -0
  126. package/packs/packs.compiled.json +371 -0
  127. package/packs/product.yaml +22 -0
  128. package/packs/project-mgmt.yaml +24 -0
  129. package/packs/research.yaml +39 -0
  130. package/packs/shared-backend.yaml +14 -0
  131. package/packs/ux-design.yaml +21 -0
  132. package/rules/backend-micronaut/API_DESIGN.md +313 -0
  133. package/rules/backend-micronaut/BATCH_PROCESSING.md +92 -0
  134. package/rules/backend-micronaut/CONTROLLERS.md +388 -0
  135. package/rules/backend-micronaut/KOTLIN.md +414 -0
  136. package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +290 -0
  137. package/rules/backend-micronaut/SERVICES_AND_BEANS.md +325 -0
  138. package/rules/core/NAMING_CONVENTIONS.md +208 -0
  139. package/rules/core/SKILL_AUTHORING.md +174 -0
  140. package/rules/core/TIMEZONE.md +316 -0
  141. package/rules/database/ORM_AND_REPO.md +289 -0
  142. package/rules/database/SCHEMA.md +146 -0
  143. package/rules/database/TRANSACTIONS.md +311 -0
  144. package/rules/frontend-react/FRONTEND.md +344 -0
  145. package/rules/infrastructure/MODULES.md +260 -0
  146. package/rules/infrastructure/NAMING.md +196 -0
  147. package/rules/infrastructure/PROVIDERS.md +309 -0
  148. package/rules/infrastructure/SECURITY.md +310 -0
  149. package/rules/infrastructure/STATE_AND_BACKEND.md +237 -0
  150. package/rules/infrastructure/STRUCTURE.md +234 -0
  151. package/rules/infrastructure/VARIABLES.md +285 -0
  152. package/rules/shared-backend/ARCHITECTURE.md +46 -0
  153. package/rules/ux-design/DESIGN_PROCESS.md +176 -0
  154. package/skills/api-endpoint-creator/SKILL.md +455 -0
  155. package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
  156. package/skills/api-endpoint-creator/examples.md +522 -0
  157. package/skills/api-endpoint-creator/testing-patterns.md +302 -0
  158. package/skills/article-writing/SKILL.md +109 -0
  159. package/skills/article-writing/examples.md +59 -0
  160. package/skills/backend-api-design/SKILL.md +84 -0
  161. package/skills/backend-api-design/code-patterns.md +138 -0
  162. package/skills/brainstorm/SKILL.md +95 -0
  163. package/skills/browser-qa/SKILL.md +87 -0
  164. package/skills/browser-qa/playwright-snippets.md +110 -0
  165. package/skills/ci-cd-patterns/SKILL.md +108 -0
  166. package/skills/ci-cd-patterns/workflows.md +149 -0
  167. package/skills/competitive-teardown/SKILL.md +93 -0
  168. package/skills/competitive-teardown/example-analysis.md +50 -0
  169. package/skills/content-engine/SKILL.md +131 -0
  170. package/skills/content-engine/examples.md +72 -0
  171. package/skills/database-patterns/SKILL.md +72 -0
  172. package/skills/database-patterns/code-templates.md +114 -0
  173. package/skills/database-table-creator/SKILL.md +141 -0
  174. package/skills/database-table-creator/examples.md +552 -0
  175. package/skills/database-table-creator/kotlin-templates.md +400 -0
  176. package/skills/database-table-creator/migration-template.sql +68 -0
  177. package/skills/database-table-creator/validation-checklist.md +337 -0
  178. package/skills/deep-research/SKILL.md +80 -0
  179. package/skills/design-intelligence/SKILL.md +268 -0
  180. package/skills/design-workflow/SKILL.md +127 -0
  181. package/skills/design-workflow/checklists.md +45 -0
  182. package/skills/idea-validation/SKILL.md +129 -0
  183. package/skills/idea-validation/example-report.md +50 -0
  184. package/skills/investor-materials/SKILL.md +122 -0
  185. package/skills/investor-materials/example-outline.md +70 -0
  186. package/skills/investor-outreach/SKILL.md +112 -0
  187. package/skills/investor-outreach/examples.md +76 -0
  188. package/skills/kotlin-best-practices/SKILL.md +58 -0
  189. package/skills/kotlin-best-practices/code-patterns.md +132 -0
  190. package/skills/market-research/SKILL.md +99 -0
  191. package/skills/security-checklist/SKILL.md +65 -0
  192. package/skills/security-checklist/audit-reference.md +95 -0
  193. package/skills/service-debugging/SKILL.md +116 -0
  194. package/skills/service-debugging/common-issues.md +65 -0
  195. package/skills/startup-pipeline/SKILL.md +152 -0
  196. package/skills/terraform-best-practices/SKILL.md +244 -0
  197. package/skills/terraform-module-creator/SKILL.md +284 -0
  198. package/skills/terraform-review/SKILL.md +222 -0
  199. package/skills/terraform-security-audit/SKILL.md +280 -0
  200. package/skills/terraform-service-scaffold/SKILL.md +574 -0
  201. package/skills/testing-strategies/SKILL.md +116 -0
  202. package/skills/testing-strategies/examples.md +103 -0
  203. package/skills/testing-strategies/integration-test-setup.md +71 -0
  204. package/skills/ui-ux-pro-max/SKILL.md +238 -0
  205. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  206. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  207. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  208. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  209. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  210. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  211. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  212. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  213. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  214. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  215. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  216. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  217. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  218. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  219. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  220. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  221. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  222. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  223. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  224. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  225. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  226. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  227. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  228. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  229. package/skills/ui-ux-pro-max/python-setup.md +146 -0
  230. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  231. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  232. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
  233. package/skills/web-to-prd/SKILL.md +478 -0
  234. package/templates/build-config.yaml +44 -0
  235. package/templates/commands-config.yaml +55 -0
  236. package/templates/competitor-analysis.md +60 -0
  237. package/templates/content/AGENT_TEMPLATE.md +47 -0
  238. package/templates/content/COMMAND_TEMPLATE.md +27 -0
  239. package/templates/content/RULE_TEMPLATE.md +40 -0
  240. package/templates/content/SKILL_TEMPLATE.md +41 -0
  241. package/templates/design-config.md +105 -0
  242. package/templates/design-doc.md +207 -0
  243. package/templates/epic.md +100 -0
  244. package/templates/feature-spec.md +181 -0
  245. package/templates/idea-canvas.md +47 -0
  246. package/templates/implementation-plan.md +159 -0
  247. package/templates/prd-template.md +86 -0
  248. package/templates/preamble.md +89 -0
  249. package/templates/project-readme.md +35 -0
  250. package/templates/quality-gates.md +230 -0
  251. package/templates/spartan-config.yaml +164 -0
  252. package/templates/user-interview.md +69 -0
  253. package/templates/validation-checklist.md +108 -0
  254. package/templates/workflow-backend-micronaut.md +409 -0
  255. package/templates/workflow-frontend-react.md +233 -0
@@ -0,0 +1,316 @@
1
+ # Timezone Rules
2
+
3
+ ## One Rule: Everything is UTC
4
+
5
+ **Server stores UTC. API sends UTC. API receives UTC. No exceptions.**
6
+
7
+ The frontend is the only place that converts to/from local time — for display only.
8
+
9
+ ```
10
+ Database (TIMESTAMPTZ/UTC) → Backend (Instant/UTC) → API JSON (ISO 8601 Z) → Frontend (UTC) → Display (local)
11
+ ← Send (local → UTC) ← Input
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Database
17
+
18
+ ### Use `TIMESTAMPTZ` — Not `TIMESTAMP`
19
+
20
+ **Always use `TIMESTAMPTZ` (with timezone).** PostgreSQL docs and wiki both say this.
21
+
22
+ Why: `TIMESTAMPTZ` converts to UTC on insert and converts back on read. If a connection has a non-UTC session timezone (DBA tools, connection pool quirks, migration scripts), `TIMESTAMPTZ` still stores the correct UTC value. `TIMESTAMP` without timezone silently stores whatever you give it — if the session isn't UTC, you get wrong data and can't tell.
23
+
24
+ ```sql
25
+ -- CORRECT — TIMESTAMPTZ is the safe default
26
+ CREATE TABLE events (
27
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
28
+ starts_at TIMESTAMPTZ NOT NULL,
29
+ ends_at TIMESTAMPTZ NOT NULL,
30
+ created_at TIMESTAMPTZ DEFAULT NOW(),
31
+ updated_at TIMESTAMPTZ,
32
+ deleted_at TIMESTAMPTZ
33
+ );
34
+
35
+ -- WRONG — TIMESTAMP without timezone is fragile
36
+ CREATE TABLE events (
37
+ id UUID PRIMARY KEY,
38
+ starts_at TIMESTAMP NOT NULL, -- Breaks if session timezone isn't UTC
39
+ created_at TIMESTAMP DEFAULT NOW()
40
+ );
41
+ ```
42
+
43
+ Both types use 8 bytes internally. No storage difference.
44
+
45
+ ### Server Must Run in UTC
46
+
47
+ The database server, application server, and all containers must run in UTC:
48
+
49
+ ```yaml
50
+ # application.yml
51
+ datasources:
52
+ default:
53
+ connection-properties:
54
+ timezone: UTC
55
+ ```
56
+
57
+ ```sql
58
+ -- PostgreSQL: verify
59
+ SHOW timezone; -- Should return 'UTC'
60
+ ```
61
+
62
+ ```dockerfile
63
+ # Dockerfile
64
+ ENV TZ=UTC
65
+ ```
66
+
67
+ ```yaml
68
+ # Kubernetes pod spec
69
+ env:
70
+ - name: TZ
71
+ value: "UTC"
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Backend (Kotlin)
77
+
78
+ ### Always Use `Instant` — Never `LocalDateTime`
79
+
80
+ `Instant` is UTC by definition. `LocalDateTime` has no timezone info and causes bugs.
81
+
82
+ ```kotlin
83
+ // CORRECT — Instant is always UTC
84
+ val now: Instant = Instant.now()
85
+ val expiresAt: Instant = Instant.now().plusSeconds(3600)
86
+
87
+ // WRONG — LocalDateTime has no timezone, ambiguous
88
+ val now: LocalDateTime = LocalDateTime.now() // What timezone? Nobody knows.
89
+ ```
90
+
91
+ ### `ZonedDateTime` — Only at Computation Boundaries
92
+
93
+ Never put `ZonedDateTime` in entities, DTOs, or API payloads. It's OK for:
94
+ - Scheduling logic (computing "next 9 AM in user's timezone")
95
+ - DST-aware date arithmetic ("add 1 day" at DST boundary)
96
+ - Generating reports in a specific timezone
97
+
98
+ Always convert back to `Instant` before passing to other layers.
99
+
100
+ ```kotlin
101
+ // CORRECT — entities and DTOs use Instant
102
+ data class UserEntity(
103
+ val createdAt: Instant,
104
+ val lastLoginAt: Instant?,
105
+ val subscriptionExpiresAt: Instant?
106
+ )
107
+
108
+ // CORRECT — ZonedDateTime only for scheduling computation
109
+ fun nextNotificationTime(userTimezone: String, localTime: LocalTime): Instant {
110
+ val zone = ZoneId.of(userTimezone)
111
+ val nextLocal = ZonedDateTime.now(zone).with(localTime)
112
+ return nextLocal.toInstant() // Convert back to Instant
113
+ }
114
+
115
+ // WRONG — ZonedDateTime in entity
116
+ data class UserEntity(
117
+ val createdAt: ZonedDateTime // NO — keep entities in Instant
118
+ )
119
+ ```
120
+
121
+ ### Jackson Serialization
122
+
123
+ Jackson must serialize `Instant` as ISO 8601 with the `Z` suffix:
124
+
125
+ ```kotlin
126
+ objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
127
+ // Output: "2024-01-15T10:30:00Z"
128
+ ```
129
+
130
+ Never output offsets like `+07:00` or timezone names in API responses.
131
+
132
+ ---
133
+
134
+ ## API Contract
135
+
136
+ ### All Datetime Fields Are ISO 8601 UTC
137
+
138
+ ```json
139
+ {
140
+ "created_at": "2024-01-15T10:30:00Z",
141
+ "updated_at": "2024-01-15T14:22:33Z",
142
+ "expires_at": "2024-02-15T00:00:00Z"
143
+ }
144
+ ```
145
+
146
+ ### Request Bodies — Frontend Sends UTC
147
+
148
+ ```json
149
+ {
150
+ "starts_at": "2024-01-20T09:00:00Z",
151
+ "ends_at": "2024-01-20T17:00:00Z"
152
+ }
153
+ ```
154
+
155
+ ### Query Parameters — Also UTC
156
+
157
+ ```
158
+ GET /events?from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z
159
+ ```
160
+
161
+ ### No Timezone Fields in Timestamp Payloads
162
+
163
+ Don't put timezone info alongside timestamps. The exception is user preferences (see below).
164
+
165
+ ```json
166
+ // WRONG — timezone alongside a timestamp
167
+ { "starts_at": "2024-01-20T09:00:00Z", "timezone": "America/New_York" }
168
+
169
+ // CORRECT — just UTC
170
+ { "starts_at": "2024-01-20T09:00:00Z" }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Frontend
176
+
177
+ ### Receive UTC, Convert for Display
178
+
179
+ ```typescript
180
+ // Convert at display time
181
+ function formatDate(utcString: string): string {
182
+ return new Intl.DateTimeFormat(undefined, {
183
+ dateStyle: 'medium',
184
+ timeStyle: 'short',
185
+ }).format(new Date(utcString))
186
+ }
187
+ ```
188
+
189
+ ### Send UTC to Server
190
+
191
+ ```typescript
192
+ // Convert local input to UTC before API call
193
+ const localDate = new Date(userInput)
194
+ const utcString = localDate.toISOString() // "2024-01-20T02:00:00.000Z"
195
+
196
+ await api.post('/events', { startsAt: utcString })
197
+
198
+ // WRONG — no Z suffix, ambiguous
199
+ await api.post('/events', { startsAt: '2024-01-20T09:00:00' })
200
+ ```
201
+
202
+ ### Use Browser Timezone at Render Time
203
+
204
+ Don't track the user's timezone in frontend state. The browser already knows it.
205
+
206
+ ```typescript
207
+ // CORRECT — use at render time
208
+ const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone
209
+
210
+ // WRONG — storing timezone in state
211
+ const [timezone, setTimezone] = useState('America/New_York')
212
+ ```
213
+
214
+ ---
215
+
216
+ ## When You DO Need Timezone
217
+
218
+ There are cases where storing a user's IANA timezone is correct. The rule is:
219
+
220
+ **Past events (created_at, login_at, order_placed_at):** Never store timezone. `TIMESTAMPTZ` (UTC) is enough.
221
+
222
+ **User preferences (notification time, business hours):** Store the user's IANA timezone as a separate column. Don't mix it with timestamps.
223
+
224
+ ```sql
225
+ -- CORRECT — timezone as a user preference, not part of timestamps
226
+ CREATE TABLE user_preferences (
227
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
228
+ user_id UUID NOT NULL,
229
+ timezone TEXT NOT NULL DEFAULT 'UTC', -- IANA timezone: 'America/New_York'
230
+ notification_time TEXT NOT NULL DEFAULT '09:00', -- local time, not a timestamp
231
+ created_at TIMESTAMPTZ DEFAULT NOW()
232
+ );
233
+
234
+ -- Then compute UTC fire time dynamically in code:
235
+ -- nextFire = ZonedDateTime.of(today, LocalTime.parse("09:00"), ZoneId.of("America/New_York")).toInstant()
236
+ ```
237
+
238
+ **Why dynamic computation?** Because DST shifts change the UTC offset. "9 AM New York" is `14:00 UTC` in winter but `13:00 UTC` in summer. Storing a fixed UTC value would drift by an hour.
239
+
240
+ **Never use fixed offsets as timezone identifiers.** `+05:30` is an offset, not a timezone. It changes with DST. Use IANA names: `Asia/Kolkata`, `America/New_York`.
241
+
242
+ ---
243
+
244
+ ## Microservices
245
+
246
+ ### Inter-Service Communication
247
+
248
+ All service-to-service datetime fields use ISO 8601 UTC, same as external APIs.
249
+
250
+ ### Event Streaming (Kafka, RabbitMQ)
251
+
252
+ - Use epoch-based types: Avro `timestamp-millis`, Protobuf `google.protobuf.Timestamp`
253
+ - These are UTC by definition — no timezone ambiguity
254
+ - Document in your schema that all timestamps are UTC epoch
255
+
256
+ ### Logging
257
+
258
+ All services must log in UTC. If services in different timezones log in local time, correlating logs across services is a nightmare.
259
+
260
+ ```xml
261
+ <!-- logback.xml — force UTC -->
262
+ <timestamp key="timestamp" datePattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" timeReference="UTC"/>
263
+ ```
264
+
265
+ ### Distributed Tracing
266
+
267
+ OpenTelemetry spans use nanosecond UTC timestamps internally. No action needed — but make sure NTP is configured on all nodes. Clock skew (not timezone) is the bigger concern.
268
+
269
+ ### Cron / Scheduler Jobs
270
+
271
+ Cron expressions are timezone-sensitive. DST transitions can cause jobs to fire twice, or not at all, in the 1-3 AM window.
272
+
273
+ ```kotlin
274
+ // CORRECT — schedule in UTC to avoid DST issues
275
+ @Scheduled(cron = "0 0 14 * * *", zone = "UTC") // 2 PM UTC, not "2 PM local"
276
+ fun dailyDigest() { ... }
277
+
278
+ // If the job MUST fire at local wall-clock time, use IANA timezone explicitly:
279
+ @Scheduled(cron = "0 0 9 * * *", zone = "America/New_York") // 9 AM New York, DST-aware
280
+ fun morningNotification() { ... }
281
+ ```
282
+
283
+ Avoid scheduling jobs in the 1:00-3:00 AM local time window for any timezone with DST.
284
+
285
+ ### IANA Timezone Database Updates
286
+
287
+ Governments change DST rules. Your JVM and OS timezone databases need updating. If you run long-lived JVMs, update the JDK or use the TZUpdater tool.
288
+
289
+ ---
290
+
291
+ ## Quick Reference
292
+
293
+ | Layer | Type | Format | Example |
294
+ |-------|------|--------|---------|
295
+ | Database | Column type | `TIMESTAMPTZ` | `2024-01-15 10:30:00+00` |
296
+ | Backend (Kotlin) | Property type | `Instant` | `Instant.now()` |
297
+ | API JSON | String | ISO 8601 + Z | `"2024-01-15T10:30:00Z"` |
298
+ | Frontend (receive) | Parse | `new Date(utcString)` | `new Date("2024-01-15T10:30:00Z")` |
299
+ | Frontend (display) | Format | `Intl.DateTimeFormat` | `"Jan 15, 2024, 5:30 PM"` |
300
+ | Frontend (send) | Serialize | `toISOString()` | `"2024-01-15T10:30:00.000Z"` |
301
+ | Events (Kafka) | Type | epoch millis | `1705312200000` |
302
+ | Cron jobs | Zone | IANA or UTC | `zone = "UTC"` |
303
+ | User preference | Column | IANA timezone | `America/New_York` |
304
+
305
+ ## What NOT to Do
306
+
307
+ - Don't use `TIMESTAMP` without timezone — use `TIMESTAMPTZ`
308
+ - Don't use `LocalDateTime` in Kotlin — use `Instant`
309
+ - Don't put `ZonedDateTime` in entities or DTOs
310
+ - Don't use fixed offsets (`+05:30`) as timezone identifiers — use IANA names
311
+ - Don't store timezone alongside timestamps for past events
312
+ - Don't convert to local time on the backend — that's the frontend's job
313
+ - Don't format dates on the server for display — return UTC, let the client format
314
+ - Don't schedule cron jobs in the 1-3 AM DST window
315
+ - Don't assume the host timezone is UTC — set `TZ=UTC` in containers
316
+ - Don't log in local time — all services log UTC
@@ -0,0 +1,289 @@
1
+ # Exposed ORM and Repository Patterns
2
+
3
+ > Full guide: use /database-patterns skill
4
+
5
+ ## Exposed ORM Patterns (CRITICAL)
6
+
7
+ ### Only `id` Uses `.value` — All Other UUID Columns Do NOT
8
+
9
+ `UUIDTable.id` returns `EntityID<UUID>` which needs `.value` to get the raw `UUID`.
10
+ All other `uuid()` columns return `UUID` directly — using `.value` on them causes compilation errors.
11
+
12
+ ```kotlin
13
+ // CORRECT
14
+ fun toEntity(row: ResultRow) = MyEntity(
15
+ id = row[MyTable.id].value, // id -> EntityID<UUID> -> needs .value
16
+ projectId = row[MyTable.projectId], // uuid() column -> UUID directly
17
+ userId = row[MyTable.userId], // uuid() column -> UUID directly
18
+ name = row[MyTable.name], // text() column -> String directly
19
+ )
20
+
21
+ // WRONG — causes "Unresolved reference 'value'"
22
+ fun toEntity(row: ResultRow) = MyEntity(
23
+ id = row[MyTable.id].value,
24
+ projectId = row[MyTable.projectId].value, // WRONG! uuid() returns UUID, not EntityID
25
+ userId = row[MyTable.userId].value, // WRONG!
26
+ )
27
+ ```
28
+
29
+ **Rule**: In `toEntity()` / `convert()` methods, ONLY `row[Table.id].value` gets `.value`. Every other column maps directly without `.value`.
30
+
31
+ ### SoftDeleteTable Base Class
32
+
33
+ All tables extend `SoftDeleteTable` which gives these columns automatically:
34
+ - `id` (from UUIDTable) — `EntityID<UUID>`
35
+ - `createdAt` — `Instant`
36
+ - `updatedAt` — `Instant?`
37
+ - `deletedAt` — `Instant?`
38
+
39
+ **Do NOT re-declare these columns in table definitions.**
40
+
41
+ ```kotlin
42
+ // CORRECT
43
+ object MyTable : SoftDeleteTable("my_table") {
44
+ val projectId = uuid("project_id")
45
+ val name = text("name")
46
+ // createdAt, updatedAt, deletedAt are inherited
47
+ }
48
+
49
+ // WRONG — re-declares inherited columns
50
+ object MyTable : SoftDeleteTable("my_table") {
51
+ val projectId = uuid("project_id")
52
+ val name = text("name")
53
+ val createdAt = timestamp("created_at") // Already in SoftDeleteTable!
54
+ }
55
+ ```
56
+
57
+ ### Entity Must Include All SoftDeleteTable Fields
58
+
59
+ Entity data classes MUST implement `Entity<Instant>` and include all fields from `SoftDeleteTable`:
60
+
61
+ ```kotlin
62
+ data class MyEntity(
63
+ override val id: UUID = UUID.randomUUID(),
64
+ val projectId: UUID,
65
+ val name: String,
66
+ // ... domain fields ...
67
+ override val createdAt: Instant = Instant.now(),
68
+ override val updatedAt: Instant? = null,
69
+ override val deletedAt: Instant? = null
70
+ ) : Entity<Instant>
71
+ ```
72
+
73
+ ### toEntity() Must Map ALL Fields
74
+
75
+ When writing `toEntity()` / `convert()`, map EVERY column including SoftDeleteTable fields:
76
+
77
+ ```kotlin
78
+ private fun toEntity(row: ResultRow) = MyEntity(
79
+ id = row[MyTable.id].value, // .value ONLY for id
80
+ projectId = row[MyTable.projectId], // NO .value
81
+ name = row[MyTable.name],
82
+ createdAt = row[MyTable.createdAt],
83
+ updatedAt = row[MyTable.updatedAt],
84
+ deletedAt = row[MyTable.deletedAt]
85
+ )
86
+ ```
87
+
88
+ ### Always Filter by `deletedAt.isNull()` for Soft Delete
89
+
90
+ Every SELECT query on a SoftDeleteTable MUST filter out soft-deleted records:
91
+
92
+ ```kotlin
93
+ // CORRECT
94
+ fun byId(id: UUID): MyEntity? {
95
+ return transaction(db.replica) {
96
+ MyTable.selectAll()
97
+ .where { MyTable.id eq id and MyTable.deletedAt.isNull() }
98
+ .singleOrNull()
99
+ ?.let { toEntity(it) }
100
+ }
101
+ }
102
+
103
+ // WRONG — returns soft-deleted records
104
+ fun byId(id: UUID): MyEntity? {
105
+ return transaction(db.replica) {
106
+ MyTable.selectAll()
107
+ .where { MyTable.id eq id } // Missing deletedAt.isNull()!
108
+ .singleOrNull()
109
+ ?.let { toEntity(it) }
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### orderBy Uses `SortOrder.DESC` / `SortOrder.ASC`
115
+
116
+ ```kotlin
117
+ import org.jetbrains.exposed.sql.SortOrder
118
+
119
+ // CORRECT
120
+ .orderBy(MyTable.createdAt, SortOrder.DESC)
121
+
122
+ // WRONG — causes compilation error
123
+ .orderBy(MyTable.createdAt to false)
124
+ ```
125
+
126
+ ### Chaining WHERE Conditions
127
+
128
+ Calling `.where {}` twice **replaces** the first condition. Use `.andWhere {}` to add conditions:
129
+
130
+ ```kotlin
131
+ // CORRECT — andWhere adds to existing condition
132
+ MyTable.selectAll()
133
+ .where { MyTable.deletedAt.isNull() }
134
+ .andWhere { MyTable.status eq "active" }
135
+
136
+ // WRONG — second where replaces the deletedAt filter!
137
+ MyTable.selectAll()
138
+ .where { MyTable.deletedAt.isNull() }
139
+ .where { MyTable.status eq "active" } // REPLACES the first where!
140
+ ```
141
+
142
+ ### Transaction Usage
143
+
144
+ - **Reads**: `transaction(db.replica) { ... }`
145
+ - **Writes**: `transaction(db.primary) { ... }`
146
+
147
+ ### insert() Must Include ALL Required Entity Fields
148
+
149
+ When inserting, map EVERY field from the entity to the table columns, including `deletedAt`:
150
+
151
+ ```kotlin
152
+ fun insert(entity: MyEntity): MyEntity {
153
+ return transaction(db.primary) {
154
+ MyTable.insert {
155
+ it[id] = entity.id
156
+ it[projectId] = entity.projectId
157
+ it[name] = entity.name
158
+ // ... all domain fields ...
159
+ it[createdAt] = entity.createdAt
160
+ it[updatedAt] = entity.updatedAt
161
+ it[deletedAt] = entity.deletedAt
162
+ }
163
+ entity
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Field Names Must Match Entity/Table Exactly
169
+
170
+ When writing repositories, ALWAYS cross-reference the Table and Entity files:
171
+ - Table file: `module-repository/.../table/MyTable.kt` — defines column names
172
+ - Entity file: `module-repository/.../entity/MyEntity.kt` — defines property names
173
+
174
+ **Common mistakes to avoid:**
175
+ - Using field names from a different service (e.g., `triggeredBy` when the table uses `deployedBy`)
176
+ - Guessing field names instead of reading the actual Table/Entity definitions
177
+ - Missing fields that exist in the Entity but weren't mapped in insert/toEntity
178
+
179
+ ### Ktlint Rules for DB Code
180
+
181
+ - **Always use braces for if/else** — Ktlint enforces braces on all `if`/`else` blocks, including single-expression ones:
182
+
183
+ ```kotlin
184
+ // CORRECT
185
+ val result = if (condition) {
186
+ valueA
187
+ } else {
188
+ valueB
189
+ }
190
+
191
+ // WRONG — ktlint error: "Missing { ... }"
192
+ val result = if (condition) {
193
+ valueA
194
+ } else valueB
195
+ ```
196
+
197
+ - **Data classes must have at least one parameter** — If a request/DTO truly has no fields, use a regular class or add an optional field:
198
+
199
+ ```kotlin
200
+ // CORRECT
201
+ data class StartRequest(
202
+ val notes: String? = null
203
+ )
204
+
205
+ // WRONG — compilation error
206
+ data class StartRequest(
207
+ // empty body
208
+ )
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Repository Pattern
214
+
215
+ ### Creating a Repository
216
+ You MUST create a repository for each table with:
217
+
218
+ 1. **Interface Definition**
219
+ - Define all CRUD operations needed
220
+ - Use domain-specific method names (e.g., `byEmail`, `byToken`)
221
+ - Return entities, not raw database rows
222
+ - Support soft delete operations
223
+
224
+ 2. **Implementation Class**
225
+ - Name: `Default{TableName}Repository`
226
+ - Constructor: Accept `DatabaseContext`
227
+ - Use transactions: `db.primary` for writes, `db.replica` for reads
228
+ - Use soft delete (update `deletedAt`) not hard delete
229
+
230
+ 3. **Standard Methods**
231
+ ```kotlin
232
+ fun insert(entity: Entity): Entity
233
+ fun update(id: UUID, ...fields): Entity?
234
+ fun byId(id: UUID): Entity?
235
+ fun byIds(ids: List<UUID>): List<Entity>
236
+ fun deleteById(id: UUID): Entity? // Soft delete
237
+ fun restoreById(id: UUID): Entity? // Restore soft deleted
238
+ ```
239
+
240
+ 4. **Query Patterns**
241
+ - Always check `deletedAt.isNull()` for active records
242
+ - Update `updatedAt` on every modification
243
+ - Use `convert()` method to transform ResultRow to Entity
244
+ - Handle empty lists gracefully
245
+
246
+ ### Repository File Structure
247
+ ```kotlin
248
+ interface {TableName}Repository {
249
+ // Method declarations
250
+ }
251
+
252
+ class Default{TableName}Repository(
253
+ private val db: DatabaseContext
254
+ ) : {TableName}Repository {
255
+
256
+ // Method implementations
257
+
258
+ private fun convert(row: ResultRow): Entity = Entity(
259
+ // Map all columns to entity properties
260
+ )
261
+ }
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Testing
267
+
268
+ > Full guide: use `/testing-strategies` skill
269
+
270
+ **Key rules:**
271
+ - Extend `AbstractRepositoryTest`. Name: `Default{TableName}RepositoryTest`
272
+ - Required coverage: insert, update, byId (exists/not exists/soft deleted), deleteById, restoreById
273
+ - Use `dummyEntity()` helper with defaults. Use AssertJ assertions.
274
+ - Always run `./gradlew :module-repository:test` after modifying repository code
275
+ - Test edge cases: null optionals, empty lists, non-existent IDs, already-deleted entities
276
+
277
+ ---
278
+
279
+ ## Checklist Before Writing Repository Code
280
+
281
+ - [ ] Read the Table file to know exact column names and types
282
+ - [ ] Read the Entity file to know exact property names and types
283
+ - [ ] Only `.value` on `id` column, never on other uuid columns
284
+ - [ ] Include `deletedAt.isNull()` in ALL select queries
285
+ - [ ] Use `SortOrder.DESC`/`SortOrder.ASC` for orderBy (import `org.jetbrains.exposed.sql.SortOrder`)
286
+ - [ ] Map ALL fields in `insert()` including `createdAt`, `updatedAt`, `deletedAt`
287
+ - [ ] Map ALL fields in `toEntity()` including `createdAt`, `updatedAt`, `deletedAt`
288
+ - [ ] Use `db.primary` for writes, `db.replica` for reads
289
+ - [ ] Always use braces for if/else blocks (ktlint)