litclaude-ai 0.2.2

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 (156) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +369 -0
  4. package/README_ko-KR.md +374 -0
  5. package/RELEASE_CHECKLIST.md +165 -0
  6. package/bin/litclaude-ai.js +643 -0
  7. package/cover.png +0 -0
  8. package/docs/agents.md +67 -0
  9. package/docs/hooks.md +134 -0
  10. package/docs/lsp.md +40 -0
  11. package/docs/migration.md +209 -0
  12. package/docs/workflow-compatibility-audit.md +119 -0
  13. package/generate_cover.py +123 -0
  14. package/package.json +48 -0
  15. package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
  16. package/plugins/litclaude/.lsp.json +13 -0
  17. package/plugins/litclaude/.mcp.json +9 -0
  18. package/plugins/litclaude/agents/boulder-executor.md +12 -0
  19. package/plugins/litclaude/agents/librarian-researcher.md +15 -0
  20. package/plugins/litclaude/agents/oracle-verifier.md +16 -0
  21. package/plugins/litclaude/agents/prometheus-planner.md +13 -0
  22. package/plugins/litclaude/agents/qa-runner.md +16 -0
  23. package/plugins/litclaude/agents/quality-reviewer.md +17 -0
  24. package/plugins/litclaude/bin/litclaude-hook.js +110 -0
  25. package/plugins/litclaude/bin/litclaude-hud.js +271 -0
  26. package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
  27. package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
  28. package/plugins/litclaude/commands/deep-interview.md +21 -0
  29. package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
  30. package/plugins/litclaude/commands/lit-loop.md +40 -0
  31. package/plugins/litclaude/commands/lit-plan.md +35 -0
  32. package/plugins/litclaude/commands/litgoal.md +30 -0
  33. package/plugins/litclaude/commands/review-work.md +35 -0
  34. package/plugins/litclaude/commands/start-work.md +36 -0
  35. package/plugins/litclaude/hooks/hooks.json +54 -0
  36. package/plugins/litclaude/lib/context-pressure.mjs +25 -0
  37. package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
  38. package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
  39. package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
  40. package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
  41. package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
  42. package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
  43. package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
  44. package/plugins/litclaude/lib/workflow-check.mjs +83 -0
  45. package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
  46. package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
  47. package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
  48. package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
  49. package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
  50. package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
  51. package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
  52. package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
  53. package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
  54. package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
  55. package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
  56. package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  57. package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
  58. package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
  59. package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
  60. package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
  61. package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
  62. package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
  63. package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
  64. package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
  65. package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
  66. package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
  67. package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
  68. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
  69. package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
  70. package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
  71. package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
  72. package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
  73. package/plugins/litclaude/skills/programming/SKILL.md +106 -0
  74. package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
  75. package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
  76. package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
  77. package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
  78. package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
  79. package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
  80. package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
  81. package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
  82. package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
  83. package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
  84. package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
  85. package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
  86. package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
  87. package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
  88. package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
  89. package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
  90. package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
  91. package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
  92. package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
  93. package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
  94. package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
  95. package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
  96. package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
  97. package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
  98. package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
  99. package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
  100. package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
  101. package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
  102. package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
  103. package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
  104. package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
  105. package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
  106. package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
  107. package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
  108. package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
  109. package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
  110. package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
  111. package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
  112. package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
  113. package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
  114. package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
  115. package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
  116. package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  117. package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  118. package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
  119. package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
  120. package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
  121. package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
  122. package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
  123. package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  124. package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
  125. package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  126. package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
  127. package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
  128. package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  129. package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  130. package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  131. package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  132. package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
  133. package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
  134. package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
  135. package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  136. package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
  137. package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  138. package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
  139. package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
  140. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  141. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  142. package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
  143. package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  144. package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
  145. package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
  146. package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
  147. package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
  148. package/plugins/litclaude/skills/rules/SKILL.md +66 -0
  149. package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
  150. package/scripts/audit-plan-checkboxes.mjs +37 -0
  151. package/scripts/doctor.mjs +41 -0
  152. package/scripts/inspect-agent-tools.mjs +27 -0
  153. package/scripts/postinstall.mjs +50 -0
  154. package/scripts/qa-claude-plugin-smoke.sh +60 -0
  155. package/scripts/qa-portable-install.sh +136 -0
  156. package/scripts/validate-plugin.mjs +72 -0
@@ -0,0 +1,362 @@
1
+ # Concurrency
2
+
3
+ Goroutines, context, errgroup, channels, locks, and the discipline that keeps them from leaking. Go makes concurrency *easy to start* and *easy to get wrong*. This document is the boring rule set.
4
+
5
+ ---
6
+
7
+ ## The four non-negotiables
8
+
9
+ 1. **`ctx context.Context` is the first parameter of every public function that does I/O or can be cancelled.**
10
+ 2. **No goroutine without a shutdown path.** Every `go` keyword must answer "how does this stop?".
11
+ 3. **`-race` on every test run.** The `Taskfile.yml` and CI both enforce it.
12
+ 4. **`goleak` in `TestMain`** for every package that spawns goroutines. Catches leaks the race detector cannot.
13
+
14
+ ---
15
+
16
+ ## `context.Context` — the cancellation backbone
17
+
18
+ ```go
19
+ // GOOD — ctx as first param, propagated through
20
+ func (s *UserService) Create(ctx context.Context, email Email) (User, error) {
21
+ user, err := s.store.Insert(ctx, email)
22
+ if err != nil {
23
+ return User{}, fmt.Errorf("insert: %w", err)
24
+ }
25
+ if err := s.notifier.Welcome(ctx, user); err != nil {
26
+ return User{}, fmt.Errorf("notify: %w", err)
27
+ }
28
+ return user, nil
29
+ }
30
+
31
+ // BAD — creates a fresh ctx, breaks request cancellation
32
+ func (s *UserService) Create(email Email) (User, error) {
33
+ ctx := context.Background() // ← contextcheck linter rejects this
34
+ // ...
35
+ }
36
+ ```
37
+
38
+ The `contextcheck` linter (enabled in `golangci-strict.md`) refuses any function that has `ctx context.Context` available but uses `context.Background()` instead.
39
+
40
+ ### `context.Value` — use sparingly
41
+
42
+ ```go
43
+ // Typed key — never use a bare string
44
+ type ctxKey struct{ name string }
45
+ var requestIDKey = ctxKey{"request_id"}
46
+
47
+ func WithRequestID(ctx context.Context, id string) context.Context {
48
+ return context.WithValue(ctx, requestIDKey, id)
49
+ }
50
+
51
+ func RequestID(ctx context.Context) string {
52
+ v, _ := ctx.Value(requestIDKey).(string)
53
+ return v
54
+ }
55
+ ```
56
+
57
+ **Rules**:
58
+ - Keys are unexported struct types, not strings. Prevents collisions across packages.
59
+ - `context.Value` is for *request-scoped metadata* (request ID, auth subject, trace span), NEVER for application-scoped dependencies.
60
+ - Dependencies (loggers, DB pools, config) go in your service struct, not in `context.Value`.
61
+
62
+ ### `WithTimeout` / `WithCancel` — always pair with `defer cancel()`
63
+
64
+ ```go
65
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
66
+ defer cancel() // ← MUST be deferred. fatcontext linter catches misses.
67
+
68
+ if err := slow(ctx); err != nil { ... }
69
+ ```
70
+
71
+ Forgetting `defer cancel()` leaks a context goroutine until the parent expires — the `lostcancel` vet check catches it.
72
+
73
+ ---
74
+
75
+ ## `errgroup` — the structured concurrency primitive
76
+
77
+ `golang.org/x/sync/errgroup` is Go's answer to Python's `asyncio.TaskGroup` or Rust's `JoinSet`. Use it instead of raw `go` for any group of related goroutines.
78
+
79
+ ```go
80
+ import "golang.org/x/sync/errgroup"
81
+
82
+ func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
83
+ g, ctx := errgroup.WithContext(ctx)
84
+ g.SetLimit(8) // concurrency cap — leave unbounded = production outage
85
+
86
+ results := make([][]byte, len(urls))
87
+ for i, u := range urls {
88
+ g.Go(func() error {
89
+ body, err := fetch(ctx, u)
90
+ if err != nil {
91
+ return fmt.Errorf("fetch %s: %w", u, err)
92
+ }
93
+ results[i] = body
94
+ return nil
95
+ })
96
+ }
97
+ if err := g.Wait(); err != nil {
98
+ return nil, err
99
+ }
100
+ return results, nil
101
+ }
102
+ ```
103
+
104
+ Properties:
105
+
106
+ - `WithContext(parent)` returns a child ctx that gets cancelled on **first non-nil error**. All in-flight goroutines see `ctx.Done()` and bail.
107
+ - `SetLimit(n)` blocks `g.Go(...)` when the in-flight count hits `n`. **Always set this.** Unbounded fan-out is how services die.
108
+ - `g.Wait()` returns the **first** non-nil error. Others are dropped. If you need all errors, accumulate them manually:
109
+ ```go
110
+ var mu sync.Mutex
111
+ var errs []error
112
+ // inside g.Go:
113
+ // mu.Lock(); errs = append(errs, err); mu.Unlock()
114
+ // after Wait, errors.Join(errs...)
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Goroutine leaks — `goleak`
120
+
121
+ ```go
122
+ package store_test
123
+
124
+ import (
125
+ "testing"
126
+ "go.uber.org/goleak"
127
+ )
128
+
129
+ func TestMain(m *testing.M) {
130
+ goleak.VerifyTestMain(m)
131
+ }
132
+ ```
133
+
134
+ This single line at the top of `*_test.go` runs goleak's check after every test in the package. If a test leaks a goroutine, the run fails — pointing at which goroutine.
135
+
136
+ **The bug it catches**: starting a goroutine in `setUp` and never joining it. Common in DB connection pools, background workers, ticker loops. The race detector does NOT catch this.
137
+
138
+ If you have a known long-lived goroutine (a singleton background worker, a metrics exporter), use `goleak.IgnoreTopFunction`:
139
+
140
+ ```go
141
+ goleak.VerifyTestMain(m,
142
+ goleak.IgnoreTopFunction("github.com/prometheus/client_golang/prometheus.(*Registry).Push"),
143
+ )
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Channels — the rules that hold
149
+
150
+ ### Direction
151
+
152
+ ```go
153
+ // GOOD — direction in signatures
154
+ func produce(out chan<- Item)
155
+ func consume(in <-chan Item)
156
+ func pipeline(in <-chan Item, out chan<- Item)
157
+ ```
158
+
159
+ Direction restricts misuse. A consumer cannot close the producer's channel.
160
+
161
+ ### Closing
162
+
163
+ - **The sender closes.** Always. Never the receiver, never multiple senders.
164
+ - **Multiple senders → use a `sync.WaitGroup` + one closer.**
165
+ - **Closing a closed channel panics.** Closing a `nil` channel panics. Sending on a closed channel panics. Receiving from a closed channel returns zero value with `ok = false`.
166
+
167
+ ```go
168
+ // Canonical fan-in: multiple producers, one closer
169
+ func fanIn(ctx context.Context, sources ...<-chan Item) <-chan Item {
170
+ out := make(chan Item)
171
+ var wg sync.WaitGroup
172
+ wg.Add(len(sources))
173
+ for _, src := range sources {
174
+ go func() {
175
+ defer wg.Done()
176
+ for item := range src {
177
+ select {
178
+ case out <- item:
179
+ case <-ctx.Done():
180
+ return
181
+ }
182
+ }
183
+ }()
184
+ }
185
+ go func() { wg.Wait(); close(out) }()
186
+ return out
187
+ }
188
+ ```
189
+
190
+ ### Selecting
191
+
192
+ ```go
193
+ select {
194
+ case msg := <-incoming:
195
+ handle(msg)
196
+ case <-ctx.Done():
197
+ return ctx.Err()
198
+ case <-time.After(5 * time.Second):
199
+ return ErrTimeout
200
+ }
201
+ ```
202
+
203
+ - `time.After` allocates a timer each call — fine for occasional selects, **NOT for hot loops**. Use `time.NewTimer` + `timer.Reset` for repeat selects.
204
+ - A `default:` case makes `select` non-blocking. Use deliberately, not by accident.
205
+
206
+ ### Buffered vs unbuffered
207
+
208
+ - **Unbuffered** (`make(chan T)`) = synchronous handoff. Sender blocks until receiver is ready. Use for *coordination*.
209
+ - **Buffered** (`make(chan T, n)`) = asynchronous up to `n`. Use for *decoupling producer rate from consumer rate*.
210
+
211
+ A buffered channel of size 1 acts as a **non-blocking signal**:
212
+
213
+ ```go
214
+ ready := make(chan struct{}, 1)
215
+ // Producer
216
+ select {
217
+ case ready <- struct{}{}: // signal once, non-blocking
218
+ default: // already signaled, skip
219
+ }
220
+ // Consumer
221
+ <-ready
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Locks — the pyramid
227
+
228
+ ```
229
+ Highest level (preferred)
230
+ channels (message passing — "share memory by communicating")
231
+ errgroup / wait group
232
+
233
+ sync.RWMutex (many readers, occasional writer)
234
+ sync.Mutex (mutual exclusion)
235
+
236
+ atomic.Int64 / atomic.Pointer (single-word lock-free)
237
+
238
+ Lowest level (rare)
239
+ unsafe.Pointer + barriers (custom lock-free; needs -race AND review)
240
+ ```
241
+
242
+ ### `sync.Mutex` — embed, don't expose
243
+
244
+ ```go
245
+ type Cache struct {
246
+ mu sync.RWMutex
247
+ items map[string]Entry
248
+ }
249
+
250
+ func (c *Cache) Get(key string) (Entry, bool) {
251
+ c.mu.RLock()
252
+ defer c.mu.RUnlock()
253
+ e, ok := c.items[key]
254
+ return e, ok
255
+ }
256
+
257
+ func (c *Cache) Set(key string, e Entry) {
258
+ c.mu.Lock()
259
+ defer c.mu.Unlock()
260
+ c.items[key] = e
261
+ }
262
+ ```
263
+
264
+ - `sync.Mutex` is **not** copyable. The `copylocks` vet check catches `var c2 = c1` where `c1` has a mutex.
265
+ - Always `defer mu.Unlock()` immediately after `Lock()`. Forgetting is the #1 deadlock cause.
266
+ - Never call user code (callbacks, listener notifications) while holding the lock. Drop the lock, snapshot the data, release, then call out.
267
+
268
+ ### `sync.OnceValue` / `sync.OnceFunc` (Go 1.21+)
269
+
270
+ Replacement for `sync.Once` for typed lazy init:
271
+
272
+ ```go
273
+ var loadConfig = sync.OnceValue(func() Config {
274
+ var cfg Config
275
+ if err := env.Parse(&cfg); err != nil { panic(err) }
276
+ return cfg
277
+ })
278
+
279
+ func handler() { cfg := loadConfig(); ... }
280
+ ```
281
+
282
+ Type-safe, no `sync.Once` + global variable boilerplate.
283
+
284
+ ### Atomics — the typed API only
285
+
286
+ ```go
287
+ // Go 1.19+ — use the typed atomic.* family
288
+ var counter atomic.Int64
289
+ counter.Add(1)
290
+ n := counter.Load()
291
+
292
+ // NEVER — the old function-style is type-unsafe
293
+ atomic.AddInt64(&counter, 1) // ← rejected
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Time — inject a clock for testability
299
+
300
+ ```go
301
+ type Clock interface {
302
+ Now() time.Time
303
+ }
304
+
305
+ type realClock struct{}
306
+ func (realClock) Now() time.Time { return time.Now() }
307
+
308
+ type Service struct {
309
+ clock Clock
310
+ }
311
+
312
+ // Tests
313
+ import "github.com/benbjohnson/clock"
314
+ fake := clock.NewMock()
315
+ fake.Set(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
316
+ svc := &Service{clock: fake}
317
+ ```
318
+
319
+ **Never call `time.Now()` in domain or service code.** The `time` package becomes a hidden dependency — tests become flaky, retries become time-of-day-dependent, expirations cannot be tested.
320
+
321
+ `time.Sleep` in production code is a code smell. Use:
322
+ - `time.NewTicker` for periodic work (and a `<-ctx.Done()` exit).
323
+ - `time.NewTimer` for one-shot delays.
324
+ - `time.After` ONLY in select statements, ONLY in non-hot paths.
325
+
326
+ ---
327
+
328
+ ## Race detector — non-negotiable in CI
329
+
330
+ ```bash
331
+ go test -race -shuffle=on -count=1 ./...
332
+ ```
333
+
334
+ - `-race` instruments memory accesses; catches data races at runtime. ~10x slow-down — acceptable for tests, not production.
335
+ - `-shuffle=on` randomizes test order; catches hidden ordering dependencies.
336
+ - `-count=1` defeats the test cache. Without it, "passing" might mean "ran 3 weeks ago".
337
+
338
+ If a test ONLY fails under `-race`, the bug is real. Don't disable the test; fix the race.
339
+
340
+ ---
341
+
342
+ ## Common antipatterns
343
+
344
+ | Bad | Why | Good |
345
+ |---|---|---|
346
+ | `go func() { ... }()` with no `ctx` plumbing | Leaks on shutdown | `errgroup.WithContext` or pass ctx |
347
+ | Bare `time.Sleep(d)` in production | Untestable, blocks | `time.NewTimer` + select with `ctx.Done()` |
348
+ | Channel of `interface{}` | Loses type | Typed channel; use sealed interface if variants needed |
349
+ | `sync.Mutex` in a struct passed by value | Locked copies, undefined behavior | Embed in pointer-receiver type; copylocks catches it |
350
+ | Locking around an entire request handler | Serializes the whole API | Lock only the smallest critical section |
351
+ | `for { select { ... } }` without `<-ctx.Done()` | Cannot stop | Add ctx case in every long-lived select |
352
+ | `sync.WaitGroup.Add(1)` inside the goroutine | Race: Wait can return before Add | Add **before** `go` |
353
+
354
+ ---
355
+
356
+ ## Sources
357
+
358
+ - Go memory model: https://go.dev/ref/mem
359
+ - `errgroup` package: https://pkg.go.dev/golang.org/x/sync/errgroup
360
+ - `goleak`: https://github.com/uber-go/goleak
361
+ - "Go concurrency patterns" (Pike): https://go.dev/blog/pipelines
362
+ - Sync.OnceValue blog: https://go.dev/blog/synctest (1.24+ note: `testing/synctest` for time-controlled tests is now experimental)
@@ -0,0 +1,329 @@
1
+ # Data Modeling — Three Layers of Validation
2
+
3
+ Go has no Pydantic. Go has no Zod. **You do not need them**, but only if you wire three layers correctly. This document is the canonical pattern.
4
+
5
+ ## The three layers
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ HTTP / RPC / CLI │
10
+ │ Raw bytes, strings, untrusted input │
11
+ │ │
12
+ │ Layer 1: validator/v10 (struct tags) ◄── parse-once │
13
+ │ OR protovalidate (proto) │
14
+ │ │
15
+ └──────────────────────────┬──────────────────────────────────┘
16
+ │ raw req → domain.X
17
+
18
+ ┌─────────────────────────────────────────────────────────────┐
19
+ │ Domain (internal/domain) │
20
+ │ │
21
+ │ Layer 2: Smart constructors + unexported fields │
22
+ │ NewEmail(s) → (Email, error) │
23
+ │ NewUserID(s) → (UserID, error) │
24
+ │ │
25
+ │ Once inside this layer, NO further validation. │
26
+ │ The types prove correctness. │
27
+ └──────────────────────────┬──────────────────────────────────┘
28
+ │ domain.X (proven valid)
29
+
30
+ ┌─────────────────────────────────────────────────────────────┐
31
+ │ Storage (internal/store) │
32
+ │ │
33
+ │ Layer 3: sqlc-generated row structs ↔ domain types │
34
+ │ Hand-written mappers, NOT struct tags │
35
+ └─────────────────────────────────────────────────────────────┘
36
+ ```
37
+
38
+ Each layer parses once, into the next layer's types. **A function in the domain layer should never receive a raw string and validate it.** If it does, the boundary above failed.
39
+
40
+ ---
41
+
42
+ ## Layer 1: HTTP boundary — `go-playground/validator/v10`
43
+
44
+ ```go
45
+ package handlers
46
+
47
+ import (
48
+ "github.com/gin-gonic/gin"
49
+ "github.com/go-playground/validator/v10"
50
+ )
51
+
52
+ // CreateUserRequest is the wire format. Tags drive validation.
53
+ type CreateUserRequest struct {
54
+ Email string `json:"email" binding:"required,email"`
55
+ Username string `json:"username" binding:"required,alphanum,min=3,max=32"`
56
+ Age int `json:"age" binding:"required,gte=13,lte=130"`
57
+ Country string `json:"country" binding:"required,iso3166_1_alpha2"`
58
+ }
59
+
60
+ func (h *Handler) CreateUser(c *gin.Context) {
61
+ var req CreateUserRequest
62
+ if err := c.ShouldBindJSON(&req); err != nil {
63
+ // validator returns ValidationErrors with field-by-field detail
64
+ var vErr validator.ValidationErrors
65
+ if errors.As(err, &vErr) {
66
+ c.JSON(400, gin.H{"errors": fieldErrors(vErr)})
67
+ return
68
+ }
69
+ c.JSON(400, gin.H{"error": "invalid json"})
70
+ return
71
+ }
72
+
73
+ // Cross into domain — single point of failure
74
+ email, err := domain.NewEmail(req.Email)
75
+ if err != nil {
76
+ c.JSON(400, gin.H{"error": err.Error()})
77
+ return
78
+ }
79
+ username, err := domain.NewUsername(req.Username)
80
+ if err != nil {
81
+ c.JSON(400, gin.H{"error": err.Error()})
82
+ return
83
+ }
84
+
85
+ user, err := h.svc.Create(c.Request.Context(), email, username, req.Age)
86
+ if err != nil {
87
+ h.writeServiceError(c, err)
88
+ return
89
+ }
90
+ c.JSON(201, user)
91
+ }
92
+
93
+ func fieldErrors(vErr validator.ValidationErrors) map[string]string {
94
+ out := make(map[string]string, len(vErr))
95
+ for _, fe := range vErr {
96
+ out[fe.Field()] = fe.Tag() + "(" + fe.Param() + ")"
97
+ }
98
+ return out
99
+ }
100
+ ```
101
+
102
+ **Tag reference — the tags you actually use**:
103
+
104
+ | Tag | Meaning |
105
+ |---|---|
106
+ | `required` | Non-zero value |
107
+ | `omitempty` (json) | Skip if zero |
108
+ | `min=N` / `max=N` | Length (strings/slices) or value (numbers) |
109
+ | `gte=N` / `lte=N` / `gt=N` / `lt=N` | Numeric comparison |
110
+ | `email` | RFC 5322-ish email |
111
+ | `url` | Valid URL |
112
+ | `uuid` / `uuid4` / `uuid7` | UUID format |
113
+ | `alphanum` / `alpha` / `numeric` | Character class |
114
+ | `iso3166_1_alpha2` | Country code (US, KR, JP) |
115
+ | `iso4217` | Currency code (USD, KRW) |
116
+ | `oneof=a b c` | Enum of literal values |
117
+ | `dive` | Apply rules to each element of slice/map |
118
+ | `eqfield=Field` | Cross-field equality (e.g., password confirm) |
119
+
120
+ ### Custom validators — register at startup
121
+
122
+ ```go
123
+ func init() {
124
+ if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
125
+ _ = v.RegisterValidation("strongpassword", validateStrongPassword)
126
+ }
127
+ }
128
+
129
+ func validateStrongPassword(fl validator.FieldLevel) bool {
130
+ s := fl.Field().String()
131
+ return len(s) >= 12 && hasUpper(s) && hasDigit(s) && hasSymbol(s)
132
+ }
133
+ ```
134
+
135
+ Use sparingly. Most domain rules belong in smart constructors, not validators.
136
+
137
+ ---
138
+
139
+ ## Layer 2: Domain — smart constructors
140
+
141
+ Covered in detail in `type-patterns.md`. Recap:
142
+
143
+ ```go
144
+ package domain
145
+
146
+ type Username struct{ raw string }
147
+
148
+ func NewUsername(s string) (Username, error) {
149
+ s = strings.TrimSpace(s)
150
+ if len(s) < 3 || len(s) > 32 {
151
+ return Username{}, ErrInvalidUsername
152
+ }
153
+ if !isAlphanum(s) {
154
+ return Username{}, ErrInvalidUsername
155
+ }
156
+ return Username{raw: s}, nil
157
+ }
158
+
159
+ func (u Username) String() string { return u.raw }
160
+ ```
161
+
162
+ **Rule**: every domain type that has invariants has:
163
+
164
+ 1. An unexported field holding the raw form.
165
+ 2. A `New<Type>(raw) (<Type>, error)` constructor as the sole entry point.
166
+ 3. A `String() string` for printing.
167
+ 4. `MarshalJSON` / `UnmarshalJSON` if it crosses a JSON boundary outside HTTP handlers (e.g., logging payloads, queue messages).
168
+ 5. Optionally: `Scan` and `Value` for `database/sql` interop (rare with sqlc).
169
+
170
+ ---
171
+
172
+ ## Layer 3: Storage — sqlc rows ↔ domain types
173
+
174
+ sqlc generates row structs from `.sql` files. **Do not put validation tags on them.** Map between sqlc rows and domain types explicitly:
175
+
176
+ ```go
177
+ // internal/store/user_store.go
178
+ package store
179
+
180
+ import "myservice/internal/domain"
181
+
182
+ func (s *UserStore) Get(ctx context.Context, id domain.UserID) (domain.User, error) {
183
+ row, err := s.q.GetUser(ctx, string(id))
184
+ if err != nil {
185
+ return domain.User{}, err
186
+ }
187
+ return rowToUser(row)
188
+ }
189
+
190
+ func rowToUser(r sqlc.UserRow) (domain.User, error) {
191
+ email, err := domain.NewEmail(r.Email)
192
+ if err != nil {
193
+ // DB invariant broken — this is a programmer error, not a user error
194
+ return domain.User{}, fmt.Errorf("db invariant: invalid email for user %s: %w", r.ID, err)
195
+ }
196
+ username, err := domain.NewUsername(r.Username)
197
+ if err != nil {
198
+ return domain.User{}, fmt.Errorf("db invariant: invalid username: %w", err)
199
+ }
200
+ return domain.User{
201
+ ID: domain.UserID(r.ID),
202
+ Email: email,
203
+ Username: username,
204
+ Created: r.CreatedAt,
205
+ }, nil
206
+ }
207
+ ```
208
+
209
+ The mapping is verbose. **That is the point.** Each field is a deliberate choice; refactors flag every site.
210
+
211
+ ---
212
+
213
+ ## Discriminated unions (sum types) at the boundary
214
+
215
+ When a wire payload has variants (e.g., `{"type": "user.created", ...}` vs `{"type": "user.deleted", ...}`):
216
+
217
+ ```go
218
+ // Wire DTO with raw discriminator
219
+ type EventDTO struct {
220
+ Type string `json:"type" binding:"required,oneof=created deleted updated"`
221
+ Payload json.RawMessage `json:"payload" binding:"required"`
222
+ }
223
+
224
+ // Parse into the sealed domain type
225
+ func ParseEvent(dto EventDTO) (event.Event, error) {
226
+ switch dto.Type {
227
+ case "created":
228
+ var c event.Created
229
+ if err := json.Unmarshal(dto.Payload, &c); err != nil {
230
+ return nil, fmt.Errorf("decode created: %w", err)
231
+ }
232
+ return c, nil
233
+ case "deleted":
234
+ var d event.Deleted
235
+ if err := json.Unmarshal(dto.Payload, &d); err != nil {
236
+ return nil, fmt.Errorf("decode deleted: %w", err)
237
+ }
238
+ return d, nil
239
+ case "updated":
240
+ var u event.Updated
241
+ if err := json.Unmarshal(dto.Payload, &u); err != nil {
242
+ return nil, fmt.Errorf("decode updated: %w", err)
243
+ }
244
+ return u, nil
245
+ default:
246
+ return nil, fmt.Errorf("unknown event type %q", dto.Type)
247
+ }
248
+ }
249
+ ```
250
+
251
+ The `exhaustive` linter on the switch + the `oneof` validation tag together cover both "unknown type" and "unhandled variant".
252
+
253
+ ---
254
+
255
+ ## Enums — typed string consts, not iota
256
+
257
+ ```go
258
+ // GOOD — string-based, JSON-serializes correctly, debuggable
259
+ type Status string
260
+
261
+ const (
262
+ StatusPending Status = "pending"
263
+ StatusActive Status = "active"
264
+ StatusClosed Status = "closed"
265
+ )
266
+
267
+ func (s Status) IsValid() bool {
268
+ switch s {
269
+ case StatusPending, StatusActive, StatusClosed:
270
+ return true
271
+ }
272
+ return false
273
+ }
274
+
275
+ func (s *Status) UnmarshalJSON(data []byte) error {
276
+ var raw string
277
+ if err := json.Unmarshal(data, &raw); err != nil { return err }
278
+ parsed := Status(raw)
279
+ if !parsed.IsValid() { return fmt.Errorf("invalid status %q", raw) }
280
+ *s = parsed
281
+ return nil
282
+ }
283
+ ```
284
+
285
+ **Never use `iota` enums for anything that crosses a wire boundary.** They serialize as integers, which (a) breaks debuggability, (b) makes reordering enum values a silent breaking change.
286
+
287
+ Use the validator tag `binding:"oneof=pending active closed"` to enforce at the HTTP boundary.
288
+
289
+ ---
290
+
291
+ ## Nullable fields — `*T` vs sentinel
292
+
293
+ Three choices, in order of preference:
294
+
295
+ 1. **Sentinel zero value**: `Age int` with `0` meaning "unknown". Works when zero is genuinely unreachable as a valid value.
296
+ 2. **`sql.Null<T>`** for DB columns: `sql.NullString`, `sql.NullInt64`, `sql.NullTime`. sqlc generates these for nullable columns.
297
+ 3. **`*T`**: only when you need to distinguish "not provided" from "set to zero" in a JSON payload (PATCH semantics).
298
+
299
+ ```go
300
+ // PATCH payload — `*string` discriminates absent vs empty
301
+ type UpdateUserRequest struct {
302
+ Email *string `json:"email,omitempty"`
303
+ Username *string `json:"username,omitempty"`
304
+ }
305
+ ```
306
+
307
+ Avoid `*T` in domain types — it bloats every consumer with nil checks. Keep `*T` at the boundary, unwrap on the way in.
308
+
309
+ ---
310
+
311
+ ## Common AI-generated antipatterns this rejects
312
+
313
+ | Bad | Why | Good |
314
+ |---|---|---|
315
+ | `func handle(req map[string]any)` | No types, no validation | Define a struct, parse with `validator` |
316
+ | `if email != "" { ... }` inside domain | Validation in the wrong layer | Make `email Email`, no check needed |
317
+ | `type Status int` with `iota` for wire field | Silent breaking on reorder | `type Status string` with const literals |
318
+ | Struct tags `json:"email,string"` (the `,string` coercion) | Magic coercion hides bad input | Strict parsing, fail-fast |
319
+ | `json.Unmarshal` then range-check after | Two-step "validate after parse" | Use `validator` tags or custom `UnmarshalJSON` |
320
+ | Reusing handler DTO as the domain type | Couples wire format to business logic | Two distinct types, explicit mapping |
321
+
322
+ ---
323
+
324
+ ## Sources
325
+
326
+ - go-playground/validator: https://github.com/go-playground/validator
327
+ - gin binding internals: https://github.com/gin-gonic/gin/blob/master/binding/json.go
328
+ - Parse, don't validate: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
329
+ - sqlc with custom types: https://docs.sqlc.dev/en/latest/howto/overrides.html