crucible-mcp 0.4.0__py3-none-any.whl → 1.0.0__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 (52) hide show
  1. crucible/cli.py +532 -12
  2. crucible/enforcement/budget.py +179 -0
  3. crucible/enforcement/bundled/error-handling.yaml +84 -0
  4. crucible/enforcement/bundled/security.yaml +123 -0
  5. crucible/enforcement/bundled/smart-contract.yaml +110 -0
  6. crucible/enforcement/compliance.py +486 -0
  7. crucible/enforcement/models.py +71 -1
  8. crucible/hooks/claudecode.py +388 -0
  9. crucible/hooks/precommit.py +117 -25
  10. crucible/knowledge/loader.py +186 -0
  11. crucible/knowledge/principles/API_DESIGN.md +176 -0
  12. crucible/knowledge/principles/COMMITS.md +127 -0
  13. crucible/knowledge/principles/DATABASE.md +138 -0
  14. crucible/knowledge/principles/DOCUMENTATION.md +201 -0
  15. crucible/knowledge/principles/ERROR_HANDLING.md +157 -0
  16. crucible/knowledge/principles/FP.md +162 -0
  17. crucible/knowledge/principles/GITIGNORE.md +218 -0
  18. crucible/knowledge/principles/OBSERVABILITY.md +147 -0
  19. crucible/knowledge/principles/PRECOMMIT.md +201 -0
  20. crucible/knowledge/principles/SECURITY.md +136 -0
  21. crucible/knowledge/principles/SMART_CONTRACT.md +153 -0
  22. crucible/knowledge/principles/SYSTEM_DESIGN.md +153 -0
  23. crucible/knowledge/principles/TESTING.md +129 -0
  24. crucible/knowledge/principles/TYPE_SAFETY.md +170 -0
  25. crucible/review/core.py +78 -7
  26. crucible/server.py +81 -14
  27. crucible/skills/accessibility-engineer/SKILL.md +71 -0
  28. crucible/skills/backend-engineer/SKILL.md +69 -0
  29. crucible/skills/customer-success/SKILL.md +69 -0
  30. crucible/skills/data-engineer/SKILL.md +70 -0
  31. crucible/skills/devops-engineer/SKILL.md +69 -0
  32. crucible/skills/fde-engineer/SKILL.md +69 -0
  33. crucible/skills/formal-verification/SKILL.md +86 -0
  34. crucible/skills/gas-optimizer/SKILL.md +89 -0
  35. crucible/skills/incident-responder/SKILL.md +91 -0
  36. crucible/skills/mev-researcher/SKILL.md +87 -0
  37. crucible/skills/mobile-engineer/SKILL.md +70 -0
  38. crucible/skills/performance-engineer/SKILL.md +68 -0
  39. crucible/skills/product-engineer/SKILL.md +68 -0
  40. crucible/skills/protocol-architect/SKILL.md +83 -0
  41. crucible/skills/security-engineer/SKILL.md +63 -0
  42. crucible/skills/tech-lead/SKILL.md +92 -0
  43. crucible/skills/uiux-engineer/SKILL.md +70 -0
  44. crucible/skills/web3-engineer/SKILL.md +79 -0
  45. crucible/tools/git.py +17 -4
  46. crucible_mcp-1.0.0.dist-info/METADATA +198 -0
  47. crucible_mcp-1.0.0.dist-info/RECORD +66 -0
  48. crucible_mcp-0.4.0.dist-info/METADATA +0 -160
  49. crucible_mcp-0.4.0.dist-info/RECORD +0 -28
  50. {crucible_mcp-0.4.0.dist-info → crucible_mcp-1.0.0.dist-info}/WHEEL +0 -0
  51. {crucible_mcp-0.4.0.dist-info → crucible_mcp-1.0.0.dist-info}/entry_points.txt +0 -0
  52. {crucible_mcp-0.4.0.dist-info → crucible_mcp-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: System Design
3
+ description: Architecture patterns, scalability, distributed systems
4
+ triggers: [architecture, system-design, scalability, distributed]
5
+ type: principle
6
+ ---
7
+
8
+ # System Design Principles
9
+
10
+ ---
11
+
12
+ ## Monolith to Microservices
13
+
14
+ ```
15
+ Progression:
16
+ ├── Monolith: One deployable, one database
17
+ ├── Modular monolith: Clear boundaries, could split
18
+ ├── Microservices: Multiple deployables, distributed
19
+
20
+ Indicators to split:
21
+ ├── Teams blocked by shared codebase
22
+ ├── Components have different scaling requirements
23
+ ├── Regulatory isolation required
24
+ └── Deployment coupling causes issues
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Reference Architecture
30
+
31
+ ```
32
+ ┌─────────────────────────────────────────────────┐
33
+ │ Client │
34
+ │ (Web / Mobile / CLI) │
35
+ └─────────────────────┬───────────────────────────┘
36
+
37
+
38
+ ┌─────────────────────────────────────────────────┐
39
+ │ API Layer │
40
+ │ (Next.js API / tRPC / REST) │
41
+ └─────────────────────┬───────────────────────────┘
42
+
43
+ ┌─────────────┼─────────────┐
44
+ ▼ ▼ ▼
45
+ ┌─────────────┐ ┌───────────┐ ┌───────────────┐
46
+ │ Database │ │ Cache │ │ File Storage │
47
+ │ (Postgres) │ │ (Redis) │ │ (S3) │
48
+ └─────────────┘ └───────────┘ └───────────────┘
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Stateless Servers
54
+
55
+ App servers should hold no state.
56
+
57
+ ```
58
+ Stateful patterns (avoid):
59
+ ├── Session stored in server memory
60
+ ├── Uploaded files on local disk
61
+ ├── In-memory cache per instance
62
+ └── Breaks on horizontal scaling
63
+
64
+ Stateless patterns:
65
+ ├── Session in database or JWT
66
+ ├── Files in S3/object storage
67
+ ├── Cache in Redis (shared)
68
+ └── Any server can handle any request
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Scaling
74
+
75
+ ```
76
+ Vertical (scale up):
77
+ ├── Larger server instance
78
+ ├── No code changes required
79
+ └── Simpler to operate
80
+
81
+ Horizontal (scale out):
82
+ ├── Multiple server instances
83
+ ├── Requires stateless design
84
+ └── More complex to operate
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Async Processing
90
+
91
+ ```
92
+ Queue candidates:
93
+ ├── Emails / notifications
94
+ ├── Image processing
95
+ ├── Report generation
96
+ ├── Third-party API calls
97
+ └── Any operation that can fail and retry
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Caching
103
+
104
+ ```
105
+ Good candidates:
106
+ ├── Read-heavy, write-light data
107
+ ├── Expensive to compute
108
+ ├── Infrequent changes
109
+ ├── Stale data acceptable
110
+
111
+ Poor candidates:
112
+ ├── Frequently changing data
113
+ ├── Strong consistency required
114
+ ├── Already fast enough
115
+ ```
116
+
117
+ Invalidation strategies: time-based expiry, event-based invalidation, cache-aside pattern.
118
+
119
+ ---
120
+
121
+ ## Idempotency
122
+
123
+ Operations that can be retried should produce the same result.
124
+
125
+ ```typescript
126
+ // Non-idempotent: double-charge on retry
127
+ stripe.charge(userId, amount);
128
+
129
+ // Idempotent: same result on retry
130
+ stripe.charge(userId, amount, { idempotencyKey });
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Failure Handling
136
+
137
+ ```
138
+ Design assumptions:
139
+ ├── External APIs will fail
140
+ ├── Database latency will spike
141
+ ├── Code has bugs
142
+
143
+ Strategies:
144
+ ├── Timeouts on all external calls
145
+ ├── Retries with exponential backoff
146
+ ├── Circuit breakers
147
+ ├── Graceful degradation
148
+ └── Health checks
149
+ ```
150
+
151
+ ---
152
+
153
+ *Template. Adapt to your needs.*
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: Testing Principles
3
+ description: Test pyramid, unit/integration/e2e, mocking patterns
4
+ triggers: [testing, tests, unit-tests, integration, e2e]
5
+ type: principle
6
+ ---
7
+
8
+ # Testing Principles
9
+
10
+ What to test, how to structure tests, and patterns that work.
11
+
12
+ ---
13
+
14
+ ## The Pyramid
15
+
16
+ ```
17
+ /\ Few E2E (critical paths only)
18
+ / \
19
+ /────\ Some integration (API, DB)
20
+ / \
21
+ /────────\ Many unit tests (fast, pure)
22
+ ```
23
+
24
+ ---
25
+
26
+ ## What to Test
27
+
28
+ ```
29
+ Test:
30
+ ├── Business logic (core package)
31
+ ├── Edge cases
32
+ ├── Regression bugs (write test, then fix)
33
+ ├── Complex calculations
34
+ └── State machines
35
+
36
+ Don't test:
37
+ ├── Framework behavior
38
+ ├── Third-party libraries
39
+ ├── Implementation details
40
+ ├── Things that change constantly (UI specifics)
41
+ └── Obvious code (getters, setters)
42
+ ```
43
+
44
+ ---
45
+
46
+ ## FP Makes Testing Easy
47
+
48
+ Pure functions = same input, same output. No mocking needed.
49
+
50
+ ```typescript
51
+ const calculateFee = (amount: Cents): Cents => {
52
+ return Math.round(amount * 0.01) as Cents;
53
+ };
54
+
55
+ test('calculates 1% fee', () => {
56
+ expect(calculateFee(1000 as Cents)).toBe(10);
57
+ expect(calculateFee(999 as Cents)).toBe(10);
58
+ expect(calculateFee(100 as Cents)).toBe(1);
59
+ });
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Test Naming
65
+
66
+ ```typescript
67
+ // Vague
68
+ test('it works', () => { ... });
69
+
70
+ // Descriptive
71
+ test('calculateFee rounds up for fractional cents', () => { ... });
72
+ test('returns NOT_FOUND when user does not exist', () => { ... });
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Integration Tests
78
+
79
+ Test real behavior, not mocks.
80
+
81
+ ```typescript
82
+ // Mocking everything
83
+ jest.mock('../database');
84
+ jest.mock('../stripe');
85
+ // What are you even testing?
86
+
87
+ // Test real behavior with test database
88
+ beforeEach(() => db.reset());
89
+ test('creates user in database', async () => {
90
+ await createUser({ email: 'test@example.com' });
91
+ const user = await db.user.findFirst({ where: { email: 'test@example.com' } });
92
+ expect(user).not.toBeNull();
93
+ });
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Property-Based Testing
99
+
100
+ For invariants that should always hold:
101
+
102
+ ```typescript
103
+ import fc from 'fast-check';
104
+
105
+ test('fee is always positive', () => {
106
+ fc.assert(
107
+ fc.property(fc.integer({ min: 1 }), (amount) => {
108
+ return calculateFee(amount as Cents) >= 0;
109
+ })
110
+ );
111
+ });
112
+ ```
113
+
114
+ ---
115
+
116
+ ## The Rule
117
+
118
+ ```
119
+ If you find a bug:
120
+ 1. Write a test that reproduces it
121
+ 2. Watch it fail
122
+ 3. Fix the bug
123
+ 4. Watch it pass
124
+ 5. Never have that bug again
125
+ ```
126
+
127
+ ---
128
+
129
+ *Template. Adapt to your needs.*
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: Type Safety
3
+ description: Type annotations, generics, strict mode, type guards
4
+ triggers: [types, typescript, typing, mypy, type-safety]
5
+ type: principle
6
+ ---
7
+
8
+ # Type Safety Principles
9
+
10
+ Patterns for making invalid states unrepresentable.
11
+
12
+ ---
13
+
14
+ ## No `any`
15
+
16
+ ```typescript
17
+ // any (defeats the purpose of TypeScript)
18
+ const process = (data: any) => { ... }
19
+
20
+ // unknown + narrowing
21
+ const process = (data: unknown) => {
22
+ if (typeof data === 'string') {
23
+ // TypeScript knows it's a string here
24
+ }
25
+ }
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Branded Types
31
+
32
+ Prevent mixing up similar primitives:
33
+
34
+ ```typescript
35
+ // Easy to mix up
36
+ const createTip = (pageId: string, amount: number) => { ... }
37
+ // createTip(tipId, pageId) compiles but is wrong!
38
+
39
+ // Branded types
40
+ type PageId = string & { readonly _brand: 'PageId' };
41
+ type TipId = string & { readonly _brand: 'TipId' };
42
+ type Cents = number & { readonly _brand: 'Cents' };
43
+
44
+ const createTip = (pageId: PageId, amount: Cents) => { ... }
45
+ // createTip(tipId, pageId) → Type error!
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Discriminated Unions
51
+
52
+ Make invalid states unrepresentable:
53
+
54
+ ```typescript
55
+ // Boolean flags (invalid states possible)
56
+ type Response = {
57
+ loading: boolean;
58
+ error: Error | null;
59
+ data: Data | null;
60
+ }
61
+ // What if loading=true AND error is set?
62
+
63
+ // Discriminated union (only valid states)
64
+ type Response =
65
+ | { status: 'loading' }
66
+ | { status: 'error'; error: Error }
67
+ | { status: 'success'; data: Data };
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Zod at Boundaries
73
+
74
+ Validate external data, then trust the types:
75
+
76
+ ```typescript
77
+ import { z } from 'zod';
78
+
79
+ const TipSchema = z.object({
80
+ pageId: z.string().uuid(),
81
+ amountCents: z.number().int().positive().max(100000),
82
+ message: z.string().max(500).optional(),
83
+ });
84
+
85
+ type Tip = z.infer<typeof TipSchema>;
86
+
87
+ // Validate at API boundary
88
+ const handler = (req: Request) => {
89
+ const result = TipSchema.safeParse(req.body);
90
+ if (!result.success) {
91
+ return { error: result.error };
92
+ }
93
+ // result.data is fully typed and validated
94
+ return createTip(result.data);
95
+ };
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Exhaustiveness Checking
101
+
102
+ TypeScript tells you when you miss a case:
103
+
104
+ ```typescript
105
+ type Status = 'pending' | 'active' | 'cancelled';
106
+
107
+ const getLabel = (status: Status): string => {
108
+ switch (status) {
109
+ case 'pending': return 'Pending';
110
+ case 'active': return 'Active';
111
+ // TypeScript error: 'cancelled' not handled
112
+ }
113
+ };
114
+
115
+ // Force exhaustiveness with never
116
+ const assertNever = (x: never): never => {
117
+ throw new Error(`Unexpected: ${x}`);
118
+ };
119
+
120
+ const getLabel = (status: Status): string => {
121
+ switch (status) {
122
+ case 'pending': return 'Pending';
123
+ case 'active': return 'Active';
124
+ case 'cancelled': return 'Cancelled';
125
+ default: return assertNever(status);
126
+ }
127
+ };
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Strict Mode
133
+
134
+ Always enable:
135
+
136
+ ```json
137
+ // tsconfig.json
138
+ {
139
+ "compilerOptions": {
140
+ "strict": true,
141
+ "noUncheckedIndexedAccess": true
142
+ }
143
+ }
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Optional vs Required
149
+
150
+ Be explicit:
151
+
152
+ ```typescript
153
+ // Ambiguous
154
+ interface User {
155
+ name: string;
156
+ email: string;
157
+ phone: string; // Required? Or just always present?
158
+ }
159
+
160
+ // Explicit
161
+ interface User {
162
+ name: string;
163
+ email: string;
164
+ phone?: string; // Optional
165
+ }
166
+ ```
167
+
168
+ ---
169
+
170
+ *Template. Adapt to your needs.*
crucible/review/core.py CHANGED
@@ -1,8 +1,11 @@
1
1
  """Core review functionality shared between CLI and MCP server."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from collections import Counter
4
6
  from pathlib import Path
5
7
 
8
+ from crucible.enforcement.models import BudgetState, ComplianceConfig
6
9
  from crucible.models import Domain, Severity, ToolFinding
7
10
  from crucible.tools.delegation import (
8
11
  delegate_bandit,
@@ -313,31 +316,42 @@ def run_enforcement(
313
316
  content: str | None = None,
314
317
  changed_files: list[str] | None = None,
315
318
  repo_root: str | None = None,
316
- ) -> tuple[list, list[str], int, int]:
317
- """Run pattern assertions.
319
+ compliance_config: ComplianceConfig | None = None,
320
+ ) -> tuple[list, list[str], int, int, BudgetState | None]:
321
+ """Run pattern and LLM assertions.
318
322
 
319
323
  Args:
320
324
  path: File or directory path
321
325
  content: File content (for single file mode)
322
326
  changed_files: List of changed files (for git mode)
323
327
  repo_root: Repository root path (for git mode)
328
+ compliance_config: Configuration for LLM compliance checking (optional)
324
329
 
325
330
  Returns:
326
- (enforcement_findings, errors, assertions_checked, assertions_skipped)
331
+ (enforcement_findings, errors, assertions_checked, assertions_skipped, budget_state)
327
332
  """
328
333
  import os
329
334
 
330
335
  from crucible.enforcement.assertions import load_assertions
336
+ from crucible.enforcement.compliance import run_llm_assertions, run_llm_assertions_batch
331
337
  from crucible.enforcement.models import EnforcementFinding
332
338
  from crucible.enforcement.patterns import run_pattern_assertions
333
339
 
334
340
  assertions, errors = load_assertions()
335
341
  if not assertions:
336
- return [], errors, 0, 0
342
+ return [], errors, 0, 0, None
337
343
 
338
344
  findings: list[EnforcementFinding] = []
339
345
  checked = 0
340
346
  skipped = 0
347
+ budget_state: BudgetState | None = None
348
+
349
+ # Default compliance config if not provided
350
+ if compliance_config is None:
351
+ compliance_config = ComplianceConfig()
352
+
353
+ # Collect files for batch LLM processing
354
+ files_for_llm: list[tuple[str, str]] = []
341
355
 
342
356
  if changed_files and repo_root:
343
357
  # Git mode: check each changed file
@@ -346,26 +360,67 @@ def run_enforcement(
346
360
  try:
347
361
  with open(full_path) as f:
348
362
  file_content = f.read()
363
+
364
+ # Run pattern assertions
349
365
  f_findings, c, s = run_pattern_assertions(file_path, file_content, assertions)
350
366
  findings.extend(f_findings)
351
367
  checked = max(checked, c)
352
368
  skipped = max(skipped, s)
369
+
370
+ # Collect for LLM processing
371
+ if compliance_config.enabled:
372
+ files_for_llm.append((file_path, file_content))
353
373
  except OSError:
354
374
  pass # File may have been deleted
375
+
376
+ # Run LLM assertions in batch
377
+ if files_for_llm and compliance_config.enabled:
378
+ llm_findings, budget_state, llm_errors = run_llm_assertions_batch(
379
+ files_for_llm, assertions, compliance_config
380
+ )
381
+ findings.extend(llm_findings)
382
+ errors.extend(llm_errors)
383
+ if budget_state:
384
+ skipped += budget_state.assertions_skipped
385
+
355
386
  elif content is not None:
356
387
  # Single file with provided content
357
388
  f_findings, checked, skipped = run_pattern_assertions(path, content, assertions)
358
389
  findings.extend(f_findings)
390
+
391
+ # Run LLM assertions
392
+ if compliance_config.enabled:
393
+ llm_findings, budget_state, llm_errors = run_llm_assertions(
394
+ path, content, assertions, compliance_config
395
+ )
396
+ findings.extend(llm_findings)
397
+ errors.extend(llm_errors)
398
+ if budget_state:
399
+ skipped += budget_state.assertions_skipped
400
+
359
401
  elif os.path.isfile(path):
360
402
  # Single file
361
403
  try:
362
404
  with open(path) as f:
363
405
  file_content = f.read()
364
- findings, checked, skipped = run_pattern_assertions(path, file_content, assertions)
406
+
407
+ p_findings, checked, skipped = run_pattern_assertions(path, file_content, assertions)
408
+ findings.extend(p_findings)
409
+
410
+ # Run LLM assertions
411
+ if compliance_config.enabled:
412
+ llm_findings, budget_state, llm_errors = run_llm_assertions(
413
+ path, file_content, assertions, compliance_config
414
+ )
415
+ findings.extend(llm_findings)
416
+ errors.extend(llm_errors)
417
+ if budget_state:
418
+ skipped += budget_state.assertions_skipped
365
419
  except OSError as e:
366
420
  errors.append(f"Failed to read {path}: {e}")
421
+
367
422
  elif os.path.isdir(path):
368
- # Directory
423
+ # Directory - collect all files for batch processing
369
424
  for root, _, files in os.walk(path):
370
425
  for fname in files:
371
426
  fpath = os.path.join(root, fname)
@@ -373,11 +428,27 @@ def run_enforcement(
373
428
  try:
374
429
  with open(fpath) as f:
375
430
  file_content = f.read()
431
+
432
+ # Run pattern assertions
376
433
  f_findings, c, s = run_pattern_assertions(rel_path, file_content, assertions)
377
434
  findings.extend(f_findings)
378
435
  checked = max(checked, c)
379
436
  skipped = max(skipped, s)
437
+
438
+ # Collect for LLM processing
439
+ if compliance_config.enabled:
440
+ files_for_llm.append((rel_path, file_content))
380
441
  except (OSError, UnicodeDecodeError):
381
442
  pass # Skip unreadable files
382
443
 
383
- return findings, errors, checked, skipped
444
+ # Run LLM assertions in batch
445
+ if files_for_llm and compliance_config.enabled:
446
+ llm_findings, budget_state, llm_errors = run_llm_assertions_batch(
447
+ files_for_llm, assertions, compliance_config
448
+ )
449
+ findings.extend(llm_findings)
450
+ errors.extend(llm_errors)
451
+ if budget_state:
452
+ skipped += budget_state.assertions_skipped
453
+
454
+ return findings, errors, checked, skipped, budget_state