invar-tools 1.8.0__py3-none-any.whl → 1.11.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.
- invar/__init__.py +8 -0
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +310 -0
- invar/mcp/handlers.py +408 -0
- invar/mcp/server.py +288 -143
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +41 -1
- invar/shell/commands/init.py +154 -16
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +67 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +902 -0
- invar/shell/skill_manager.py +355 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +85 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +220 -248
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
- invar_tools-1.11.0.dist-info/RECORD +178 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# Python Onboarding Patterns
|
|
2
|
+
|
|
3
|
+
> Patterns for migrating Python projects to Invar framework.
|
|
4
|
+
> Library: `returns` (dry-python)
|
|
5
|
+
|
|
6
|
+
## 1. Overview
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
# Library: returns (dry-python)
|
|
10
|
+
# Install: pip install returns
|
|
11
|
+
|
|
12
|
+
from returns.result import Result, Success, Failure
|
|
13
|
+
from returns.io import IOResultE
|
|
14
|
+
from returns.future import FutureResultE
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Error Handling
|
|
20
|
+
|
|
21
|
+
### 2.1 Basic Transformation
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# Before: raise
|
|
25
|
+
def get_user(id: str) -> User:
|
|
26
|
+
user = db.find(id)
|
|
27
|
+
if not user:
|
|
28
|
+
raise NotFoundError(f"User {id} not found")
|
|
29
|
+
return user
|
|
30
|
+
|
|
31
|
+
# After: Result
|
|
32
|
+
from returns.result import Result, Success, Failure
|
|
33
|
+
|
|
34
|
+
def get_user(id: str) -> Result[User, NotFoundError]:
|
|
35
|
+
user = db.find(id)
|
|
36
|
+
if not user:
|
|
37
|
+
return Failure(NotFoundError(f"User {id} not found"))
|
|
38
|
+
return Success(user)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2.2 Async Handling
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from returns.future import FutureResultE
|
|
45
|
+
from returns.io import IOResultE
|
|
46
|
+
|
|
47
|
+
# Async I/O operations
|
|
48
|
+
async def get_user_async(id: str) -> FutureResultE[User, GetUserError]:
|
|
49
|
+
"""Use FutureResultE for async operations."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
# Sync I/O operations
|
|
53
|
+
def read_config() -> IOResultE[Config, ConfigError]:
|
|
54
|
+
"""Use IOResultE for sync I/O with Result."""
|
|
55
|
+
...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2.3 Exception Catching (@safe)
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from returns.result import safe
|
|
62
|
+
|
|
63
|
+
@safe
|
|
64
|
+
def parse_json(data: str) -> dict:
|
|
65
|
+
"""Automatically converts exceptions to Failure.
|
|
66
|
+
|
|
67
|
+
>>> parse_json('{"a": 1}').unwrap()
|
|
68
|
+
{'a': 1}
|
|
69
|
+
>>> parse_json('invalid').is_failure()
|
|
70
|
+
True
|
|
71
|
+
"""
|
|
72
|
+
return json.loads(data) # JSONDecodeError -> Failure
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2.4 Chaining (bind, map)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
def process_order(order_id: str) -> Result[Receipt, OrderError]:
|
|
79
|
+
"""Chain multiple Result operations."""
|
|
80
|
+
return (
|
|
81
|
+
get_order(order_id) # Result[Order, NotFoundError]
|
|
82
|
+
.bind(validate_order) # -> Result[Order, ValidationError]
|
|
83
|
+
.map(calculate_total) # -> Result[float, ...] (pure transform)
|
|
84
|
+
.bind(charge_payment) # -> Result[Payment, PaymentError]
|
|
85
|
+
.map(generate_receipt) # -> Result[Receipt, ...]
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2.5 Error Type Hierarchy
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from dataclasses import dataclass
|
|
93
|
+
from typing import Union
|
|
94
|
+
from returns.result import Result
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class DomainError:
|
|
98
|
+
"""Base error type."""
|
|
99
|
+
message: str
|
|
100
|
+
code: str
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class NotFoundError(DomainError):
|
|
104
|
+
entity: str
|
|
105
|
+
id: str
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ValidationError(DomainError):
|
|
109
|
+
field: str
|
|
110
|
+
reason: str
|
|
111
|
+
|
|
112
|
+
# Union type for function signatures
|
|
113
|
+
OrderError = Union[NotFoundError, ValidationError, PaymentError]
|
|
114
|
+
|
|
115
|
+
def process(id: str) -> Result[Order, OrderError]:
|
|
116
|
+
...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2.6 Combining Multiple Results
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from returns.result import Result, Success
|
|
123
|
+
from returns.pointfree import bind
|
|
124
|
+
from returns.pipeline import flow
|
|
125
|
+
|
|
126
|
+
def validate_all(items: list[Item]) -> Result[list[Item], ValidationError]:
|
|
127
|
+
"""Collect all validation results."""
|
|
128
|
+
results = [validate_item(item) for item in items]
|
|
129
|
+
|
|
130
|
+
# Fail on first error
|
|
131
|
+
for r in results:
|
|
132
|
+
if r.is_failure():
|
|
133
|
+
return r
|
|
134
|
+
|
|
135
|
+
return Success([r.unwrap() for r in results])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 3. Contracts
|
|
141
|
+
|
|
142
|
+
### 3.1 Input Validation (@pre)
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from deal import pre
|
|
146
|
+
from returns.result import Result
|
|
147
|
+
|
|
148
|
+
@pre(lambda id: len(id) > 0 and len(id) <= 36)
|
|
149
|
+
@pre(lambda id: id.replace("-", "").isalnum()) # UUID format
|
|
150
|
+
def get_user(id: str) -> Result[User, NotFoundError]:
|
|
151
|
+
"""
|
|
152
|
+
Get user by ID.
|
|
153
|
+
|
|
154
|
+
>>> get_user("user-123").unwrap().name
|
|
155
|
+
'Alice'
|
|
156
|
+
>>> get_user("").is_failure() # Precondition fails
|
|
157
|
+
True
|
|
158
|
+
"""
|
|
159
|
+
...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3.2 Pure Function Contracts
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from deal import pre, post
|
|
166
|
+
|
|
167
|
+
@pre(lambda amount, rate=0.1: amount > 0 and 0 <= rate <= 1)
|
|
168
|
+
@post(lambda result: result >= 0)
|
|
169
|
+
def calculate_tax(amount: float, rate: float = 0.1) -> float:
|
|
170
|
+
"""
|
|
171
|
+
Calculate tax amount.
|
|
172
|
+
|
|
173
|
+
>>> calculate_tax(100)
|
|
174
|
+
10.0
|
|
175
|
+
>>> calculate_tax(100, 0.2)
|
|
176
|
+
20.0
|
|
177
|
+
>>> calculate_tax(-100) # Precondition fails
|
|
178
|
+
Traceback (most recent call last):
|
|
179
|
+
...
|
|
180
|
+
"""
|
|
181
|
+
return amount * rate
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 3.3 Pydantic Integration
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from pydantic import BaseModel, field_validator
|
|
188
|
+
from deal import pre
|
|
189
|
+
from returns.result import Result
|
|
190
|
+
|
|
191
|
+
class CreateUserRequest(BaseModel):
|
|
192
|
+
"""Input validation via Pydantic."""
|
|
193
|
+
email: str
|
|
194
|
+
name: str
|
|
195
|
+
|
|
196
|
+
@field_validator('email')
|
|
197
|
+
def validate_email(cls, v):
|
|
198
|
+
if '@' not in v:
|
|
199
|
+
raise ValueError('Invalid email')
|
|
200
|
+
return v.lower()
|
|
201
|
+
|
|
202
|
+
# Invar contract for business rules
|
|
203
|
+
@pre(lambda req: not user_exists(req.email)) # Business rule
|
|
204
|
+
def create_user(req: CreateUserRequest) -> Result[User, CreateUserError]:
|
|
205
|
+
...
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 4. Core/Shell Separation
|
|
211
|
+
|
|
212
|
+
### 4.1 Directory Structure
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
src/myapp/
|
|
216
|
+
├── core/ # Pure functions, no I/O
|
|
217
|
+
│ ├── __init__.py
|
|
218
|
+
│ ├── order/
|
|
219
|
+
│ │ ├── validation.py # @pre/@post + doctests
|
|
220
|
+
│ │ ├── calculation.py # Pure calculations
|
|
221
|
+
│ │ └── types.py # Domain types
|
|
222
|
+
│ └── user/
|
|
223
|
+
│ └── ...
|
|
224
|
+
└── shell/ # I/O operations
|
|
225
|
+
├── __init__.py
|
|
226
|
+
├── repositories/ # Data access
|
|
227
|
+
│ └── order_repo.py
|
|
228
|
+
├── services/ # Business orchestration
|
|
229
|
+
│ └── order_service.py
|
|
230
|
+
└── api/ # HTTP layer
|
|
231
|
+
└── routes.py
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 4.2 Core Layer Example
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# core/order/validation.py
|
|
238
|
+
from deal import pre, post
|
|
239
|
+
from returns.result import Result, Success, Failure
|
|
240
|
+
from .types import Order, ValidationError
|
|
241
|
+
|
|
242
|
+
@pre(lambda order: len(order.items) > 0)
|
|
243
|
+
def validate_order(order: Order) -> Result[Order, ValidationError]:
|
|
244
|
+
"""
|
|
245
|
+
Validate order business rules.
|
|
246
|
+
|
|
247
|
+
>>> order = Order(items=[OrderItem(sku="A", qty=1)])
|
|
248
|
+
>>> validate_order(order).is_success()
|
|
249
|
+
True
|
|
250
|
+
>>> validate_order(Order(items=[])).is_failure()
|
|
251
|
+
True
|
|
252
|
+
"""
|
|
253
|
+
for item in order.items:
|
|
254
|
+
if item.qty <= 0:
|
|
255
|
+
return Failure(ValidationError(field="qty", reason="must be positive"))
|
|
256
|
+
return Success(order)
|
|
257
|
+
|
|
258
|
+
def calculate_total(order: Order) -> float:
|
|
259
|
+
"""
|
|
260
|
+
Calculate order total (pure function).
|
|
261
|
+
|
|
262
|
+
>>> order = Order(items=[OrderItem(sku="A", qty=2, price=10.0)])
|
|
263
|
+
>>> calculate_total(order)
|
|
264
|
+
20.0
|
|
265
|
+
"""
|
|
266
|
+
return sum(item.qty * item.price for item in order.items)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 4.3 Shell Layer Example
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# shell/services/order_service.py
|
|
273
|
+
from returns.result import Result
|
|
274
|
+
from myapp.core.order.validation import validate_order, calculate_total
|
|
275
|
+
from myapp.shell.repositories.order_repo import OrderRepository
|
|
276
|
+
|
|
277
|
+
class OrderService:
|
|
278
|
+
def __init__(self, repo: OrderRepository):
|
|
279
|
+
self._repo = repo
|
|
280
|
+
|
|
281
|
+
def process_order(self, order_id: str) -> Result[Receipt, OrderError]:
|
|
282
|
+
"""Orchestrate Core functions and I/O operations."""
|
|
283
|
+
return (
|
|
284
|
+
self._repo.find(order_id) # Shell: I/O
|
|
285
|
+
.bind(validate_order) # Core: pure validation
|
|
286
|
+
.map(calculate_total) # Core: pure calculation
|
|
287
|
+
.bind(lambda total:
|
|
288
|
+
self._repo.save_total(order_id, total)) # Shell: I/O
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 5. FastAPI Integration
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from fastapi import FastAPI, HTTPException
|
|
298
|
+
from returns.result import Result
|
|
299
|
+
|
|
300
|
+
app = FastAPI()
|
|
301
|
+
|
|
302
|
+
def result_to_response(result: Result):
|
|
303
|
+
"""Convert Result to HTTP response."""
|
|
304
|
+
if result.is_success():
|
|
305
|
+
return result.unwrap()
|
|
306
|
+
|
|
307
|
+
error = result.failure()
|
|
308
|
+
if isinstance(error, NotFoundError):
|
|
309
|
+
raise HTTPException(status_code=404, detail=error.message)
|
|
310
|
+
elif isinstance(error, ValidationError):
|
|
311
|
+
raise HTTPException(status_code=400, detail=error.message)
|
|
312
|
+
else:
|
|
313
|
+
raise HTTPException(status_code=500, detail="Internal error")
|
|
314
|
+
|
|
315
|
+
@app.get("/users/{user_id}")
|
|
316
|
+
async def get_user_endpoint(user_id: str):
|
|
317
|
+
result = user_service.get_user(user_id)
|
|
318
|
+
return result_to_response(result)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 6. Must Keep `raise` Scenarios
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
# 1. WSGI/ASGI error handlers (framework expects exceptions)
|
|
327
|
+
# 2. Click/Typer CLI (uses exceptions for flow control)
|
|
328
|
+
# 3. pytest fixtures (uses exceptions)
|
|
329
|
+
# 4. Context managers (__enter__/__exit__)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 7. Migration Checklist
|
|
335
|
+
|
|
336
|
+
- [ ] Install `returns` library: `pip install returns`
|
|
337
|
+
- [ ] Define error type hierarchy (`@dataclass` or Pydantic)
|
|
338
|
+
- [ ] Transform entry points to return `Result[T, E]`
|
|
339
|
+
- [ ] Extract pure functions to `core/` directory
|
|
340
|
+
- [ ] Add `@pre/@post` contracts to Core functions
|
|
341
|
+
- [ ] Add doctests to all Core functions
|
|
342
|
+
- [ ] Run `invar guard` to verify
|
|
343
|
+
- [ ] Update API handlers to use `result_to_response`
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
*Pattern Library v1.0 — LX-09*
|