crucible-mcp 0.5.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
- crucible/cli.py +109 -2
- crucible/enforcement/bundled/error-handling.yaml +84 -0
- crucible/enforcement/bundled/security.yaml +123 -0
- crucible/enforcement/bundled/smart-contract.yaml +110 -0
- crucible/enforcement/compliance.py +9 -5
- crucible/hooks/claudecode.py +388 -0
- crucible/hooks/precommit.py +117 -25
- crucible/knowledge/loader.py +186 -0
- crucible/knowledge/principles/API_DESIGN.md +176 -0
- crucible/knowledge/principles/COMMITS.md +127 -0
- crucible/knowledge/principles/DATABASE.md +138 -0
- crucible/knowledge/principles/DOCUMENTATION.md +201 -0
- crucible/knowledge/principles/ERROR_HANDLING.md +157 -0
- crucible/knowledge/principles/FP.md +162 -0
- crucible/knowledge/principles/GITIGNORE.md +218 -0
- crucible/knowledge/principles/OBSERVABILITY.md +147 -0
- crucible/knowledge/principles/PRECOMMIT.md +201 -0
- crucible/knowledge/principles/SECURITY.md +136 -0
- crucible/knowledge/principles/SMART_CONTRACT.md +153 -0
- crucible/knowledge/principles/SYSTEM_DESIGN.md +153 -0
- crucible/knowledge/principles/TESTING.md +129 -0
- crucible/knowledge/principles/TYPE_SAFETY.md +170 -0
- crucible/skills/accessibility-engineer/SKILL.md +71 -0
- crucible/skills/backend-engineer/SKILL.md +69 -0
- crucible/skills/customer-success/SKILL.md +69 -0
- crucible/skills/data-engineer/SKILL.md +70 -0
- crucible/skills/devops-engineer/SKILL.md +69 -0
- crucible/skills/fde-engineer/SKILL.md +69 -0
- crucible/skills/formal-verification/SKILL.md +86 -0
- crucible/skills/gas-optimizer/SKILL.md +89 -0
- crucible/skills/incident-responder/SKILL.md +91 -0
- crucible/skills/mev-researcher/SKILL.md +87 -0
- crucible/skills/mobile-engineer/SKILL.md +70 -0
- crucible/skills/performance-engineer/SKILL.md +68 -0
- crucible/skills/product-engineer/SKILL.md +68 -0
- crucible/skills/protocol-architect/SKILL.md +83 -0
- crucible/skills/security-engineer/SKILL.md +63 -0
- crucible/skills/tech-lead/SKILL.md +92 -0
- crucible/skills/uiux-engineer/SKILL.md +70 -0
- crucible/skills/web3-engineer/SKILL.md +79 -0
- crucible_mcp-1.0.1.dist-info/METADATA +198 -0
- crucible_mcp-1.0.1.dist-info/RECORD +66 -0
- crucible_mcp-0.5.0.dist-info/METADATA +0 -161
- crucible_mcp-0.5.0.dist-info/RECORD +0 -30
- {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/WHEEL +0 -0
- {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/entry_points.txt +0 -0
- {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/top_level.txt +0 -0
crucible/knowledge/loader.py
CHANGED
|
@@ -4,12 +4,96 @@ Knowledge follows the same cascade as skills:
|
|
|
4
4
|
1. Project: .crucible/knowledge/
|
|
5
5
|
2. User: ~/.claude/crucible/knowledge/
|
|
6
6
|
3. Bundled: package knowledge/
|
|
7
|
+
|
|
8
|
+
Knowledge files support frontmatter for better Claude Code integration:
|
|
9
|
+
---
|
|
10
|
+
name: Security Principles
|
|
11
|
+
description: Core security principles for all code
|
|
12
|
+
triggers: [security, auth, crypto]
|
|
13
|
+
type: principle # principle | pattern | preference
|
|
14
|
+
assertions: security.yaml # linked assertion file
|
|
15
|
+
---
|
|
7
16
|
"""
|
|
8
17
|
|
|
18
|
+
from dataclasses import dataclass, field
|
|
9
19
|
from pathlib import Path
|
|
10
20
|
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
11
23
|
from crucible.errors import Result, err, ok
|
|
12
24
|
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class KnowledgeMetadata:
|
|
28
|
+
"""Metadata parsed from knowledge file frontmatter."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
description: str = ""
|
|
32
|
+
triggers: tuple[str, ...] = ()
|
|
33
|
+
type: str = "principle" # principle, pattern, preference
|
|
34
|
+
assertions: str | None = None # linked assertion file
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_frontmatter(cls, data: dict, filename: str) -> "KnowledgeMetadata":
|
|
38
|
+
"""Create metadata from parsed frontmatter dict."""
|
|
39
|
+
return cls(
|
|
40
|
+
name=data.get("name", filename.replace(".md", "").replace("_", " ").title()),
|
|
41
|
+
description=data.get("description", ""),
|
|
42
|
+
triggers=tuple(data.get("triggers", [])),
|
|
43
|
+
type=data.get("type", "principle"),
|
|
44
|
+
assertions=data.get("assertions"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class KnowledgeFile:
|
|
50
|
+
"""A knowledge file with metadata and content."""
|
|
51
|
+
|
|
52
|
+
filename: str
|
|
53
|
+
path: Path
|
|
54
|
+
source: str # project, user, bundled
|
|
55
|
+
metadata: KnowledgeMetadata
|
|
56
|
+
content: str = field(repr=False)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_frontmatter(content: str, filename: str) -> tuple[KnowledgeMetadata, str]:
|
|
60
|
+
"""Parse YAML frontmatter from knowledge file content.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
content: Full file content
|
|
64
|
+
filename: Filename for default metadata
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (metadata, content without frontmatter)
|
|
68
|
+
"""
|
|
69
|
+
if not content.startswith("---"):
|
|
70
|
+
# No frontmatter, return defaults
|
|
71
|
+
return KnowledgeMetadata(name=filename.replace(".md", "").replace("_", " ").title()), content
|
|
72
|
+
|
|
73
|
+
# Find closing ---
|
|
74
|
+
lines = content.split("\n")
|
|
75
|
+
end_idx = None
|
|
76
|
+
for i, line in enumerate(lines[1:], 1):
|
|
77
|
+
if line.strip() == "---":
|
|
78
|
+
end_idx = i
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
if end_idx is None:
|
|
82
|
+
# Malformed frontmatter, treat as no frontmatter
|
|
83
|
+
return KnowledgeMetadata(name=filename.replace(".md", "").replace("_", " ").title()), content
|
|
84
|
+
|
|
85
|
+
# Parse YAML
|
|
86
|
+
frontmatter_text = "\n".join(lines[1:end_idx])
|
|
87
|
+
remaining_content = "\n".join(lines[end_idx + 1 :]).lstrip()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
data = yaml.safe_load(frontmatter_text) or {}
|
|
91
|
+
metadata = KnowledgeMetadata.from_frontmatter(data, filename)
|
|
92
|
+
except yaml.YAMLError:
|
|
93
|
+
metadata = KnowledgeMetadata(name=filename.replace(".md", "").replace("_", " ").title())
|
|
94
|
+
|
|
95
|
+
return metadata, remaining_content
|
|
96
|
+
|
|
13
97
|
# Knowledge directories (same pattern as skills)
|
|
14
98
|
KNOWLEDGE_BUNDLED = Path(__file__).parent / "principles"
|
|
15
99
|
KNOWLEDGE_USER = Path.home() / ".claude" / "crucible" / "knowledge"
|
|
@@ -71,6 +155,77 @@ def get_all_knowledge_files() -> set[str]:
|
|
|
71
155
|
return files
|
|
72
156
|
|
|
73
157
|
|
|
158
|
+
def load_knowledge_with_metadata(filename: str) -> Result[KnowledgeFile, str]:
|
|
159
|
+
"""Load a knowledge file with parsed metadata.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
filename: Knowledge file name (e.g., "SECURITY.md")
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Result containing KnowledgeFile or error message
|
|
166
|
+
"""
|
|
167
|
+
path, source = resolve_knowledge_file(filename)
|
|
168
|
+
if path is None:
|
|
169
|
+
return err(f"Knowledge file '{filename}' not found")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
raw_content = path.read_text()
|
|
173
|
+
metadata, content = parse_frontmatter(raw_content, filename)
|
|
174
|
+
return ok(
|
|
175
|
+
KnowledgeFile(
|
|
176
|
+
filename=filename,
|
|
177
|
+
path=path,
|
|
178
|
+
source=source,
|
|
179
|
+
metadata=metadata,
|
|
180
|
+
content=content,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
except OSError as e:
|
|
184
|
+
return err(f"Failed to read '{filename}': {e}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_all_knowledge_metadata() -> list[tuple[str, KnowledgeMetadata]]:
|
|
188
|
+
"""Get metadata for all knowledge files (for discovery/indexing).
|
|
189
|
+
|
|
190
|
+
Returns list of (filename, metadata) tuples. This is cheap - only parses
|
|
191
|
+
frontmatter, doesn't load full content into memory.
|
|
192
|
+
"""
|
|
193
|
+
results: list[tuple[str, KnowledgeMetadata]] = []
|
|
194
|
+
|
|
195
|
+
for filename in sorted(get_all_knowledge_files()):
|
|
196
|
+
path, _ = resolve_knowledge_file(filename)
|
|
197
|
+
if path:
|
|
198
|
+
try:
|
|
199
|
+
# Read just enough to get frontmatter
|
|
200
|
+
content = path.read_text()
|
|
201
|
+
metadata, _ = parse_frontmatter(content, filename)
|
|
202
|
+
results.append((filename, metadata))
|
|
203
|
+
except OSError:
|
|
204
|
+
# Skip unreadable files
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
return results
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_knowledge_by_trigger(trigger: str) -> list[str]:
|
|
211
|
+
"""Get knowledge files that match a trigger.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
trigger: Trigger to match (e.g., "security", "auth")
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of matching filenames
|
|
218
|
+
"""
|
|
219
|
+
matches: list[str] = []
|
|
220
|
+
trigger_lower = trigger.lower()
|
|
221
|
+
|
|
222
|
+
for filename, metadata in get_all_knowledge_metadata():
|
|
223
|
+
if trigger_lower in [t.lower() for t in metadata.triggers]:
|
|
224
|
+
matches.append(filename)
|
|
225
|
+
|
|
226
|
+
return matches
|
|
227
|
+
|
|
228
|
+
|
|
74
229
|
def get_custom_knowledge_files() -> set[str]:
|
|
75
230
|
"""Get knowledge files from project and user directories only.
|
|
76
231
|
|
|
@@ -157,6 +312,37 @@ def load_principles(topic: str | None = None) -> Result[str, str]:
|
|
|
157
312
|
return ok("\n\n---\n\n".join(content_parts))
|
|
158
313
|
|
|
159
314
|
|
|
315
|
+
def get_linked_assertion_files(knowledge_files: set[str] | None = None) -> set[str]:
|
|
316
|
+
"""Get assertion files linked to knowledge files.
|
|
317
|
+
|
|
318
|
+
Looks at the `assertions` field in knowledge frontmatter to find
|
|
319
|
+
linked assertion files that should be loaded.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
knowledge_files: Specific knowledge files to check (if None, checks all)
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Set of assertion filenames to load
|
|
326
|
+
"""
|
|
327
|
+
if knowledge_files is None:
|
|
328
|
+
knowledge_files = get_all_knowledge_files()
|
|
329
|
+
|
|
330
|
+
assertion_files: set[str] = set()
|
|
331
|
+
|
|
332
|
+
for filename in knowledge_files:
|
|
333
|
+
path, _ = resolve_knowledge_file(filename)
|
|
334
|
+
if path:
|
|
335
|
+
try:
|
|
336
|
+
content = path.read_text()
|
|
337
|
+
metadata, _ = parse_frontmatter(content, filename)
|
|
338
|
+
if metadata.assertions:
|
|
339
|
+
assertion_files.add(metadata.assertions)
|
|
340
|
+
except OSError:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
return assertion_files
|
|
344
|
+
|
|
345
|
+
|
|
160
346
|
def get_persona_section(persona: str, content: str) -> str | None:
|
|
161
347
|
"""
|
|
162
348
|
Extract a specific persona section from the checklist content.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: API Design
|
|
3
|
+
description: REST conventions, versioning, pagination, error responses
|
|
4
|
+
triggers: [api, rest, http, endpoints]
|
|
5
|
+
type: pattern
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# API Design Principles
|
|
9
|
+
|
|
10
|
+
REST conventions, response shapes, and common patterns.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## REST Conventions
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Nouns, not verbs:
|
|
18
|
+
├── GET /tips ← List tips
|
|
19
|
+
├── POST /tips ← Create tip
|
|
20
|
+
├── GET /tips/:id ← Get single tip
|
|
21
|
+
├── PUT /tips/:id ← Update tip (full)
|
|
22
|
+
├── PATCH /tips/:id ← Update tip (partial)
|
|
23
|
+
├── DELETE /tips/:id ← Delete tip
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Consistent Response Shapes
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// Success
|
|
32
|
+
{ data: T }
|
|
33
|
+
|
|
34
|
+
// Error
|
|
35
|
+
{ error: { code: string, message: string } }
|
|
36
|
+
|
|
37
|
+
// List (with pagination)
|
|
38
|
+
{ data: T[], meta: { total: number, page: number, pageSize: number } }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## HTTP Status Codes
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
200 OK ← Success (GET, PUT, PATCH)
|
|
47
|
+
201 Created ← Resource created (POST)
|
|
48
|
+
204 No Content ← Success, no body (DELETE)
|
|
49
|
+
400 Bad Request ← Client error (validation)
|
|
50
|
+
401 Unauthorized ← Not authenticated
|
|
51
|
+
403 Forbidden ← Authenticated but not allowed
|
|
52
|
+
404 Not Found ← Resource doesn't exist
|
|
53
|
+
409 Conflict ← State conflict
|
|
54
|
+
422 Unprocessable← Validation failed
|
|
55
|
+
429 Too Many ← Rate limited
|
|
56
|
+
500 Server Error ← Server fault
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Rate Limiting
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Headers to return:
|
|
65
|
+
├── X-RateLimit-Limit: 100
|
|
66
|
+
├── X-RateLimit-Remaining: 95
|
|
67
|
+
├── X-RateLimit-Reset: 1609459200
|
|
68
|
+
└── Retry-After: 60 (on 429)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Pagination
|
|
74
|
+
|
|
75
|
+
Use bounded lists:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Request
|
|
79
|
+
GET /tips?page=2&pageSize=20
|
|
80
|
+
|
|
81
|
+
// Response
|
|
82
|
+
{
|
|
83
|
+
data: [...],
|
|
84
|
+
meta: {
|
|
85
|
+
total: 150,
|
|
86
|
+
page: 2,
|
|
87
|
+
pageSize: 20,
|
|
88
|
+
totalPages: 8
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Versioning
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
For internal APIs (tRPC, same team):
|
|
99
|
+
├── No explicit versioning
|
|
100
|
+
├── Type system catches breakage
|
|
101
|
+
└── Change and deploy together
|
|
102
|
+
|
|
103
|
+
For public APIs (external consumers):
|
|
104
|
+
├── Version in URL: /v1/tips
|
|
105
|
+
├── Support N-1 version minimum
|
|
106
|
+
├── Deprecation warnings before removal
|
|
107
|
+
└── Breaking change = major version bump
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Idempotency
|
|
113
|
+
|
|
114
|
+
For operations that can be retried:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Not idempotent
|
|
118
|
+
POST /payments
|
|
119
|
+
{ amount: 100 }
|
|
120
|
+
// Called twice = charged twice
|
|
121
|
+
|
|
122
|
+
// Idempotent
|
|
123
|
+
POST /payments
|
|
124
|
+
{
|
|
125
|
+
amount: 100,
|
|
126
|
+
idempotencyKey: "user-123-order-456"
|
|
127
|
+
}
|
|
128
|
+
// Called twice = charged once
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Error Responses
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Vague
|
|
137
|
+
{ error: "Something went wrong" }
|
|
138
|
+
|
|
139
|
+
// Actionable
|
|
140
|
+
{
|
|
141
|
+
error: {
|
|
142
|
+
code: "VALIDATION_ERROR",
|
|
143
|
+
message: "Invalid request",
|
|
144
|
+
details: [
|
|
145
|
+
{ field: "email", message: "Must be a valid email" },
|
|
146
|
+
{ field: "amount", message: "Must be positive" }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## tRPC for Internal APIs
|
|
155
|
+
|
|
156
|
+
Type-safe end-to-end:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
export const tipRouter = router({
|
|
160
|
+
create: protectedProcedure
|
|
161
|
+
.input(CreateTipSchema)
|
|
162
|
+
.mutation(({ input, ctx }) => {
|
|
163
|
+
return createTip(ctx.db, input);
|
|
164
|
+
}),
|
|
165
|
+
|
|
166
|
+
list: protectedProcedure
|
|
167
|
+
.input(z.object({ pageId: z.string() }))
|
|
168
|
+
.query(({ input }) => {
|
|
169
|
+
return getTipsByPage(input.pageId);
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
*Template. Adapt to your needs.*
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Commit Messages
|
|
3
|
+
description: Conventional commits format and best practices
|
|
4
|
+
triggers: [git, commits, version-control]
|
|
5
|
+
type: pattern
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Commit Message Principles
|
|
9
|
+
|
|
10
|
+
Semantic commits for readable history.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Format
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
(type): description
|
|
18
|
+
|
|
19
|
+
Body (optional)
|
|
20
|
+
|
|
21
|
+
Co-Authored-By: Name <email>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Types
|
|
27
|
+
|
|
28
|
+
| Type | Use When |
|
|
29
|
+
|------|----------|
|
|
30
|
+
| `feat` | New feature |
|
|
31
|
+
| `fix` | Bug fix |
|
|
32
|
+
| `docs` | Documentation only |
|
|
33
|
+
| `refactor` | Code restructure (no behavior change) |
|
|
34
|
+
| `test` | Adding or updating tests |
|
|
35
|
+
| `chore` | Build, deps, config changes |
|
|
36
|
+
| `perf` | Performance improvement |
|
|
37
|
+
| `style` | Formatting, whitespace |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Good Examples
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Feature
|
|
45
|
+
(feat): add pagination to tips endpoint
|
|
46
|
+
|
|
47
|
+
# Bug fix
|
|
48
|
+
(fix): handle empty response from Stripe webhook
|
|
49
|
+
|
|
50
|
+
# Refactor
|
|
51
|
+
(refactor): extract fee calculation to pure function
|
|
52
|
+
|
|
53
|
+
# Docs
|
|
54
|
+
(docs): add API authentication examples
|
|
55
|
+
|
|
56
|
+
# Test
|
|
57
|
+
(test): add integration tests for payment flow
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Bad Examples
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Too vague
|
|
66
|
+
fix stuff
|
|
67
|
+
update code
|
|
68
|
+
changes
|
|
69
|
+
|
|
70
|
+
# Too long
|
|
71
|
+
(feat): add a new endpoint for handling user authentication with OAuth2 and also add rate limiting and logging
|
|
72
|
+
|
|
73
|
+
# Wrong type
|
|
74
|
+
(feat): fix typo in README # should be (docs) or (fix)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Commit Body
|
|
80
|
+
|
|
81
|
+
For complex changes, add context:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
(fix): prevent duplicate charges on retry
|
|
85
|
+
|
|
86
|
+
Stripe webhook was being processed multiple times when
|
|
87
|
+
the initial response timed out. Added idempotency check
|
|
88
|
+
using the event ID.
|
|
89
|
+
|
|
90
|
+
Closes #123
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Atomic Commits
|
|
96
|
+
|
|
97
|
+
One logical change per commit:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
# Good: atomic commits
|
|
101
|
+
(feat): add User model
|
|
102
|
+
(feat): add user registration endpoint
|
|
103
|
+
(test): add user registration tests
|
|
104
|
+
|
|
105
|
+
# Bad: everything at once
|
|
106
|
+
(feat): add user feature
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Squashing
|
|
112
|
+
|
|
113
|
+
For feature branches with messy history:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Interactive rebase before merge
|
|
117
|
+
git rebase -i main
|
|
118
|
+
|
|
119
|
+
# Squash WIP commits into meaningful ones
|
|
120
|
+
pick abc1234 (feat): add payment processing
|
|
121
|
+
squash def5678 WIP
|
|
122
|
+
squash ghi9012 fix tests
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
*Template. Adapt format to your team's conventions.*
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Database Patterns
|
|
3
|
+
description: Migrations, indexing, query patterns, connection management
|
|
4
|
+
triggers: [database, sql, postgres, mysql, migrations]
|
|
5
|
+
type: pattern
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Database Principles
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Core Rules
|
|
13
|
+
|
|
14
|
+
| Rule | Reason |
|
|
15
|
+
|------|--------|
|
|
16
|
+
| UUIDs for primary keys | No enumeration attacks, no auto-increment collisions |
|
|
17
|
+
| Timestamps on everything | `created_at`, `updated_at` for debugging |
|
|
18
|
+
| Soft delete when data matters | `deleted_at` instead of hard delete |
|
|
19
|
+
| Amounts in cents | No floating point money (500 = $5.00) |
|
|
20
|
+
| Encrypt sensitive data at rest | Defense in depth |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Index Strategy
|
|
25
|
+
|
|
26
|
+
```sql
|
|
27
|
+
-- Every foreign key
|
|
28
|
+
CREATE INDEX idx_tips_page_id ON tips(page_id);
|
|
29
|
+
|
|
30
|
+
-- Everything you WHERE on
|
|
31
|
+
CREATE INDEX idx_profiles_username ON user_profiles(username);
|
|
32
|
+
|
|
33
|
+
-- Everything you ORDER BY
|
|
34
|
+
CREATE INDEX idx_tips_created_at ON tips(created_at);
|
|
35
|
+
|
|
36
|
+
-- Composite for common query patterns
|
|
37
|
+
CREATE INDEX idx_tips_page_status ON tips(page_id, status);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Optimization Order
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
1. Indexes (90% of performance issues)
|
|
46
|
+
2. Query design (N+1, unbounded selects)
|
|
47
|
+
3. Connection pooling
|
|
48
|
+
4. Read replicas
|
|
49
|
+
5. Caching layer
|
|
50
|
+
6. Sharding (rarely needed)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## N+1 Queries
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// N+1 (1 query for pages + N queries for tips)
|
|
59
|
+
const pages = await db.page.findMany();
|
|
60
|
+
for (const page of pages) {
|
|
61
|
+
const tips = await db.tip.findMany({ where: { pageId: page.id } });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Include (1 query with join)
|
|
65
|
+
const pages = await db.page.findMany({
|
|
66
|
+
include: { tips: true }
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Naming Conventions
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Tables: plural, snake_case → user_profiles, orders
|
|
76
|
+
Columns: snake_case → created_at, user_id
|
|
77
|
+
Indexes: descriptive → idx_tips_page_id
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Migrations
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
├── One migration per change
|
|
86
|
+
├── Migrations should be reversible
|
|
87
|
+
├── Test migrations on production data (copy)
|
|
88
|
+
├── Do not edit deployed migrations
|
|
89
|
+
└── Separate schema changes from data migrations
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Transactions
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Atomic operations
|
|
98
|
+
await db.$transaction(async (tx) => {
|
|
99
|
+
await tx.account.update({
|
|
100
|
+
where: { id: fromAccount },
|
|
101
|
+
data: { balance: { decrement: amount } }
|
|
102
|
+
});
|
|
103
|
+
await tx.account.update({
|
|
104
|
+
where: { id: toAccount },
|
|
105
|
+
data: { balance: { increment: amount } }
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// Both succeed or both fail
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Connection Pooling
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
├── Use connection pools (Prisma does this)
|
|
117
|
+
├── Size pool based on: (cores * 2) + disk spindles
|
|
118
|
+
├── Set connection timeout
|
|
119
|
+
├── Monitor pool exhaustion
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Query Safety
|
|
125
|
+
|
|
126
|
+
Use parameterized queries:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// SQL injection risk
|
|
130
|
+
const query = `SELECT * FROM users WHERE id = '${userId}'`;
|
|
131
|
+
|
|
132
|
+
// Parameterized (ORMs do this)
|
|
133
|
+
const user = await db.user.findUnique({ where: { id: userId } });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
*Template. Adapt to your needs.*
|