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.
Files changed (47) hide show
  1. crucible/cli.py +109 -2
  2. crucible/enforcement/bundled/error-handling.yaml +84 -0
  3. crucible/enforcement/bundled/security.yaml +123 -0
  4. crucible/enforcement/bundled/smart-contract.yaml +110 -0
  5. crucible/enforcement/compliance.py +9 -5
  6. crucible/hooks/claudecode.py +388 -0
  7. crucible/hooks/precommit.py +117 -25
  8. crucible/knowledge/loader.py +186 -0
  9. crucible/knowledge/principles/API_DESIGN.md +176 -0
  10. crucible/knowledge/principles/COMMITS.md +127 -0
  11. crucible/knowledge/principles/DATABASE.md +138 -0
  12. crucible/knowledge/principles/DOCUMENTATION.md +201 -0
  13. crucible/knowledge/principles/ERROR_HANDLING.md +157 -0
  14. crucible/knowledge/principles/FP.md +162 -0
  15. crucible/knowledge/principles/GITIGNORE.md +218 -0
  16. crucible/knowledge/principles/OBSERVABILITY.md +147 -0
  17. crucible/knowledge/principles/PRECOMMIT.md +201 -0
  18. crucible/knowledge/principles/SECURITY.md +136 -0
  19. crucible/knowledge/principles/SMART_CONTRACT.md +153 -0
  20. crucible/knowledge/principles/SYSTEM_DESIGN.md +153 -0
  21. crucible/knowledge/principles/TESTING.md +129 -0
  22. crucible/knowledge/principles/TYPE_SAFETY.md +170 -0
  23. crucible/skills/accessibility-engineer/SKILL.md +71 -0
  24. crucible/skills/backend-engineer/SKILL.md +69 -0
  25. crucible/skills/customer-success/SKILL.md +69 -0
  26. crucible/skills/data-engineer/SKILL.md +70 -0
  27. crucible/skills/devops-engineer/SKILL.md +69 -0
  28. crucible/skills/fde-engineer/SKILL.md +69 -0
  29. crucible/skills/formal-verification/SKILL.md +86 -0
  30. crucible/skills/gas-optimizer/SKILL.md +89 -0
  31. crucible/skills/incident-responder/SKILL.md +91 -0
  32. crucible/skills/mev-researcher/SKILL.md +87 -0
  33. crucible/skills/mobile-engineer/SKILL.md +70 -0
  34. crucible/skills/performance-engineer/SKILL.md +68 -0
  35. crucible/skills/product-engineer/SKILL.md +68 -0
  36. crucible/skills/protocol-architect/SKILL.md +83 -0
  37. crucible/skills/security-engineer/SKILL.md +63 -0
  38. crucible/skills/tech-lead/SKILL.md +92 -0
  39. crucible/skills/uiux-engineer/SKILL.md +70 -0
  40. crucible/skills/web3-engineer/SKILL.md +79 -0
  41. crucible_mcp-1.0.1.dist-info/METADATA +198 -0
  42. crucible_mcp-1.0.1.dist-info/RECORD +66 -0
  43. crucible_mcp-0.5.0.dist-info/METADATA +0 -161
  44. crucible_mcp-0.5.0.dist-info/RECORD +0 -30
  45. {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/WHEEL +0 -0
  46. {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/entry_points.txt +0 -0
  47. {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/top_level.txt +0 -0
@@ -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.*