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.
- crucible/cli.py +532 -12
- crucible/enforcement/budget.py +179 -0
- 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 +486 -0
- crucible/enforcement/models.py +71 -1
- 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/review/core.py +78 -7
- crucible/server.py +81 -14
- 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/tools/git.py +17 -4
- crucible_mcp-1.0.0.dist-info/METADATA +198 -0
- crucible_mcp-1.0.0.dist-info/RECORD +66 -0
- crucible_mcp-0.4.0.dist-info/METADATA +0 -160
- crucible_mcp-0.4.0.dist-info/RECORD +0 -28
- {crucible_mcp-0.4.0.dist-info → crucible_mcp-1.0.0.dist-info}/WHEEL +0 -0
- {crucible_mcp-0.4.0.dist-info → crucible_mcp-1.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|