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,152 @@
|
|
|
1
|
+
# Strict tsconfig + Biome
|
|
2
|
+
|
|
3
|
+
The canonical ultra-strict config. Copy-paste, then add your own paths.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## tsconfig.json
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
{
|
|
11
|
+
"compilerOptions": {
|
|
12
|
+
// ── Strict core ──────────────────────────────────────────
|
|
13
|
+
"strict": true, // enables all strict* flags below
|
|
14
|
+
// strict includes: strictNullChecks, strictFunctionTypes,
|
|
15
|
+
// strictBindCallApply, strictPropertyInitialization,
|
|
16
|
+
// noImplicitAny, noImplicitThis, alwaysStrict, useUnknownInCatchVariables
|
|
17
|
+
|
|
18
|
+
// ── Additional strict flags (NOT included in "strict") ──
|
|
19
|
+
"noUncheckedIndexedAccess": true, // obj[key] is T | undefined, not T
|
|
20
|
+
"exactOptionalPropertyTypes": true, // { x?: string } !== { x: string | undefined }
|
|
21
|
+
"noFallthroughCasesInSwitch": true, // switch fall-through is an error
|
|
22
|
+
"noPropertyAccessFromIndexSignature": true, // forces bracket notation for index sigs
|
|
23
|
+
"forceConsistentCasingInFileNames": true, // prevents case-sensitivity bugs on macOS/Win
|
|
24
|
+
|
|
25
|
+
// ── Module system ────────────────────────────────────────
|
|
26
|
+
"module": "ESNext",
|
|
27
|
+
"moduleResolution": "bundler",
|
|
28
|
+
"verbatimModuleSyntax": true, // forces `import type` for type-only imports
|
|
29
|
+
"isolatedModules": true, // safe for esbuild / swc / Bun transpilation
|
|
30
|
+
"esModuleInterop": true,
|
|
31
|
+
"resolveJsonModule": true,
|
|
32
|
+
|
|
33
|
+
// ── Target ───────────────────────────────────────────────
|
|
34
|
+
"target": "ESNext",
|
|
35
|
+
"lib": ["ESNext"],
|
|
36
|
+
|
|
37
|
+
// ── Emit ─────────────────────────────────────────────────
|
|
38
|
+
"declaration": true,
|
|
39
|
+
"declarationMap": true,
|
|
40
|
+
"sourceMap": true,
|
|
41
|
+
"outDir": "dist",
|
|
42
|
+
"rootDir": "src",
|
|
43
|
+
|
|
44
|
+
// ── Performance ──────────────────────────────────────────
|
|
45
|
+
"skipLibCheck": true, // skip checking .d.ts files for speed
|
|
46
|
+
"incremental": true
|
|
47
|
+
},
|
|
48
|
+
"include": ["src"],
|
|
49
|
+
"exclude": ["node_modules", "dist"]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### What each extra flag catches
|
|
54
|
+
|
|
55
|
+
| Flag | What it prevents |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `noUncheckedIndexedAccess` | `arr[0]` is `T \| undefined`, not `T`. Forces you to check before using. |
|
|
58
|
+
| `exactOptionalPropertyTypes` | `{ x?: string }` means "missing or string", NOT "string \| undefined". Assigns `undefined` explicitly? Type error. |
|
|
59
|
+
| `noFallthroughCasesInSwitch` | Forgetting `break` / `return` in a switch case. |
|
|
60
|
+
| `noPropertyAccessFromIndexSignature` | `obj.foo` on `Record<string, X>` is an error. Use `obj["foo"]`. |
|
|
61
|
+
| `verbatimModuleSyntax` | Forces `import type { X }` for type-only imports. Prevents runtime import of types. |
|
|
62
|
+
|
|
63
|
+
### Bun-specific additions
|
|
64
|
+
|
|
65
|
+
For Bun projects, add to `compilerOptions`:
|
|
66
|
+
```jsonc
|
|
67
|
+
{
|
|
68
|
+
"types": ["bun-types"],
|
|
69
|
+
"moduleDetection": "force"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## biome.jsonc
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
{
|
|
79
|
+
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
|
80
|
+
"organizeImports": {
|
|
81
|
+
"enabled": true
|
|
82
|
+
},
|
|
83
|
+
"linter": {
|
|
84
|
+
"enabled": true,
|
|
85
|
+
"rules": {
|
|
86
|
+
"recommended": true,
|
|
87
|
+
"suspicious": {
|
|
88
|
+
"noExplicitAny": "error",
|
|
89
|
+
"noConfusingVoidType": "error",
|
|
90
|
+
"noFallthroughSwitchClause": "error"
|
|
91
|
+
},
|
|
92
|
+
"style": {
|
|
93
|
+
"noDefaultExport": "error",
|
|
94
|
+
"useImportType": "error",
|
|
95
|
+
"noNonNullAssertion": "error",
|
|
96
|
+
"useEnumInitializers": "off",
|
|
97
|
+
"noParameterAssign": "error"
|
|
98
|
+
},
|
|
99
|
+
"correctness": {
|
|
100
|
+
"noUnusedVariables": "error",
|
|
101
|
+
"noUnusedImports": "error"
|
|
102
|
+
},
|
|
103
|
+
"complexity": {
|
|
104
|
+
"noBannedTypes": "error"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"formatter": {
|
|
109
|
+
"enabled": true,
|
|
110
|
+
"indentStyle": "space",
|
|
111
|
+
"indentWidth": 2,
|
|
112
|
+
"lineWidth": 100
|
|
113
|
+
},
|
|
114
|
+
"javascript": {
|
|
115
|
+
"formatter": {
|
|
116
|
+
"quoteStyle": "double",
|
|
117
|
+
"semicolons": "asNeeded"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"files": {
|
|
121
|
+
"ignore": ["node_modules", "dist", "build", ".next", ".nuxt", "coverage"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Key Biome rules
|
|
127
|
+
|
|
128
|
+
| Rule | What |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `noExplicitAny` | `any` in annotations is an error |
|
|
131
|
+
| `noNonNullAssertion` | `x!` is an error |
|
|
132
|
+
| `noDefaultExport` | Forces named exports |
|
|
133
|
+
| `useImportType` | Forces `import type` for type-only imports |
|
|
134
|
+
| `noParameterAssign` | No mutation of function parameters |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## CI gate
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bunx biome check .
|
|
142
|
+
bunx tsc --noEmit
|
|
143
|
+
bun test
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Sources
|
|
149
|
+
|
|
150
|
+
- TypeScript: [tsconfig reference](https://www.typescriptlang.org/tsconfig)
|
|
151
|
+
- Biome: [configuration](https://biomejs.dev/reference/configuration/)
|
|
152
|
+
- Total TypeScript: [tsconfig cheat sheet](https://www.totaltypescript.com/tsconfig-cheat-sheet)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Type Patterns
|
|
2
|
+
|
|
3
|
+
How to use TypeScript's type system to catch bugs at compile time.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Branded types — distinct primitives
|
|
8
|
+
|
|
9
|
+
Same runtime type, different meaning. The compiler prevents mixing.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
declare const brand: unique symbol
|
|
13
|
+
type Brand<T, B extends string> = T & { readonly [brand]: B }
|
|
14
|
+
|
|
15
|
+
type UserId = Brand<string, "UserId">
|
|
16
|
+
type OrderId = Brand<string, "OrderId">
|
|
17
|
+
type Milliseconds = Brand<number, "Milliseconds">
|
|
18
|
+
type Seconds = Brand<number, "Seconds">
|
|
19
|
+
|
|
20
|
+
function UserId(value: string): UserId { return value as UserId }
|
|
21
|
+
function OrderId(value: string): OrderId { return value as OrderId }
|
|
22
|
+
|
|
23
|
+
function getUser(id: UserId): User { ... }
|
|
24
|
+
|
|
25
|
+
getUser(UserId("abc")) // OK
|
|
26
|
+
getUser(OrderId("abc")) // type error: OrderId is not UserId
|
|
27
|
+
getUser("abc") // type error: string is not UserId
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With Zod (preferred at boundaries):
|
|
31
|
+
```typescript
|
|
32
|
+
import { z } from "zod"
|
|
33
|
+
|
|
34
|
+
const UserIdSchema = z.string().uuid().brand("UserId")
|
|
35
|
+
type UserId = z.infer<typeof UserIdSchema>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Use when**: IDs, indices, units of measurement — any pair where swapping is a bug.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## as const — literal types from values
|
|
43
|
+
|
|
44
|
+
Freezes a value to its narrowest possible type. The foundation for enum-free TypeScript.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const ROLES = ["admin", "user", "guest"] as const
|
|
48
|
+
type Role = (typeof ROLES)[number] // "admin" | "user" | "guest"
|
|
49
|
+
|
|
50
|
+
const HTTP_STATUS = {
|
|
51
|
+
OK: 200,
|
|
52
|
+
NOT_FOUND: 404,
|
|
53
|
+
INTERNAL: 500,
|
|
54
|
+
} as const
|
|
55
|
+
type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS] // 200 | 404 | 500
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Use when**: fixed set of constants. Replaces `enum` entirely.
|
|
59
|
+
**Skip when**: the set is open-ended or user-defined.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## satisfies — validate without widening
|
|
64
|
+
|
|
65
|
+
Type-checks a value against a type while preserving the literal type. Best of both worlds.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
type Config = Record<string, string | number>
|
|
69
|
+
|
|
70
|
+
// BAD — widens to Record<string, string | number>
|
|
71
|
+
const config: Config = { api: "https://api.example.com", timeout: 30 }
|
|
72
|
+
config.api // string | number — lost the narrowing
|
|
73
|
+
|
|
74
|
+
// GOOD — validates AND preserves literal types
|
|
75
|
+
const config = {
|
|
76
|
+
api: "https://api.example.com",
|
|
77
|
+
timeout: 30,
|
|
78
|
+
} satisfies Config
|
|
79
|
+
config.api // string (narrowed)
|
|
80
|
+
config.timeout // number (narrowed)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Use when**: you want type validation on a value without losing narrowing.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Discriminated unions — algebraic data types
|
|
88
|
+
|
|
89
|
+
Model every outcome as a type. Force the caller to handle all cases.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
type GetUserResult =
|
|
93
|
+
| { readonly kind: "found"; readonly user: User }
|
|
94
|
+
| { readonly kind: "not_found"; readonly id: UserId }
|
|
95
|
+
| { readonly kind: "forbidden"; readonly reason: string }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The `kind` field (or `type`, `status`, `_tag`) is the discriminant. TypeScript narrows on it automatically.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Exhaustive switch — assertNever
|
|
103
|
+
|
|
104
|
+
Every switch on a discriminated union ends with a default that calls `assertNever`.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
function assertNever(x: never): never {
|
|
108
|
+
throw new Error(`Unexpected value: ${JSON.stringify(x)}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleResult(result: GetUserResult): string {
|
|
112
|
+
switch (result.kind) {
|
|
113
|
+
case "found":
|
|
114
|
+
return result.user.name
|
|
115
|
+
case "not_found":
|
|
116
|
+
return `No user ${result.id}`
|
|
117
|
+
case "forbidden":
|
|
118
|
+
return `Denied: ${result.reason}`
|
|
119
|
+
default:
|
|
120
|
+
return assertNever(result)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Add a new variant to `GetUserResult`? The compiler errors on the `assertNever` call until you handle it.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Narrowing — let the compiler follow your logic
|
|
130
|
+
|
|
131
|
+
TypeScript narrows types through `typeof`, `instanceof`, `in`, equality checks, and discriminants.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
function process(value: string | number | null): string {
|
|
135
|
+
if (value === null) return "nothing"
|
|
136
|
+
// compiler knows: string | number
|
|
137
|
+
|
|
138
|
+
if (typeof value === "string") return value.toUpperCase()
|
|
139
|
+
// compiler knows: number
|
|
140
|
+
|
|
141
|
+
return String(value * 2)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Custom type guards
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
function isNonNull<T>(value: T | null | undefined): value is T {
|
|
149
|
+
return value != null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const items = [1, null, 2, undefined, 3]
|
|
153
|
+
const clean = items.filter(isNonNull) // number[]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## import type — separate values from types
|
|
159
|
+
|
|
160
|
+
Always use `import type` for type-only imports. Enforced by `verbatimModuleSyntax`.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import type { User, Config } from "./types" // erased at runtime
|
|
164
|
+
import { createUser } from "./services" // kept at runtime
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
For mixed imports:
|
|
168
|
+
```typescript
|
|
169
|
+
import { createUser, type User } from "./users"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Utility types — quick reference
|
|
175
|
+
|
|
176
|
+
| Need | Use |
|
|
177
|
+
|---|---|
|
|
178
|
+
| All properties readonly | `Readonly<T>` |
|
|
179
|
+
| All properties optional | `Partial<T>` |
|
|
180
|
+
| All properties required | `Required<T>` |
|
|
181
|
+
| Pick specific properties | `Pick<T, "a" \| "b">` |
|
|
182
|
+
| Omit specific properties | `Omit<T, "a" \| "b">` |
|
|
183
|
+
| Key-value map | `Record<K, V>` |
|
|
184
|
+
| Extract from union | `Extract<T, U>` |
|
|
185
|
+
| Exclude from union | `Exclude<T, U>` |
|
|
186
|
+
| Return type of function | `ReturnType<typeof fn>` |
|
|
187
|
+
| Parameters of function | `Parameters<typeof fn>` |
|
|
188
|
+
| Awaited type | `Awaited<Promise<T>>` → `T` |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Sources
|
|
193
|
+
|
|
194
|
+
- TypeScript Handbook: [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)
|
|
195
|
+
- TypeScript Handbook: [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html)
|
|
196
|
+
- Total TypeScript: [as const](https://www.totaltypescript.com/as-const)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# No-excuse rule checker for Go files.
|
|
3
|
+
# Mirrors the philosophy of python-programmer / typescript-programmer / rust-programmer scripts:
|
|
4
|
+
# only rules that can be enforced via pure text matching live here.
|
|
5
|
+
# Everything semantic is on golangci-lint + nilaway + go test -race.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
if [ $# -eq 0 ]; then
|
|
10
|
+
echo "Usage: $0 <file.go> [file.go ...]" >&2
|
|
11
|
+
exit 2
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
violations=0
|
|
15
|
+
report() {
|
|
16
|
+
local file="$1"
|
|
17
|
+
local line="$2"
|
|
18
|
+
local rule="$3"
|
|
19
|
+
local detail="$4"
|
|
20
|
+
echo "::error file=${file},line=${line}::[${rule}] ${detail}" >&2
|
|
21
|
+
violations=$((violations + 1))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
is_test_file() {
|
|
25
|
+
case "$1" in
|
|
26
|
+
*_test.go) return 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
return 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
is_generated_file() {
|
|
32
|
+
local file="$1"
|
|
33
|
+
case "$file" in
|
|
34
|
+
*.pb.go|*.connect.go|*.gen.go) return 0 ;;
|
|
35
|
+
*_string.go) return 0 ;;
|
|
36
|
+
esac
|
|
37
|
+
# First-line check for "Code generated ... DO NOT EDIT." (the official marker)
|
|
38
|
+
if [ -f "$file" ]; then
|
|
39
|
+
head -n 5 "$file" 2>/dev/null | grep -qE "^// Code generated .* DO NOT EDIT\.$" && return 0
|
|
40
|
+
fi
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for file in "$@"; do
|
|
45
|
+
[ -f "$file" ] || continue
|
|
46
|
+
case "$file" in
|
|
47
|
+
*.go) ;;
|
|
48
|
+
*) continue ;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
if is_generated_file "$file"; then
|
|
52
|
+
continue
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
in_test=0
|
|
56
|
+
if is_test_file "$file"; then
|
|
57
|
+
in_test=1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
line_no=0
|
|
61
|
+
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
|
|
62
|
+
line_no=$((line_no + 1))
|
|
63
|
+
line="$raw_line"
|
|
64
|
+
|
|
65
|
+
# Strip line comments before pattern checks
|
|
66
|
+
# (block comments are not handled — keep the rules robust to that limitation).
|
|
67
|
+
code_only="${line%%//*}"
|
|
68
|
+
|
|
69
|
+
# ── Exemption marker: // no-excuse-ok: <reason> ──────────────────
|
|
70
|
+
if [[ "$line" =~ //[[:space:]]*no-excuse-ok:[[:space:]]*.+ ]]; then
|
|
71
|
+
continue
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ── Rule: no `_ = err` (silent error swallow) ────────────────────
|
|
75
|
+
# The errcheck linter catches most of these but the `_ = err` form
|
|
76
|
+
# specifically slips through if used with named returns.
|
|
77
|
+
if [[ "$code_only" =~ ^[[:space:]]*_[[:space:]]*=[[:space:]]*err[[:space:]]*$ ]] ||
|
|
78
|
+
[[ "$code_only" =~ ^[[:space:]]*_[[:space:]]*=[[:space:]]*err[[:space:]]*[^a-zA-Z0-9_].*$ ]]; then
|
|
79
|
+
if [ "$in_test" -eq 0 ]; then
|
|
80
|
+
report "$file" "$line_no" "silent-err" "discarding err with '_ = err' — handle the error"
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Rule: no `panic(` in non-test, non-main code ─────────────────
|
|
85
|
+
# Allowed in main(), allowed in tests, allowed with explicit marker.
|
|
86
|
+
if [[ "$code_only" =~ [^a-zA-Z0-9_]panic\( ]] || [[ "$code_only" =~ ^[[:space:]]*panic\( ]]; then
|
|
87
|
+
if [ "$in_test" -eq 0 ]; then
|
|
88
|
+
# main package main.go is the one exception
|
|
89
|
+
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
|
90
|
+
if [[ "$pkg_line" != "package main" ]]; then
|
|
91
|
+
report "$file" "$line_no" "panic-in-lib" "panic outside main/test — return error instead"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── Rule: no `log.Fatal` / `log.Panic` in library code ───────────
|
|
97
|
+
if [[ "$code_only" =~ log\.(Fatal|Panic)(f|ln)?\( ]]; then
|
|
98
|
+
if [ "$in_test" -eq 0 ]; then
|
|
99
|
+
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
|
100
|
+
if [[ "$pkg_line" != "package main" ]]; then
|
|
101
|
+
report "$file" "$line_no" "log-fatal-in-lib" "log.Fatal/Panic outside main — return error"
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# ── Rule: no init() functions ─────────────────────────────────
|
|
107
|
+
# init() ruins testability and creates hidden global state.
|
|
108
|
+
# Exception: //go:build constraint files and generated code.
|
|
109
|
+
if [[ "$code_only" =~ ^func[[:space:]]+init\(\)[[:space:]]*\{ ]]; then
|
|
110
|
+
report "$file" "$line_no" "no-init-func" "init() ruins testability — use explicit constructor"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# ── Rule: no `time.Sleep` in non-test code ──────────────────────
|
|
114
|
+
if [[ "$code_only" =~ time\.Sleep\( ]]; then
|
|
115
|
+
if [ "$in_test" -eq 0 ]; then
|
|
116
|
+
report "$file" "$line_no" "time-sleep" "time.Sleep in production code — use ticker/timer with ctx"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# ── Rule: no `context.Background()` inside functions (only in main/init/test) ──
|
|
121
|
+
if [[ "$code_only" =~ context\.Background\(\) ]]; then
|
|
122
|
+
if [ "$in_test" -eq 0 ]; then
|
|
123
|
+
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
|
124
|
+
if [[ "$pkg_line" != "package main" ]]; then
|
|
125
|
+
report "$file" "$line_no" "ctx-background-in-lib" "context.Background() outside main — propagate ctx as parameter"
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# ── Rule: no `interface{}` (use `any`, the alias from Go 1.18+) ──
|
|
131
|
+
if [[ "$code_only" =~ interface\{\} ]]; then
|
|
132
|
+
report "$file" "$line_no" "old-interface-empty" "use 'any' instead of 'interface{}' (Go 1.18+)"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# ── Rule: no bare `fmt.Println` for logging (use slog) ───────────
|
|
136
|
+
# Acceptable in main.go (CLI output) and tests. Reject in libraries.
|
|
137
|
+
if [[ "$code_only" =~ fmt\.(Print|Println|Printf)\( ]]; then
|
|
138
|
+
if [ "$in_test" -eq 0 ]; then
|
|
139
|
+
pkg_line=$(head -n 5 "$file" 2>/dev/null | grep -m1 "^package ")
|
|
140
|
+
if [[ "$pkg_line" != "package main" ]]; then
|
|
141
|
+
report "$file" "$line_no" "fmt-print-in-lib" "fmt.Print* in library — use slog for structured logs"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# ── Rule: no `nolint` directive without reason ───────────────────
|
|
147
|
+
if [[ "$line" =~ //nolint(:|$| ) ]]; then
|
|
148
|
+
if ! [[ "$line" =~ //nolint:[a-zA-Z0-9_,-]+[[:space:]]+//[[:space:]]*[^[:space:]] ]]; then
|
|
149
|
+
report "$file" "$line_no" "nolint-no-reason" "//nolint requires a // reason after the linter list"
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# ── Rule: no TODO / FIXME without an issue link or owner ─────────
|
|
154
|
+
# Check the full line — TODOs live in comments, which $code_only has stripped.
|
|
155
|
+
if echo "$line" | grep -qE '(TODO|FIXME|XXX)([[:space:]]|:)'; then
|
|
156
|
+
if ! echo "$line" | grep -qE '(TODO|FIXME|XXX).*[(@[]'; then
|
|
157
|
+
report "$file" "$line_no" "todo-no-owner" "TODO/FIXME requires (#issue) or @owner attribution"
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
done < "$file"
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
if [ "$violations" -gt 0 ]; then
|
|
164
|
+
echo "" >&2
|
|
165
|
+
echo "go-programmer: $violations violation(s). Run also:" >&2
|
|
166
|
+
echo " gofumpt -l ." >&2
|
|
167
|
+
echo " golangci-lint run --timeout 5m ./..." >&2
|
|
168
|
+
echo " nilaway ./..." >&2
|
|
169
|
+
echo " go test -race -shuffle=on -count=1 ./..." >&2
|
|
170
|
+
exit 1
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
echo "go-programmer: no-excuse rules passed for $# file(s)."
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "typer",
|
|
6
|
+
# "rich",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
# ─── How to run ───
|
|
11
|
+
# 1. Install uv (if not installed):
|
|
12
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
13
|
+
# 2. Run:
|
|
14
|
+
# uv run new-project.py myservice
|
|
15
|
+
# uv run new-project.py myservice --module github.com/your-org/myservice
|
|
16
|
+
# ──────────────────
|
|
17
|
+
#
|
|
18
|
+
# Creates a new Go project with the canonical strict layout:
|
|
19
|
+
# - go.mod with go 1.23
|
|
20
|
+
# - .golangci.yml (v2, strict bundle)
|
|
21
|
+
# - Taskfile.yml (fmt + lint + test + build)
|
|
22
|
+
# - cmd/server/main.go entrypoint
|
|
23
|
+
# - internal/{cmd,config,api,domain,obs} skeletons
|
|
24
|
+
# - .github/workflows/ci.yml
|
|
25
|
+
#
|
|
26
|
+
# Templates live in ./templates/ — keep this script under 250 pure LOC.
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from string import Template
|
|
34
|
+
|
|
35
|
+
import typer
|
|
36
|
+
from rich.console import Console
|
|
37
|
+
|
|
38
|
+
console = Console(stderr=True)
|
|
39
|
+
|
|
40
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render(template_file: str, **subs: str) -> str:
|
|
44
|
+
"""Read a template file and apply $placeholder substitutions.
|
|
45
|
+
|
|
46
|
+
Uses string.Template ($name) so Go/YAML curly braces stay literal.
|
|
47
|
+
"""
|
|
48
|
+
raw = (TEMPLATES_DIR / template_file).read_text()
|
|
49
|
+
if not subs:
|
|
50
|
+
return raw
|
|
51
|
+
return Template(raw).substitute(**subs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# (template-file → relative output path; is_format = .format() is run)
|
|
55
|
+
FILES: list[tuple[str, str, bool]] = [
|
|
56
|
+
(".golangci.yml", ".golangci.yml", False),
|
|
57
|
+
("Taskfile.yml", "Taskfile.yml", False),
|
|
58
|
+
(".editorconfig", ".editorconfig", False),
|
|
59
|
+
("gitignore", ".gitignore", False),
|
|
60
|
+
("ci.yml", ".github/workflows/ci.yml", False),
|
|
61
|
+
("run.go", "internal/cmd/run.go", False),
|
|
62
|
+
("config.go", "internal/config/config.go", False),
|
|
63
|
+
("main.go.tmpl", "cmd/server/main.go", True),
|
|
64
|
+
("AGENTS.md.tmpl", "AGENTS.md", True),
|
|
65
|
+
("README.md.tmpl", "README.md", True),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _init_go_module(project_dir: Path, module: str) -> None:
|
|
70
|
+
try:
|
|
71
|
+
subprocess.run(
|
|
72
|
+
["go", "mod", "init", module],
|
|
73
|
+
cwd=project_dir,
|
|
74
|
+
check=True,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
)
|
|
77
|
+
console.print(f" [dim]ran[/] go mod init {module}")
|
|
78
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
79
|
+
console.print(f" [yellow]warn[/] go mod init failed ({e}); writing fallback go.mod")
|
|
80
|
+
(project_dir / "go.mod").write_text(f"module {module}\n\ngo 1.23\n")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _create_layout(project_dir: Path) -> None:
|
|
84
|
+
"""Create the canonical internal/ tree."""
|
|
85
|
+
subdirs = [
|
|
86
|
+
"cmd/server",
|
|
87
|
+
"internal/cmd",
|
|
88
|
+
"internal/config",
|
|
89
|
+
"internal/api",
|
|
90
|
+
"internal/domain",
|
|
91
|
+
"internal/obs",
|
|
92
|
+
".github/workflows",
|
|
93
|
+
]
|
|
94
|
+
for sd in subdirs:
|
|
95
|
+
(project_dir / sd).mkdir(parents=True)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _write_files(project_dir: Path, name: str, module: str, purpose: str) -> None:
|
|
99
|
+
"""Render every template into the project tree."""
|
|
100
|
+
for tmpl_name, out_rel, is_format in FILES:
|
|
101
|
+
subs = (
|
|
102
|
+
{"name": name, "module": module, "short_purpose": purpose}
|
|
103
|
+
if is_format
|
|
104
|
+
else {}
|
|
105
|
+
)
|
|
106
|
+
content = _render(tmpl_name, **subs)
|
|
107
|
+
out_path = project_dir / out_rel
|
|
108
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
out_path.write_text(content)
|
|
110
|
+
console.print(f" [dim]wrote[/] {out_rel}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(
|
|
114
|
+
name: str,
|
|
115
|
+
path: str = typer.Option(".", help="Parent dir"),
|
|
116
|
+
module: str = typer.Option("", help="Go module path; default: <name>"),
|
|
117
|
+
purpose: str = typer.Option("HTTP", help="Short purpose for AGENTS.md"),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Scaffold a new Go project with the strict toolchain."""
|
|
120
|
+
project_dir = Path(path) / name
|
|
121
|
+
if project_dir.exists():
|
|
122
|
+
console.print(f"[red]✗[/red] {project_dir} already exists")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
module_path = module or name
|
|
126
|
+
|
|
127
|
+
project_dir.mkdir(parents=True)
|
|
128
|
+
_create_layout(project_dir)
|
|
129
|
+
_init_go_module(project_dir, module_path)
|
|
130
|
+
_write_files(project_dir, name, module_path, purpose)
|
|
131
|
+
|
|
132
|
+
console.print(f"\n[bold green]Done![/] cd {project_dir}")
|
|
133
|
+
console.print(" go get github.com/caarlos0/env/v11")
|
|
134
|
+
console.print(" task # fmt + lint + test")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
typer.run(main)
|