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,359 @@
1
+ # Error Handling
2
+
3
+ Typed errors, wrap chains, `errors.Is` / `errors.As`, no panic in libraries, resource cleanup. Go errors look simple and are full of footguns. This document is the canonical set of moves.
4
+
5
+ ---
6
+
7
+ ## The five rules
8
+
9
+ 1. **Every error is wrapped on the way up, with `%w`, with context.** Never `return err` from a non-trivial site.
10
+ 2. **Compare with `errors.Is`, not `==`.** Wrap chains break `==`. The `errorlint` linter forbids `==` on errors.
11
+ 3. **Cast with `errors.As`, not type assertion.** Same reason.
12
+ 4. **`panic` is reserved for programmer errors.** Library code never panics on user input or environment failures. Use `(T, error)`.
13
+ 5. **Resources released via `defer` immediately after acquisition.** No "I'll add it later".
14
+
15
+ ---
16
+
17
+ ## Sentinel errors — for invariant programmatic checks
18
+
19
+ ```go
20
+ package domain
21
+
22
+ import "errors"
23
+
24
+ var (
25
+ ErrInvalidEmail = errors.New("domain: invalid email")
26
+ ErrInvalidPhone = errors.New("domain: invalid phone")
27
+ ErrInvalidAge = errors.New("domain: invalid age")
28
+ )
29
+
30
+ func NewEmail(s string) (Email, error) {
31
+ if !emailRe.MatchString(s) {
32
+ return Email{}, fmt.Errorf("email %q: %w", s, ErrInvalidEmail)
33
+ }
34
+ return Email{raw: strings.ToLower(s)}, nil
35
+ }
36
+ ```
37
+
38
+ Caller branches on identity:
39
+
40
+ ```go
41
+ email, err := domain.NewEmail(input)
42
+ if errors.Is(err, domain.ErrInvalidEmail) {
43
+ return c.JSON(400, gin.H{"error": "email format"})
44
+ }
45
+ ```
46
+
47
+ `errors.Is` walks the wrap chain. `err == domain.ErrInvalidEmail` would have failed because `fmt.Errorf` wrapped it.
48
+
49
+ ---
50
+
51
+ ## Typed errors — when you need structured data
52
+
53
+ When callers need fields off the error (the offending value, the failing field name, the upstream HTTP status):
54
+
55
+ ```go
56
+ type ValidationError struct {
57
+ Field string
58
+ Value string
59
+ Rule string
60
+ }
61
+
62
+ func (e *ValidationError) Error() string {
63
+ return fmt.Sprintf("validation: %s=%q failed %s", e.Field, e.Value, e.Rule)
64
+ }
65
+
66
+ // Optional: identity sentinel for errors.Is comparisons
67
+ var ErrValidation = errors.New("validation")
68
+
69
+ func (e *ValidationError) Is(target error) bool {
70
+ return target == ErrValidation
71
+ }
72
+ ```
73
+
74
+ Caller:
75
+
76
+ ```go
77
+ err := svc.Save(ctx, user)
78
+
79
+ var vErr *ValidationError
80
+ if errors.As(err, &vErr) {
81
+ // vErr.Field, vErr.Rule are available
82
+ c.JSON(400, gin.H{"field": vErr.Field, "rule": vErr.Rule})
83
+ return
84
+ }
85
+ ```
86
+
87
+ **`errors.As` requires a non-nil pointer-to-pointer.** Almost always the type is `*ConcreteError`. Forgetting the leading `*` is the most common bug here.
88
+
89
+ ---
90
+
91
+ ## Wrapping — `%w` is mandatory
92
+
93
+ ```go
94
+ // BAD — drops context
95
+ return err
96
+
97
+ // BAD — drops the error chain (errors.Is/As stops working)
98
+ return fmt.Errorf("failed to save user: %v", err)
99
+
100
+ // GOOD — preserves chain via %w
101
+ return fmt.Errorf("save user %s: %w", userID, err)
102
+ ```
103
+
104
+ The `errorlint` linter catches `%v` where `%w` was meant. **Wrap once per layer**, with the minimum useful context:
105
+
106
+ ```
107
+ api/handler: "create user request: %w"
108
+ service: "validate inputs: %w"
109
+ domain: "email %q: %w"
110
+ ```
111
+
112
+ Each frame adds one fact, not a duplicate. The top-level error message reads as a path: `create user request: validate inputs: email "foo": domain: invalid email`.
113
+
114
+ ### `errors.Join` — multiple errors at once
115
+
116
+ ```go
117
+ // Validate all fields, collect all errors
118
+ var errs []error
119
+ if _, err := NewEmail(req.Email); err != nil {
120
+ errs = append(errs, fmt.Errorf("email: %w", err))
121
+ }
122
+ if _, err := NewUsername(req.Username); err != nil {
123
+ errs = append(errs, fmt.Errorf("username: %w", err))
124
+ }
125
+ if len(errs) > 0 {
126
+ return errors.Join(errs...)
127
+ }
128
+ ```
129
+
130
+ `errors.Is` still walks each joined error. Use when reporting batch validation, not for "wrap two unrelated errors".
131
+
132
+ ---
133
+
134
+ ## Panics — when allowed, when banned
135
+
136
+ **Banned**:
137
+
138
+ - Anywhere a `(T, error)` could be returned.
139
+ - Inside HTTP handlers (gin's `Recovery` middleware catches them, but you've already lost the error context).
140
+ - Inside any goroutine that survives request lifetime.
141
+
142
+ **Allowed** (with documentation):
143
+
144
+ - Map literal init at package level: `var statusNames = map[Status]string{...}` followed by a `func init()` that panics if a const has no name. Catches the bug at startup, not runtime.
145
+ - The `must*` convention for genuinely unrecoverable startup:
146
+ ```go
147
+ func MustParseURL(s string) *url.URL {
148
+ u, err := url.Parse(s)
149
+ if err != nil { panic(err) }
150
+ return u
151
+ }
152
+ // Use only with literals known at compile time:
153
+ var defaultAPI = MustParseURL("https://api.example.com")
154
+ ```
155
+ - `default:` case of an exhaustive sealed-interface switch — see `type-patterns.md`.
156
+
157
+ The `revive` linter rule `error-return` will flag suspect panic sites; treat them as bugs.
158
+
159
+ ---
160
+
161
+ ## `defer` for resources — the only safe pattern
162
+
163
+ ```go
164
+ func writeReport(path string) (err error) {
165
+ f, err := os.Create(path)
166
+ if err != nil {
167
+ return fmt.Errorf("create %s: %w", path, err)
168
+ }
169
+ defer func() {
170
+ if cerr := f.Close(); cerr != nil && err == nil {
171
+ err = fmt.Errorf("close %s: %w", path, cerr)
172
+ }
173
+ }()
174
+
175
+ if _, err := f.Write(data); err != nil {
176
+ return fmt.Errorf("write %s: %w", path, err)
177
+ }
178
+ return nil
179
+ }
180
+ ```
181
+
182
+ Key points:
183
+
184
+ - `defer f.Close()` immediately after `os.Create` — never further down.
185
+ - Named return `(err error)` so the deferred close can mutate it on close failure.
186
+ - `bodyclose` linter catches missed `defer resp.Body.Close()` for HTTP responses.
187
+ - `sqlclosecheck` linter catches missed `defer rows.Close()` for SQL.
188
+
189
+ ### `errors.Join` for multi-stage cleanup
190
+
191
+ ```go
192
+ func process(path string) (err error) {
193
+ f, err := os.Open(path)
194
+ if err != nil { return err }
195
+ defer func() {
196
+ err = errors.Join(err, f.Close())
197
+ }()
198
+ // ... use f ...
199
+ return nil
200
+ }
201
+ ```
202
+
203
+ When both the main operation AND `Close` can fail, `errors.Join` reports both without dropping either.
204
+
205
+ ---
206
+
207
+ ## HTTP error responses — a single funnel
208
+
209
+ Build one helper, route all handler errors through it:
210
+
211
+ ```go
212
+ package httperr
213
+
214
+ type APIError struct {
215
+ Status int `json:"-"`
216
+ Code string `json:"code"`
217
+ Message string `json:"message"`
218
+ }
219
+
220
+ func (e *APIError) Error() string { return e.Code + ": " + e.Message }
221
+
222
+ var (
223
+ NotFound = &APIError{Status: 404, Code: "not_found", Message: "resource not found"}
224
+ Unauthorized = &APIError{Status: 401, Code: "unauthorized", Message: "unauthorized"}
225
+ BadRequest = &APIError{Status: 400, Code: "bad_request", Message: "bad request"}
226
+ Internal = &APIError{Status: 500, Code: "internal", Message: "internal error"}
227
+ )
228
+
229
+ // Wrap a domain error into an API error.
230
+ func From(err error) *APIError {
231
+ if err == nil { return nil }
232
+
233
+ var apiErr *APIError
234
+ if errors.As(err, &apiErr) { return apiErr }
235
+
236
+ switch {
237
+ case errors.Is(err, domain.ErrInvalidEmail),
238
+ errors.Is(err, domain.ErrInvalidUsername):
239
+ return &APIError{Status: 400, Code: "validation", Message: err.Error()}
240
+ case errors.Is(err, ErrNotFound):
241
+ return NotFound
242
+ case errors.Is(err, ErrUnauthorized):
243
+ return Unauthorized
244
+ default:
245
+ // unknown — log full chain, return generic
246
+ slog.Error("unmapped error", slog.Any("err", err))
247
+ return Internal
248
+ }
249
+ }
250
+
251
+ func Write(c *gin.Context, err error) {
252
+ apiErr := From(err)
253
+ c.JSON(apiErr.Status, apiErr)
254
+ }
255
+ ```
256
+
257
+ Handlers become trivial:
258
+
259
+ ```go
260
+ func (h *Handler) Create(c *gin.Context) {
261
+ user, err := h.svc.Create(c.Request.Context(), req)
262
+ if err != nil {
263
+ httperr.Write(c, err)
264
+ return
265
+ }
266
+ c.JSON(201, user)
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## errgroup — error propagation across goroutines
273
+
274
+ ```go
275
+ import "golang.org/x/sync/errgroup"
276
+
277
+ func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
278
+ g, ctx := errgroup.WithContext(ctx)
279
+ g.SetLimit(8) // concurrency cap
280
+
281
+ results := make([][]byte, len(urls))
282
+ for i, u := range urls {
283
+ g.Go(func() error {
284
+ body, err := fetch(ctx, u)
285
+ if err != nil {
286
+ return fmt.Errorf("fetch %s: %w", u, err)
287
+ }
288
+ results[i] = body
289
+ return nil
290
+ })
291
+ }
292
+
293
+ if err := g.Wait(); err != nil {
294
+ return nil, err
295
+ }
296
+ return results, nil
297
+ }
298
+ ```
299
+
300
+ - `errgroup.WithContext` cancels remaining tasks on first error.
301
+ - `SetLimit` bounds concurrency.
302
+ - First non-nil error is returned; others are discarded — by design.
303
+
304
+ See `concurrency.md` for the full pattern.
305
+
306
+ ---
307
+
308
+ ## Logging errors — structured, once
309
+
310
+ ```go
311
+ slog.ErrorContext(ctx, "save user failed",
312
+ slog.String("user_id", string(id)),
313
+ slog.Any("err", err), // %w chain is fully rendered
314
+ )
315
+ ```
316
+
317
+ **Log once, at the outermost frame.** Logging at every wrap site produces five log lines for one error.
318
+
319
+ The `sloglint` linter enforces `slog.Any("err", err)` over `slog.String("err", err.Error())` — the former preserves the chain when handlers walk the value.
320
+
321
+ ---
322
+
323
+ ## Antipatterns
324
+
325
+ | Bad | Why | Good |
326
+ |---|---|---|
327
+ | `_ = err` | Silent ignore | Handle, log, or wrap |
328
+ | `if err != nil { return err }` chained 10 deep without wrap | No path info | Add one fact per layer: `fmt.Errorf("step: %w", err)` |
329
+ | `panic(err)` in HTTP handlers | Loses error chain, hits gin Recovery | `httperr.Write(c, err)` |
330
+ | `err.Error() == "some string"` | Brittle, breaks on wrap | Define a sentinel, use `errors.Is` |
331
+ | `if err == sql.ErrNoRows` | Breaks under wrap | `errors.Is(err, sql.ErrNoRows)` |
332
+ | `catch-all log.Fatal(err)` in library code | Crashes the caller's process | Return error, let main decide |
333
+ | Returning a typed nil pointer wrapped in error interface | Classic "nil != nil" bug | Return explicit `nil` for the error |
334
+
335
+ The last bug deserves its own example:
336
+
337
+ ```go
338
+ // BUG — returns a non-nil error interface containing a nil concrete type
339
+ func bad() error {
340
+ var e *MyError = nil
341
+ return e // interface wraps nil pointer; errors == nil is FALSE
342
+ }
343
+
344
+ // Caller
345
+ if err := bad(); err != nil {
346
+ // ← entered, but err.(*MyError) is nil — surprise panic
347
+ }
348
+ ```
349
+
350
+ Fix: return explicit `nil`, not a typed nil. The `nilnil` linter catches this in `(T, error)` returns.
351
+
352
+ ---
353
+
354
+ ## Sources
355
+
356
+ - Go blog "Working with Errors in Go 1.13+": https://go.dev/blog/go1.13-errors
357
+ - `errors.Join` (Go 1.20+): https://pkg.go.dev/errors#Join
358
+ - errorlint: https://github.com/polyfloyd/go-errorlint
359
+ - nilaway nil-interface check: https://github.com/uber-go/nilaway
@@ -0,0 +1,236 @@
1
+ # Strict `.golangci.yml` (golangci-lint v2)
2
+
3
+ The single source of truth for "is this Go code acceptable". Drop this in unmodified. **Every linter below is enabled deliberately — read the rationale before disabling one.**
4
+
5
+ `golangci-lint` v2 changed config schema (top-level `version: "2"`). All v1 configs are incompatible. The block below is v2.
6
+
7
+ ## `.golangci.yml`
8
+
9
+ ```yaml
10
+ version: "2"
11
+
12
+ run:
13
+ timeout: 5m
14
+ tests: true
15
+ modules-download-mode: readonly
16
+
17
+ linters:
18
+ default: none
19
+ enable:
20
+ # ── Correctness — bug catchers ───────────────────────────────
21
+ - govet # stdlib vet, includes shadow, fieldalignment, nilness
22
+ - staticcheck # SA1*-SA9* — the de facto Go correctness linter
23
+ - errcheck # unhandled errors. ZERO tolerance.
24
+ - errorlint # %w wrapping, errors.As vs type-assertion, errors.Is vs ==
25
+ - nilerr # `return nil` after `err != nil` — classic bug
26
+ - nilnil # returning `(nil, nil)` from a (*T, error) function
27
+ - bodyclose # http.Response.Body not closed
28
+ - rowserrcheck # sql.Rows.Err() not checked
29
+ - sqlclosecheck # sql.Rows / sql.Stmt not closed
30
+ - contextcheck # functions taking context.Context don't get context.Background()
31
+ - fatcontext # context.WithValue() in a loop — leaks
32
+ - copyloopvar # Go 1.22 loop-var capture — should now use the new semantics
33
+ - intrange # use `for i := range N` (Go 1.22+) instead of `for i := 0; i < N; i++`
34
+ - usetesting # use t.TempDir/t.Setenv over os.* in tests
35
+ - testifylint # require vs assert correctness, ObjectsAreEqual misuse
36
+
37
+ # ── Style / readability — kept narrow to avoid bikeshedding ─
38
+ - gofumpt # stricter gofmt
39
+ - goimports # import grouping + local prefix
40
+ - whitespace # leading/trailing whitespace
41
+ - misspell # typos in comments and strings
42
+ - unconvert # redundant type conversions
43
+ - unparam # unused function parameters
44
+ - ineffassign # ineffective assignments
45
+ - dupword # duplicate words ("the the")
46
+
47
+ # ── Architecture — file size, complexity, dead code ─────────
48
+ - gocognit # cognitive complexity per function (threshold 25)
49
+ - gocyclo # cyclomatic complexity per function (threshold 15)
50
+ - funlen # function length (90 lines, 60 statements)
51
+ - lll # line length 120
52
+ - nestif # excessive nesting depth (>4)
53
+ - dupl # duplicate code blocks
54
+ - revive # extensible replacement for golint; selected rules below
55
+ - unused # unused vars/funcs/types
56
+
57
+ # ── Exhaustiveness — Go's weakest spot ──────────────────────
58
+ - exhaustive # type switch and enum-like const groups completeness
59
+
60
+ # ── Security ────────────────────────────────────────────────
61
+ - gosec # CWE-aware security scanner
62
+
63
+ # ── Logging ─────────────────────────────────────────────────
64
+ - sloglint # slog attr style + no slog.Any(); enforce structured logs
65
+
66
+ # ── Performance ─────────────────────────────────────────────
67
+ - perfsprint # fmt.Sprintf where strconv suffices
68
+ - prealloc # slice prealloc when length is known
69
+ - makezero # make([]T, n) with non-zero n then append (the classic bug)
70
+
71
+ linters-settings:
72
+ errcheck:
73
+ check-type-assertions: true
74
+ check-blank: true # `_ = err` is a violation
75
+
76
+ govet:
77
+ enable-all: true
78
+ settings:
79
+ shadow:
80
+ strict: true
81
+ fieldalignment:
82
+ # On by default; this catches struct layouts wasting memory.
83
+ # Disable per-file with //nolint:fieldalignment ONLY for boundary types
84
+ # whose JSON tag order matters for OpenAPI doc stability.
85
+
86
+ errorlint:
87
+ errorf: true # %w mandatory for wrapping
88
+ asserts: true # errors.As over type-assertion on `error`
89
+ comparison: true # errors.Is over ==
90
+
91
+ gocognit:
92
+ min-complexity: 25
93
+
94
+ gocyclo:
95
+ min-complexity: 15
96
+
97
+ funlen:
98
+ lines: 90
99
+ statements: 60
100
+ ignore-comments: true
101
+
102
+ lll:
103
+ line-length: 120
104
+ tab-width: 4
105
+
106
+ nestif:
107
+ min-complexity: 4
108
+
109
+ exhaustive:
110
+ default-signifies-exhaustive: false
111
+ check:
112
+ - switch
113
+ - map
114
+
115
+ gosec:
116
+ excludes:
117
+ - G104 # handled by errcheck/errorlint
118
+ - G304 # file path provided as input — too noisy for CLIs
119
+
120
+ sloglint:
121
+ no-mixed-args: true # all attr or all key-value, never mixed
122
+ kv-only: false
123
+ attr-only: true # force slog.String(...) form
124
+ no-global: all # disallow slog.Info; force a logger receiver
125
+ context: scope # require *Context variants where ctx is in scope
126
+ static-msg: true # msg must be a string literal (not fmt.Sprintf)
127
+ no-raw-keys: true # use slog.String("key", ...) not raw "key", "val"
128
+ key-naming-case: snake
129
+
130
+ testifylint:
131
+ enable-all: true
132
+ disable:
133
+ - require-error # We DO use assert.Error in table-driven loops
134
+
135
+ revive:
136
+ severity: warning
137
+ rules:
138
+ - name: var-naming
139
+ - name: package-comments
140
+ - name: exported
141
+ - name: error-return
142
+ - name: error-naming
143
+ - name: errorf # use fmt.Errorf instead of errors.New(fmt.Sprintf)
144
+ - name: if-return
145
+ - name: indent-error-flow
146
+ - name: range-val-in-closure
147
+ - name: redefines-builtin-id
148
+ - name: superfluous-else
149
+ - name: unhandled-error
150
+ arguments:
151
+ - "fmt.Print.*"
152
+ - "fmt.Fprint.*"
153
+
154
+ perfsprint:
155
+ integer-format: true
156
+ error-format: true
157
+ bool-format: true
158
+ string-format: true
159
+
160
+ goimports:
161
+ local-prefixes:
162
+ - github.com/your-org
163
+
164
+ issues:
165
+ max-issues-per-linter: 0
166
+ max-same-issues: 0
167
+ exclude-rules:
168
+ # Tests get a longer leash on funlen + lll
169
+ - path: _test\.go
170
+ linters:
171
+ - funlen
172
+ - lll
173
+ - dupl
174
+ - gosec
175
+ # Generated code never lints
176
+ - path: \.pb\.go$
177
+ linters: [all]
178
+ - path: \.connect\.go$
179
+ linters: [all]
180
+ - path: ^.*sqlc/.*\.sql\.go$
181
+ linters: [all]
182
+
183
+ formatters:
184
+ enable:
185
+ - gofumpt
186
+ - goimports
187
+ ```
188
+
189
+ ## Per-linter rationale (why each is on)
190
+
191
+ | Linter | What it catches | Why no compromise |
192
+ |---|---|---|
193
+ | `errcheck` (incl. `check-blank: true`) | `_ = err`, ignored errors from `Close()`, `Write()`, `json.Marshal()` | Silent error ignore is the #1 Go bug class. Banning `_ = err` forces a decision at every site. |
194
+ | `errorlint` | `err == io.EOF` instead of `errors.Is(err, io.EOF)`; missing `%w` in `fmt.Errorf` | Once you wrap in middleware, `==` checks silently break. `errors.Is/As` is the only safe form. |
195
+ | `nilerr` / `nilnil` | `return nil` after `err != nil`; `return nil, nil` from `(*T, error)` | Classic AI-generated bugs. Linter catches them mechanically. |
196
+ | `bodyclose` | `defer resp.Body.Close()` missed | Single most common Go memory leak. |
197
+ | `contextcheck` | `ctx := context.Background()` inside a function that received `ctx` | Breaks cancellation propagation — the entire reason ctx exists. |
198
+ | `exhaustive` | `switch x.(type)` missing a sealed-interface variant | **Go's weakest type-system spot.** This linter is the closest thing to compiler-enforced exhaustiveness. |
199
+ | `sloglint` | `slog.Info(...)` (global), mixed `Any`/typed attrs | Without this, structured logging silently degrades into string concatenation. |
200
+ | `govet/shadow` strict | `err := ... ; if ... { err := ...; ... }` shadowing | Hides the real error from outer scope — extremely common. |
201
+ | `govet/fieldalignment` | Struct field order wasting memory | Cheap correctness signal. Disable per-file when JSON tag order matters for OpenAPI. |
202
+ | `copyloopvar` + `intrange` | Pre-1.22 loop-var capture and old `for i := 0; i < N; i++` | The language modernized; the lint enforces it. |
203
+ | `usetesting` | `os.Setenv` / `os.Mkdir` in tests instead of `t.Setenv` / `t.TempDir` | Avoids test isolation bugs. |
204
+ | `gocognit` / `gocyclo` / `funlen` | Functions exceeding cognitive thresholds | Direct architectural signal — same purpose as the 250 LOC ceiling, at function granularity. |
205
+ | `gosec` | CWE patterns — SQL injection, weak crypto, path traversal | Production must pass this. |
206
+ | `testifylint` | `assert.Equal` where `require.Equal` was meant; `ObjectsAreEqual` misuse | Subtle test-correctness bugs. |
207
+ | `perfsprint` | `fmt.Sprintf("%d", n)` instead of `strconv.Itoa(n)` | 5–10x faster in tight loops, lints catch the lazy form. |
208
+
209
+ ## `nolint` policy
210
+
211
+ `//nolint:linter1,linter2 // <reason>` is permitted with **two hard rules**:
212
+
213
+ 1. **One linter at a time per directive.** No `//nolint:all`. No omitting the linter name.
214
+ 2. **A reason after `//` is mandatory.** "Generated code", "false positive — protobuf imports", "OpenAPI field order" are acceptable. "Ignore" is not.
215
+
216
+ The skill auto-rejects `//nolint` without a reason. So does `revive` if you enable its `nolint` rule.
217
+
218
+ ## CI gate
219
+
220
+ ```bash
221
+ gofumpt -l . | (! grep .) # format
222
+ golangci-lint run --timeout 5m ./... # everything above
223
+ go vet -vettool=$(which fieldalignment) ./... # extra check (also in govet)
224
+ nilaway ./... # nil-deref static analysis
225
+ go test -race -shuffle=on -count=1 ./... # races + ordering
226
+ ```
227
+
228
+ Any non-zero exit = the change does not ship.
229
+
230
+ ## Sources
231
+
232
+ - golangci-lint v2 docs: https://golangci-lint.run/docs/configuration/
233
+ - staticcheck rules: https://staticcheck.dev/docs/checks
234
+ - sloglint: https://github.com/go-simpler/sloglint
235
+ - exhaustive: https://github.com/nishanths/exhaustive
236
+ - nilaway: https://github.com/uber-go/nilaway