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.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Type Patterns
|
|
2
|
+
|
|
3
|
+
How to use Go's *limited* type system to catch bugs at compile time. Go gives you fewer tools than Python/TS/Rust — this document covers the four patterns that buy back most of the safety.
|
|
4
|
+
|
|
5
|
+
The four patterns:
|
|
6
|
+
|
|
7
|
+
1. **Named types** for branding primitives (the Go answer to `NewType` / branded TS).
|
|
8
|
+
2. **Smart constructors with unexported fields** for parse-don't-validate.
|
|
9
|
+
3. **Sealed interfaces** for sum types, with `type switch` + `exhaustive` linter.
|
|
10
|
+
4. **Generics with constraints** for bounded polymorphism (1.18+).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Named types — distinct primitives
|
|
15
|
+
|
|
16
|
+
Same underlying type, different meaning. The Go type checker prevents *implicit* mixing — but explicit conversion is always possible. Treat this as a contract enforced at boundaries.
|
|
17
|
+
|
|
18
|
+
```go
|
|
19
|
+
package domain
|
|
20
|
+
|
|
21
|
+
type UserID string
|
|
22
|
+
type OrderID string
|
|
23
|
+
type EmailRaw string // raw, unvalidated string from input
|
|
24
|
+
|
|
25
|
+
func GetUser(id UserID) User { /* ... */ }
|
|
26
|
+
|
|
27
|
+
uid := UserID("u-123")
|
|
28
|
+
oid := OrderID("o-456")
|
|
29
|
+
|
|
30
|
+
GetUser(uid) // ✅ OK
|
|
31
|
+
GetUser(oid) // ❌ cannot use oid (type OrderID) as UserID
|
|
32
|
+
GetUser("u-123") // ❌ untyped string literal — Go DOES catch this
|
|
33
|
+
GetUser(UserID("u-123")) // ✅ explicit conversion — accept it
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Use when**: IDs, opaque tokens, foreign keys, units that share a base primitive.
|
|
37
|
+
|
|
38
|
+
**Reality check**: Go does NOT prevent `UserID(orderIDAsString)`. The defense is **smart constructors** for everything beyond an internal identifier. Use named types for cheap brand-only protection; combine with constructors for protection that actually holds.
|
|
39
|
+
|
|
40
|
+
### Time-of-day units
|
|
41
|
+
|
|
42
|
+
```go
|
|
43
|
+
type Milliseconds int64
|
|
44
|
+
type Seconds int64
|
|
45
|
+
|
|
46
|
+
func (ms Milliseconds) ToSeconds() Seconds {
|
|
47
|
+
return Seconds(ms / 1000)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No implicit `Milliseconds + Seconds`. The compiler refuses. Convert explicitly.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2. Smart constructors with unexported fields — the Go answer to Pydantic/Zod
|
|
56
|
+
|
|
57
|
+
The single most important pattern in this document. **Go has no Pydantic. It has this.**
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
package domain
|
|
61
|
+
|
|
62
|
+
import (
|
|
63
|
+
"errors"
|
|
64
|
+
"regexp"
|
|
65
|
+
"strings"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
var (
|
|
69
|
+
ErrInvalidEmail = errors.New("invalid email")
|
|
70
|
+
emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Email is a parsed, lowercased, valid email address.
|
|
74
|
+
// The zero value is invalid; construct via NewEmail.
|
|
75
|
+
type Email struct {
|
|
76
|
+
raw string // unexported — cannot be set from outside the package
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func NewEmail(s string) (Email, error) {
|
|
80
|
+
s = strings.TrimSpace(strings.ToLower(s))
|
|
81
|
+
if !emailRe.MatchString(s) {
|
|
82
|
+
return Email{}, ErrInvalidEmail
|
|
83
|
+
}
|
|
84
|
+
return Email{raw: s}, nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// String implements fmt.Stringer for printing.
|
|
88
|
+
func (e Email) String() string { return e.raw }
|
|
89
|
+
|
|
90
|
+
// MarshalJSON keeps the wire format unchanged.
|
|
91
|
+
func (e Email) MarshalJSON() ([]byte, error) {
|
|
92
|
+
return []byte(`"` + e.raw + `"`), nil
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// UnmarshalJSON is the parsing boundary — strict mode.
|
|
96
|
+
func (e *Email) UnmarshalJSON(data []byte) error {
|
|
97
|
+
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
|
|
98
|
+
return ErrInvalidEmail
|
|
99
|
+
}
|
|
100
|
+
parsed, err := NewEmail(string(data[1 : len(data)-1]))
|
|
101
|
+
if err != nil {
|
|
102
|
+
return err
|
|
103
|
+
}
|
|
104
|
+
*e = parsed
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Why this works**:
|
|
110
|
+
|
|
111
|
+
- `Email{raw: "anything"}` from outside the `domain` package is a compile error — `raw` is unexported.
|
|
112
|
+
- The only way to obtain a non-zero `Email` is `NewEmail(...)`, which validates.
|
|
113
|
+
- `UnmarshalJSON` routes wire input through the same constructor — boundary parsing is automatic.
|
|
114
|
+
- Once a function signature has `email Email`, the caller has *proven* it is valid. No internal `if email == ""` checks.
|
|
115
|
+
|
|
116
|
+
**Use for every domain value that has invariants**: emails, URLs, phone numbers, currency amounts, percentages, semver versions, IDs with format constraints, time ranges, anything you currently validate in three places.
|
|
117
|
+
|
|
118
|
+
### The "zero value problem"
|
|
119
|
+
|
|
120
|
+
Go's zero value (`Email{}`) is reachable. The mitigation is documentation + a `IsValid()` method when needed:
|
|
121
|
+
|
|
122
|
+
```go
|
|
123
|
+
func (e Email) IsZero() bool { return e.raw == "" }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or accept it: receivers that take `Email` should *never* receive a zero-value `Email` in correct code. Tests verify it.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 3. Sealed interfaces — sum types in Go
|
|
131
|
+
|
|
132
|
+
Go has no sum types. The closest thing: an interface with an **unexported method** that only types in the same package can satisfy, dispatched via `type switch`, with the `exhaustive` linter ensuring completeness.
|
|
133
|
+
|
|
134
|
+
```go
|
|
135
|
+
package event
|
|
136
|
+
|
|
137
|
+
// Event is a closed sum: Created | Updated | Deleted.
|
|
138
|
+
// The sealed() method is unexported so external packages cannot add variants.
|
|
139
|
+
type Event interface {
|
|
140
|
+
sealed()
|
|
141
|
+
OccurredAt() time.Time
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type Created struct {
|
|
145
|
+
UserID UserID
|
|
146
|
+
Email Email
|
|
147
|
+
Timestamp time.Time
|
|
148
|
+
}
|
|
149
|
+
func (Created) sealed() {}
|
|
150
|
+
func (e Created) OccurredAt() time.Time { return e.Timestamp }
|
|
151
|
+
|
|
152
|
+
type Updated struct {
|
|
153
|
+
UserID UserID
|
|
154
|
+
Changes map[string]any
|
|
155
|
+
Timestamp time.Time
|
|
156
|
+
}
|
|
157
|
+
func (Updated) sealed() {}
|
|
158
|
+
func (e Updated) OccurredAt() time.Time { return e.Timestamp }
|
|
159
|
+
|
|
160
|
+
type Deleted struct {
|
|
161
|
+
UserID UserID
|
|
162
|
+
Reason string
|
|
163
|
+
Timestamp time.Time
|
|
164
|
+
}
|
|
165
|
+
func (Deleted) sealed() {}
|
|
166
|
+
func (e Deleted) OccurredAt() time.Time { return e.Timestamp }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Consumer code:
|
|
170
|
+
|
|
171
|
+
```go
|
|
172
|
+
func Render(e event.Event) string {
|
|
173
|
+
switch v := e.(type) {
|
|
174
|
+
case event.Created:
|
|
175
|
+
return fmt.Sprintf("created %s with %s", v.UserID, v.Email)
|
|
176
|
+
case event.Updated:
|
|
177
|
+
return fmt.Sprintf("updated %s: %v", v.UserID, v.Changes)
|
|
178
|
+
case event.Deleted:
|
|
179
|
+
return fmt.Sprintf("deleted %s (reason: %s)", v.UserID, v.Reason)
|
|
180
|
+
default:
|
|
181
|
+
panic(fmt.Sprintf("unhandled event variant: %T", v))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The `panic` in `default` is the Go equivalent of TS's `assertNever` or Python's `assert_never`. It is only reachable if a new variant is added without updating the switch.
|
|
187
|
+
|
|
188
|
+
### The `exhaustive` linter — your compiler
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
# .golangci.yml
|
|
192
|
+
linters:
|
|
193
|
+
enable: [exhaustive]
|
|
194
|
+
linters-settings:
|
|
195
|
+
exhaustive:
|
|
196
|
+
check:
|
|
197
|
+
- switch
|
|
198
|
+
- map
|
|
199
|
+
default-signifies-exhaustive: false
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Now adding `event.Suspended` without updating `Render` is a **lint error**. This is the closest thing Go has to Rust's match exhaustiveness check. **Treat it as compulsory.**
|
|
203
|
+
|
|
204
|
+
### Sealed interface gotchas
|
|
205
|
+
|
|
206
|
+
- The method MUST be unexported (`sealed()`, not `Sealed()`). Otherwise other packages can implement it.
|
|
207
|
+
- `type switch` with `*Created` vs `Created` matters — pick value receivers and value cases, or pointer receivers and pointer cases. **Mixing them causes silent miss.**
|
|
208
|
+
- `interface{}` is not a sealed type. Anything implementing zero methods satisfies it. Sealed interfaces have at least the `sealed()` method.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 4. Generics with constraints — bounded polymorphism
|
|
213
|
+
|
|
214
|
+
Go 1.18+. Use for genuinely generic algorithms; **do not** use for "I want this to accept anything".
|
|
215
|
+
|
|
216
|
+
```go
|
|
217
|
+
import "cmp"
|
|
218
|
+
|
|
219
|
+
// Ordered constraint includes all ordered types (int, float, string, …).
|
|
220
|
+
func Max[T cmp.Ordered](a, b T) T {
|
|
221
|
+
if a > b { return a }
|
|
222
|
+
return b
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Custom constraint
|
|
226
|
+
type Stringer interface {
|
|
227
|
+
String() string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func Join[T Stringer](items []T, sep string) string {
|
|
231
|
+
parts := make([]string, len(items))
|
|
232
|
+
for i, item := range items {
|
|
233
|
+
parts[i] = item.String()
|
|
234
|
+
}
|
|
235
|
+
return strings.Join(parts, sep)
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The `cmp.Ordered` (Go 1.21+), `cmp.Compare`, and `slices`/`maps` packages cover the common cases without you writing constraints.
|
|
240
|
+
|
|
241
|
+
### When NOT to use generics
|
|
242
|
+
|
|
243
|
+
- "I want to accept multiple types, so I'll make it generic." Use an **interface** instead. Generics are for parametric polymorphism (same code, different types). Interfaces are for behavioral polymorphism (different code behind a contract).
|
|
244
|
+
- "I want to return `any`." Use a sealed interface and a `type switch`. `any` returns are anti-patterns past public APIs.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 5. Type assertions — the controlled escape hatch
|
|
249
|
+
|
|
250
|
+
```go
|
|
251
|
+
// Bad — panics on failure
|
|
252
|
+
e := evt.(event.Created)
|
|
253
|
+
|
|
254
|
+
// Good — comma-ok form, always
|
|
255
|
+
if e, ok := evt.(event.Created); ok {
|
|
256
|
+
// use e
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Use errors.As for error chains
|
|
260
|
+
var pgErr *pgconn.PgError
|
|
261
|
+
if errors.As(err, &pgErr) {
|
|
262
|
+
// pgErr is the wrapped pg error
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**The `errcheck` and `errorlint` linters reject bare type assertions on `error` values.** Use `errors.As`. See `error-handling.md`.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 6. Pointers vs values — the only durable rule
|
|
271
|
+
|
|
272
|
+
You will see endless debates. The rule that holds up:
|
|
273
|
+
|
|
274
|
+
- **If a type has a mutex, never copy it.** Use `*T` everywhere.
|
|
275
|
+
- **If a type is large (> 64 bytes) and read-only, pass by value or pointer is a measured choice.** Default to pointer for "large" things.
|
|
276
|
+
- **Receivers must be consistent.** All methods on `T` either take `T` or `*T`. Don't mix. The `staticcheck` linter catches mixed-receiver bugs.
|
|
277
|
+
- **`nil` pointer = absence. Zero value = "not set yet".** Choose ONE convention per type. Document it.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 7. `any` / `interface{}` — when it is acceptable
|
|
282
|
+
|
|
283
|
+
Almost never in domain code. Acceptable cases:
|
|
284
|
+
|
|
285
|
+
- JSON parsing of genuinely heterogeneous payloads (and even then, prefer `json.RawMessage` + targeted parsing).
|
|
286
|
+
- `fmt.Sprintf` arguments (variadic `any` is unavoidable here).
|
|
287
|
+
- Generic container internals before the user-facing API.
|
|
288
|
+
|
|
289
|
+
The skill rejects `any` in handler signatures, service signatures, store signatures. If you find yourself writing `func Handle(payload any) error`, you have a sealed-interface waiting to happen.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Sources
|
|
294
|
+
|
|
295
|
+
- "Parse, don't validate" — Alexis King: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
|
|
296
|
+
- exhaustive linter: https://github.com/nishanths/exhaustive
|
|
297
|
+
- Generics constraints: https://go.dev/blog/intro-generics
|
|
298
|
+
- cmp.Ordered: https://pkg.go.dev/cmp
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
|
|
2
|
+
# Python Programmer
|
|
3
|
+
|
|
4
|
+
Modern Python. Type-strict, stack-first, async-correct.
|
|
5
|
+
|
|
6
|
+
## Philosophy
|
|
7
|
+
|
|
8
|
+
The type checker is your compiler. Make illegal states unrepresentable. Parse at boundaries. Own resources explicitly. Every function has a contract; the type system enforces it.
|
|
9
|
+
|
|
10
|
+
## Hard rules
|
|
11
|
+
|
|
12
|
+
These are deliberate project choices. Violations are always wrong, not "style preferences".
|
|
13
|
+
|
|
14
|
+
### Tooling
|
|
15
|
+
|
|
16
|
+
| Category | Use | Never |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Package manager | `uv` | pip, poetry, conda, pipenv |
|
|
19
|
+
| Type checker | `basedpyright` (`typeCheckingMode = "all"`) | pyright, mypy |
|
|
20
|
+
| Linter + formatter | `ruff` (`select = ["ALL"]`) | flake8, black, isort, autopep8 |
|
|
21
|
+
| Async runtime | `anyio` | `import asyncio` |
|
|
22
|
+
| Data | `polars` + `duckdb` + `numpy` | pandas |
|
|
23
|
+
| Web framework | FastAPI + Pydantic v2 | Flask, Django REST |
|
|
24
|
+
| ORM | SQLAlchemy 2.x async | Django ORM, Tortoise |
|
|
25
|
+
| HTTP client | [`httpx2`](https://github.com/pydantic/httpx2) | requests, aiohttp, httpx |
|
|
26
|
+
| Testing | `pytest` | unittest |
|
|
27
|
+
| CLI | `typer` + `rich` | argparse, click, fire |
|
|
28
|
+
|
|
29
|
+
### The iron list
|
|
30
|
+
|
|
31
|
+
1. **Frozen by default** — `@dataclass(frozen=True, slots=True)`. Pydantic: `model_config = ConfigDict(frozen=True)`. Mutable only when mutation is the documented purpose.
|
|
32
|
+
2. **NewType for distinct IDs** — `UserId = NewType("UserId", int)`. Never pass raw `int` where a branded type exists.
|
|
33
|
+
3. **`match` only for variants, `if` only for booleans** — **NEVER** use `if/elif/else` to discriminate on type (`isinstance`), enum value, or literal variant. `match/case` is mandatory for these — non-negotiable. **ALWAYS** end with `case unreachable: assert_never(unreachable)` — bare `case _: pass` and `case _: raise ValueError` are banned (they silently swallow new variants). `if/else` is fine only for boolean expressions, range checks, and predicate calls that aren't variant discrimination. See "Why `if/elif` on variants is banned" below for examples.
|
|
34
|
+
4. **Protocol over ABC** — `typing.Protocol` for interfaces. ABC only when you need shared method implementation.
|
|
35
|
+
5. **No raw dicts in signatures** — params and returns use `TypedDict`, `dataclass`, or Pydantic model. Internal scratch dicts are fine.
|
|
36
|
+
6. **Parse, don't validate** — constructors produce typed objects or raise. Never pass unvalidated data deeper into the call stack.
|
|
37
|
+
7. **Typed errors** — error types are dataclasses or exceptions with typed fields. Never `raise ValueError("something")` with a bare string. Use union returns when the caller is within 1-2 call levels and must handle the outcome (repository → service). Use exceptions when the error should propagate up many layers to a boundary handler (service → HTTP handler).
|
|
38
|
+
8. **Final for constants** — module-level constants use `Final`. Mutable module globals are a code smell.
|
|
39
|
+
9. **Explicit None** — annotate `-> X | None`. Never return `None` from a function whose signature omits it.
|
|
40
|
+
10. **Context managers for resources** — files, DB connections, HTTP clients, locks. No manual `.close()`.
|
|
41
|
+
11. **No Any, no object** — both are banned as type annotations. `object` erases all structural information (zero callable attributes, zero narrowing). Use `Protocol` (structural typing), `TypeVar` (generic pass-through), explicit union (known variants), or `TypedDict` (dict shapes).
|
|
42
|
+
12. **No cast** — `cast()` is banned. Redesign the types.
|
|
43
|
+
13. **No type: ignore** — fix the type error. The checker is right; you are wrong.
|
|
44
|
+
14. **No broad except** — `except Exception` and `except BaseException` are banned. Catch the **specific** exception you expect. A broad catch swallows bugs you need to see — `KeyError`, `AttributeError`, `TypeError` all vanish silently. If you genuinely need a catch-all at a top-level boundary (CLI entry, HTTP handler), use `# noqa: BROAD_EXCEPT_OK` and log + re-raise.
|
|
45
|
+
|
|
46
|
+
### Typing and safety
|
|
47
|
+
|
|
48
|
+
- `basedpyright` in `typeCheckingMode = "all"`. Every public function has full annotations. Internal helpers: annotate return type; parameter types may be inferred.
|
|
49
|
+
- `ruff` with `select = ["ALL"]`. Override specific rules per project in `pyproject.toml`, never globally disable the strict baseline.
|
|
50
|
+
- Every new function must have a `docstring` unless its name + signature makes it completely obvious (e.g. `def full_name(first: str, last: str) -> str:`).
|
|
51
|
+
- Use `X | Y` union syntax (PEP 604), never `Union[X, Y]` or `Optional[X]`.
|
|
52
|
+
|
|
53
|
+
### Why `object` is banned
|
|
54
|
+
|
|
55
|
+
`object` pretends to be safe ("it's the top type!") but gives **zero** narrowing and **zero** attributes. Even `Any` is more honest — it admits the boundary is untyped.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# BANNED
|
|
59
|
+
def process(data: object) -> object: ...
|
|
60
|
+
def store(items: list[object]) -> None: ...
|
|
61
|
+
results: dict[str, object] = {}
|
|
62
|
+
|
|
63
|
+
# GOOD — Protocol for structural typing
|
|
64
|
+
class Serializable(Protocol):
|
|
65
|
+
def serialize(self) -> bytes: ...
|
|
66
|
+
def process(data: Serializable) -> ProcessResult: ...
|
|
67
|
+
|
|
68
|
+
# GOOD — TypeVar for generic pass-through
|
|
69
|
+
def identity[T](x: T) -> T: ...
|
|
70
|
+
def first[T](items: Sequence[T]) -> T: ...
|
|
71
|
+
|
|
72
|
+
# GOOD — explicit union for known variants
|
|
73
|
+
def parse(raw: str | bytes) -> Document: ...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Why `if/elif` on variants is banned
|
|
77
|
+
|
|
78
|
+
`if/elif/else` chains on type, enum, or literal values lose compile-time exhaustiveness. When a new variant is added, nothing warns you. `match/case` + `assert_never` does.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# BANNED — if/elif for type discrimination
|
|
82
|
+
if isinstance(event, Click):
|
|
83
|
+
handle_click(event.x, event.y)
|
|
84
|
+
elif isinstance(event, Scroll):
|
|
85
|
+
handle_scroll(event.delta)
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unknown: {event}") # runtime bomb
|
|
88
|
+
|
|
89
|
+
# BANNED — if/elif for enum discrimination
|
|
90
|
+
if status == Status.PENDING:
|
|
91
|
+
start_review()
|
|
92
|
+
elif status == Status.ACTIVE:
|
|
93
|
+
continue_processing()
|
|
94
|
+
elif status == Status.CLOSED:
|
|
95
|
+
archive()
|
|
96
|
+
|
|
97
|
+
# BANNED — non-exhaustive match (swallows new variants)
|
|
98
|
+
match event:
|
|
99
|
+
case Click(x, y): handle_click(x, y)
|
|
100
|
+
case _: pass
|
|
101
|
+
|
|
102
|
+
# GOOD — exhaustive match with assert_never
|
|
103
|
+
match event:
|
|
104
|
+
case Click(x=x, y=y):
|
|
105
|
+
handle_click(x, y)
|
|
106
|
+
case Scroll(delta=delta):
|
|
107
|
+
handle_scroll(delta)
|
|
108
|
+
case unreachable:
|
|
109
|
+
assert_never(unreachable)
|
|
110
|
+
|
|
111
|
+
# GOOD — enum match
|
|
112
|
+
match status:
|
|
113
|
+
case Status.PENDING: start_review()
|
|
114
|
+
case Status.ACTIVE: continue_processing()
|
|
115
|
+
case Status.CLOSED: archive()
|
|
116
|
+
case unreachable: assert_never(unreachable)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`if/else` is fine for boolean conditions and range checks — things that aren't variant discrimination:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# FINE — boolean, not variant
|
|
123
|
+
if age >= 18:
|
|
124
|
+
grant_access()
|
|
125
|
+
else:
|
|
126
|
+
deny_access()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Why broad `except` is banned
|
|
130
|
+
|
|
131
|
+
`except Exception` catches **every** non-system exception — `KeyError`, `TypeError`, `AttributeError`, `ValueError` all vanish. You lose the stack trace that would have told you exactly what went wrong. The fix is always to name the exception you expect.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# BANNED — swallows bugs
|
|
135
|
+
try:
|
|
136
|
+
result = api.fetch(url)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(e)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# BANNED — catch-and-ignore
|
|
142
|
+
try:
|
|
143
|
+
parse(data)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# GOOD — catch what you expect
|
|
148
|
+
try:
|
|
149
|
+
result = api.fetch(url)
|
|
150
|
+
except httpx.HTTPStatusError as e:
|
|
151
|
+
logger.error("API %d: %s", e.response.status_code, e.request.url)
|
|
152
|
+
return None
|
|
153
|
+
except httpx.ConnectError:
|
|
154
|
+
raise ServiceUnavailableError(service="api") from None
|
|
155
|
+
|
|
156
|
+
# GOOD — top-level boundary (only place broad catch is acceptable)
|
|
157
|
+
def main() -> int: # noqa: BROAD_EXCEPT_OK
|
|
158
|
+
try:
|
|
159
|
+
return run()
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.exception("unhandled error")
|
|
162
|
+
return 1
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Async
|
|
166
|
+
|
|
167
|
+
- `import asyncio` is **BANNED**. Use `import anyio`.
|
|
168
|
+
- For background tasks, use `anyio.create_task_group`. Never fire-and-forget with `asyncio.create_task`.
|
|
169
|
+
- For concurrency gates, use `anyio.CapacityLimiter` (not `asyncio.Semaphore`).
|
|
170
|
+
- Load `async-anyio.md` when writing async code for the full pattern library.
|
|
171
|
+
|
|
172
|
+
### Data modeling — which container, when
|
|
173
|
+
|
|
174
|
+
All model fields carry type annotations. No `Any`, no untyped dicts in public APIs.
|
|
175
|
+
Use `polars` + `duckdb` for data. pandas is never the right answer in this stack.
|
|
176
|
+
|
|
177
|
+
| Situation | Use |
|
|
178
|
+
|---|---|
|
|
179
|
+
| User input, API request/response | `Pydantic BaseModel (frozen=True)` |
|
|
180
|
+
| Internal value object (no I/O) | `@dataclass(frozen=True, slots=True)` |
|
|
181
|
+
| Function with multiple outcomes | Union of frozen dataclasses + `match` |
|
|
182
|
+
| Dict shape for JSON compat / `**kwargs` | `TypedDict` |
|
|
183
|
+
| Fixed constants | `StrEnum` / `IntEnum` |
|
|
184
|
+
| Distinct primitive (UserId vs MovieId) | `NewType` |
|
|
185
|
+
| Contract / capability | `Protocol` |
|
|
186
|
+
| Contract + shared implementation | `ABC` |
|
|
187
|
+
| ORM model (SQLAlchemy) | `Mapped[]` — inherently mutable, `# noqa: MUTABLE_OK` |
|
|
188
|
+
| Config from env vars | `pydantic-settings BaseSettings` |
|
|
189
|
+
|
|
190
|
+
**The one rule**: data crosses trust boundary → Pydantic. Everything else → dataclass.
|
|
191
|
+
|
|
192
|
+
Load `data-modeling.md` for the full decision flowchart and comparison matrix.
|
|
193
|
+
|
|
194
|
+
### When frozen=True does not apply
|
|
195
|
+
|
|
196
|
+
- **ORM models** — SQLAlchemy `Mapped[]` requires mutation. Use `# noqa: MUTABLE_OK`.
|
|
197
|
+
- **Builder / accumulator** — object exists to be mutated (counter, buffer, state machine). Docstring must explain why.
|
|
198
|
+
- **Pydantic Settings** — tests override fields. Mutable is acceptable.
|
|
199
|
+
|
|
200
|
+
If you need `# noqa: MUTABLE_OK`, the class docstring must say why mutation is required.
|
|
201
|
+
|
|
202
|
+
### Libraries
|
|
203
|
+
|
|
204
|
+
Canonical defaults (override only if `pyproject.toml` explicitly picks something else):
|
|
205
|
+
|
|
206
|
+
| Domain | Library | Reason |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| CLI | `typer` | Type-annotated CLI from function sigs |
|
|
209
|
+
| Pretty output | `rich` | Tables, progress, tracebacks, markdown |
|
|
210
|
+
| HTTP client | [`httpx2`](https://github.com/pydantic/httpx2) | Next-gen HTTP client (Pydantic stewardship), HTTP/2, brotli+zstd. Always `httpx2[http2,brotli,zstd]`. See `httpx2-optimization.md` |
|
|
211
|
+
| Validation | `pydantic` v2 | Fast native validator, JSON Schema |
|
|
212
|
+
| Web API | `fastapi` | Async, Pydantic-native, OpenAPI |
|
|
213
|
+
| ORM | `sqlalchemy` 2.x async | `Mapped[]` types, async sessions |
|
|
214
|
+
| DB driver (Postgres) | `asyncpg` (via SQLAlchemy) | Fastest PG driver |
|
|
215
|
+
| AI agents | `pydantic-ai` | Typed deps, structured output |
|
|
216
|
+
| TUI | `textual` | Rich-based, CSS layout, widgets |
|
|
217
|
+
| Logging | `rich.logging.RichHandler` | Pretty; swap to `structlog` in prod |
|
|
218
|
+
|
|
219
|
+
## pyproject.toml — the one true config
|
|
220
|
+
|
|
221
|
+
Scaffold a new project with all strict defaults pre-configured:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uv run ../../scripts/python/new-project.py myproject
|
|
225
|
+
uv run ../../scripts/python/new-project.py myproject --path ./workspace
|
|
226
|
+
uv run ../../scripts/python/new-project.py myproject --lib # publishable library
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Creates via `uv init`, then injects basedpyright `typeCheckingMode = "all"` + ruff `select = ["ALL"]` + pytest strict. Cross-platform (macOS, Linux, Windows).
|
|
230
|
+
|
|
231
|
+
For manual setup: `uv init --app myproject`, then load `pyproject-strict.md`.
|
|
232
|
+
|
|
233
|
+
## PEP 723 — inline script metadata (mandatory for ALL scripts)
|
|
234
|
+
|
|
235
|
+
Every `.py` script — even throwaway — MUST use PEP 723 inline metadata with the `# ─── How to run ───` comment block. No venv, no `requirements.txt`. The script IS the environment spec. A script without the usage comment block is incomplete.
|
|
236
|
+
|
|
237
|
+
Scaffold with: `uv run ../../scripts/python/new-script.py <name> --deps "httpx2[http2,brotli,zstd]"` (writes to temp dir by default, `--output` for specific path).
|
|
238
|
+
|
|
239
|
+
Load `one-liners.md` for full patterns, examples, and anti-patterns.
|
|
240
|
+
|
|
241
|
+
## Reference loading
|
|
242
|
+
|
|
243
|
+
Load on demand — not all at once.
|
|
244
|
+
|
|
245
|
+
| Need | Load |
|
|
246
|
+
|---|---|
|
|
247
|
+
| Full pyproject.toml config | `pyproject-strict.md` |
|
|
248
|
+
| Type patterns (NewType, Final, enums, narrowing) | `type-patterns.md` |
|
|
249
|
+
| Data modeling (container choice, frozen, parse-don't-validate) | `data-modeling.md` |
|
|
250
|
+
| Error handling (typed errors, union returns, exhaustive match) | `error-handling.md` |
|
|
251
|
+
| Async patterns (anyio) | `async-anyio.md` |
|
|
252
|
+
| Data processing (polars / duckdb) | `data-processing.md` |
|
|
253
|
+
| FastAPI + SQLAlchemy stack | `fastapi-stack.md` |
|
|
254
|
+
| Library decision tree | `libraries.md` |
|
|
255
|
+
| **httpx2 optimization** (MUST load for any network code) | `httpx2-optimization.md` |
|
|
256
|
+
| **orjson** (when JSON is in the hot path; FastAPI/Pydantic v2 integration) | `orjson-stack.md` |
|
|
257
|
+
| One-liner scripts (PEP 723) | `one-liners.md` |
|
|
258
|
+
| PydanticAI agents | `pydantic-ai.md` |
|
|
259
|
+
| Textual TUI | `textual-tui.md` |
|
|
260
|
+
|
|
261
|
+
## httpx2 — mandatory for ALL network requests
|
|
262
|
+
|
|
263
|
+
Every outgoing HTTP call MUST use [`httpx2`](https://github.com/pydantic/httpx2) (`httpx2[http2,brotli,zstd]`). Never `requests`, never `aiohttp`, never the original `httpx`.
|
|
264
|
+
|
|
265
|
+
**ALL optimizations are ON by default — not optional, not progressive, not "nice to have".** A bare `httpx2.AsyncClient()` is a bug — treat it like a lint violation. The correct way is the factory pattern in `httpx2-optimization.md` with: HTTP/2 enabled, tuned connection pool (200/40/30s), split timeouts (5/30/10/10), transport retries (3), TCP_NODELAY, follow_redirects, and event hooks for observability.
|
|
266
|
+
|
|
267
|
+
When writing or reviewing ANY network code, **ALWAYS load `httpx2-optimization.md`** and use the factory pattern verbatim. No exceptions.
|
|
268
|
+
|
|
269
|
+
## No-excuse audit
|
|
270
|
+
|
|
271
|
+
Violations caught by `../../scripts/python/check-no-excuse-rules.py`. Run after every edit session.
|
|
272
|
+
|
|
273
|
+
| Rule ID | Catches | Opt-out |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `cast-any` | `cast(Any, ...)` | None — redesign types |
|
|
276
|
+
| `type-ignore` | `# type: ignore` | None — fix the type |
|
|
277
|
+
| `pyright-ignore` | `# pyright: ignore` | None — fix the type |
|
|
278
|
+
| `bare-except` | `except:` with no class | None — name the exception |
|
|
279
|
+
| `silent-except` | `except X: pass` / `except X: ...` | None — handle or re-raise |
|
|
280
|
+
| `no-asyncio` | `import asyncio` | `# noqa: ANYIO_OK` |
|
|
281
|
+
| `no-pandas` | `import pandas` | `# noqa: PANDAS_OK` |
|
|
282
|
+
| `mutable-dataclass` | `@dataclass` without `frozen=True` | `# noqa: MUTABLE_OK` |
|
|
283
|
+
| `missing-slots` | `@dataclass` without `slots=True` | `# noqa: SLOTS_OK` |
|
|
284
|
+
| `raw-dict-return` | `-> dict` in function return type | `# noqa: DICT_OK` |
|
|
285
|
+
| `missing-assert-never` | `match` block without `assert_never` default | `# noqa: MATCH_OK` |
|
|
286
|
+
| `generic-exception` | `raise ValueError("...")` / `raise TypeError("...")` with bare string | `# noqa: GENERIC_ERR_OK` |
|
|
287
|
+
| `no-object` | `object` used as type annotation (param, return, generic arg) | `# noqa: OBJECT_OK` |
|
|
288
|
+
| `if-elif-on-variant` | `if isinstance()`/`if x == Enum.V` chain that should be `match/case` | `# noqa: IF_VARIANT_OK` |
|
|
289
|
+
| `oversized-module` | File exceeds 250 pure LOC (non-blank, non-comment) | `# noqa: SIZE_OK` |
|
|
290
|
+
| `broad-except` | `except Exception` / `except BaseException` (too broad) | `# noqa: BROAD_EXCEPT_OK` |
|
|
291
|
+
|
|
292
|
+
Fix every violation before declaring work done. basedpyright + ruff strict config catches the rest.
|
|
293
|
+
|
|
294
|
+
## In tests
|
|
295
|
+
|
|
296
|
+
Tests are strict too, with these exceptions (already configured in `pyproject.toml` per-file-ignores):
|
|
297
|
+
|
|
298
|
+
| In tests you may | Why |
|
|
299
|
+
|---|---|
|
|
300
|
+
| Use `assert` | That's how pytest works (`S101` ignored) |
|
|
301
|
+
| Use magic numbers | Test data (`PLR2004` ignored) |
|
|
302
|
+
| Access `_private` members | Testing internals (`SLF001` ignored) |
|
|
303
|
+
| Skip docstrings | Test names are the docs (`D` ignored) |
|
|
304
|
+
| Have unused function args | Fixtures (`ARG` ignored) |
|
|
305
|
+
|
|
306
|
+
Tests still follow the iron list — frozen dataclasses, typed errors, exhaustive match. If test fixtures need mutable state, use `# noqa: MUTABLE_OK` on the fixture class.
|
|
307
|
+
|
|
308
|
+
## Existing codebases
|
|
309
|
+
|
|
310
|
+
When editing an existing file that doesn't follow these rules: **write new code in strict style, don't refactor existing code in the same change.** Mixing feature work with style migration makes reviews harder and bugs likelier.
|
|
311
|
+
|
|
312
|
+
## Activation
|
|
313
|
+
|
|
314
|
+
This skill activates whenever you are writing or modifying any `.py` file. Even one-off scripts get the strict treatment — that is the whole point of PEP 723 + uv: production hygiene with throwaway ergonomics.
|