invar-tools 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.
- invar/__init__.py +68 -0
- invar/contracts.py +152 -0
- invar/core/__init__.py +8 -0
- invar/core/contracts.py +375 -0
- invar/core/extraction.py +172 -0
- invar/core/formatter.py +281 -0
- invar/core/hypothesis_strategies.py +454 -0
- invar/core/inspect.py +154 -0
- invar/core/lambda_helpers.py +190 -0
- invar/core/models.py +289 -0
- invar/core/must_use.py +172 -0
- invar/core/parser.py +276 -0
- invar/core/property_gen.py +383 -0
- invar/core/purity.py +369 -0
- invar/core/purity_heuristics.py +184 -0
- invar/core/references.py +180 -0
- invar/core/rule_meta.py +203 -0
- invar/core/rules.py +435 -0
- invar/core/strategies.py +267 -0
- invar/core/suggestions.py +324 -0
- invar/core/tautology.py +137 -0
- invar/core/timeout_inference.py +114 -0
- invar/core/utils.py +364 -0
- invar/decorators.py +94 -0
- invar/invariant.py +57 -0
- invar/mcp/__init__.py +10 -0
- invar/mcp/__main__.py +13 -0
- invar/mcp/server.py +251 -0
- invar/py.typed +0 -0
- invar/resource.py +99 -0
- invar/shell/__init__.py +8 -0
- invar/shell/cli.py +358 -0
- invar/shell/config.py +248 -0
- invar/shell/fs.py +112 -0
- invar/shell/git.py +85 -0
- invar/shell/guard_helpers.py +324 -0
- invar/shell/guard_output.py +235 -0
- invar/shell/init_cmd.py +289 -0
- invar/shell/mcp_config.py +171 -0
- invar/shell/perception.py +125 -0
- invar/shell/property_tests.py +227 -0
- invar/shell/prove.py +460 -0
- invar/shell/prove_cache.py +133 -0
- invar/shell/prove_fallback.py +183 -0
- invar/shell/templates.py +443 -0
- invar/shell/test_cmd.py +117 -0
- invar/shell/testing.py +297 -0
- invar/shell/update_cmd.py +191 -0
- invar/templates/CLAUDE.md.template +58 -0
- invar/templates/INVAR.md +134 -0
- invar/templates/__init__.py +1 -0
- invar/templates/aider.conf.yml.template +29 -0
- invar/templates/context.md.template +51 -0
- invar/templates/cursorrules.template +28 -0
- invar/templates/examples/README.md +21 -0
- invar/templates/examples/contracts.py +111 -0
- invar/templates/examples/core_shell.py +121 -0
- invar/templates/pre-commit-config.yaml.template +44 -0
- invar/templates/proposal.md.template +93 -0
- invar_tools-1.0.0.dist-info/METADATA +321 -0
- invar_tools-1.0.0.dist-info/RECORD +64 -0
- invar_tools-1.0.0.dist-info/WHEEL +4 -0
- invar_tools-1.0.0.dist-info/entry_points.txt +2 -0
- invar_tools-1.0.0.dist-info/licenses/LICENSE +21 -0
invar/templates/INVAR.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
3
|
+
│ INVAR-MANAGED FILE - DO NOT EDIT DIRECTLY │
|
|
4
|
+
│ │
|
|
5
|
+
│ This file is managed by Invar. Changes may be lost on │
|
|
6
|
+
│ `invar update`. Add project content to CLAUDE.md instead. │
|
|
7
|
+
└─────────────────────────────────────────────────────────────┘
|
|
8
|
+
-->
|
|
9
|
+
# The Invar Protocol v3.26
|
|
10
|
+
|
|
11
|
+
> **"Trade structure for safety."**
|
|
12
|
+
|
|
13
|
+
## Six Laws
|
|
14
|
+
|
|
15
|
+
| Law | Principle |
|
|
16
|
+
|-----|-----------|
|
|
17
|
+
| 1. Separation | Core (pure logic) / Shell (I/O) physically separate |
|
|
18
|
+
| 2. Contract Complete | @pre/@post + doctests uniquely determine implementation |
|
|
19
|
+
| 3. Context Economy | map → sig → code (only read what's needed) |
|
|
20
|
+
| 4. Decompose First | Break into sub-functions before implementing |
|
|
21
|
+
| 5. Verify Reflectively | Fail → Reflect (why?) → Fix → Verify |
|
|
22
|
+
| 6. Integrate Fully | Local correct ≠ Global correct; verify all paths |
|
|
23
|
+
|
|
24
|
+
## Core/Shell Architecture
|
|
25
|
+
|
|
26
|
+
| Zone | Location | Requirements |
|
|
27
|
+
|------|----------|--------------|
|
|
28
|
+
| Core | `**/core/**` | @pre/@post, pure (no I/O), doctests |
|
|
29
|
+
| Shell | `**/shell/**` | `Result[T, E]` return type |
|
|
30
|
+
|
|
31
|
+
**Forbidden in Core:** `os`, `sys`, `subprocess`, `pathlib`, `open`, `requests`, `datetime.now`
|
|
32
|
+
|
|
33
|
+
## Core Example (Pure Logic)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from deal import pre, post
|
|
37
|
+
|
|
38
|
+
@pre(lambda price, discount: price > 0 and 0 <= discount <= 1)
|
|
39
|
+
@post(lambda result: result >= 0)
|
|
40
|
+
def discounted_price(price: float, discount: float) -> float:
|
|
41
|
+
"""
|
|
42
|
+
>>> discounted_price(100, 0.2)
|
|
43
|
+
80.0
|
|
44
|
+
>>> discounted_price(100, 0) # Edge: no discount
|
|
45
|
+
100.0
|
|
46
|
+
"""
|
|
47
|
+
return price * (1 - discount)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Self-test:** Can someone else write the exact same function from just @pre/@post + doctests?
|
|
51
|
+
|
|
52
|
+
## Shell Example (I/O Operations)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from returns.result import Result, Success, Failure
|
|
57
|
+
|
|
58
|
+
def read_config(path: Path) -> Result[dict, str]:
|
|
59
|
+
"""Shell: handles I/O, returns Result for error handling."""
|
|
60
|
+
try:
|
|
61
|
+
import json
|
|
62
|
+
return Success(json.loads(path.read_text()))
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
return Failure(f"File not found: {path}")
|
|
65
|
+
except json.JSONDecodeError as e:
|
|
66
|
+
return Failure(f"Invalid JSON: {e}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Pattern:** Shell reads file → passes content to Core → returns Result.
|
|
70
|
+
|
|
71
|
+
More examples: `.invar/examples/`
|
|
72
|
+
|
|
73
|
+
## Session Start (Required)
|
|
74
|
+
|
|
75
|
+
Before writing any code, execute:
|
|
76
|
+
|
|
77
|
+
1. **invar_guard** (changed=true) — Check existing violations
|
|
78
|
+
2. **invar_map** (top=10) — Understand code structure
|
|
79
|
+
|
|
80
|
+
Then read:
|
|
81
|
+
- `.invar/examples/` — Core/Shell patterns
|
|
82
|
+
- `.invar/context.md` — Project state, lessons learned
|
|
83
|
+
|
|
84
|
+
**Skipping these steps → Non-compliant code → Rework required.**
|
|
85
|
+
|
|
86
|
+
Use MCP tools if available (`invar_guard`, `invar_map`), otherwise use CLI commands.
|
|
87
|
+
For agent-specific entry format, see your configuration file (CLAUDE.md, .cursorrules, etc).
|
|
88
|
+
|
|
89
|
+
## ICIDIV Workflow (Required Order)
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
1. Intent — What? Core or Shell? Edge cases?
|
|
93
|
+
2. Contract — @pre/@post + doctests BEFORE code
|
|
94
|
+
3. Inspect — invar sig <file>, invar map --top 10
|
|
95
|
+
4. Design — Decompose: leaves first, then compose
|
|
96
|
+
5. Implement — Write code to pass your doctests
|
|
97
|
+
6. Verify — invar guard. If fail: reflect → fix → verify
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Contract before Implement. Verify after every change. No exceptions.**
|
|
101
|
+
|
|
102
|
+
## Task Completion
|
|
103
|
+
|
|
104
|
+
A task is complete only when ALL conditions are met:
|
|
105
|
+
- Session Start executed (invar_guard + invar_map, context read)
|
|
106
|
+
- Intent explicitly stated
|
|
107
|
+
- Contract written before implementation
|
|
108
|
+
- Final **invar_guard** passed
|
|
109
|
+
- User requirement satisfied
|
|
110
|
+
|
|
111
|
+
**Missing any = Task incomplete.**
|
|
112
|
+
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
invar guard # Full: static + doctests + CrossHair + Hypothesis (default)
|
|
117
|
+
invar guard --static # Static only (quick debug, ~0.5s)
|
|
118
|
+
invar guard --changed # Modified files only
|
|
119
|
+
invar sig <file> # Show contracts + signatures
|
|
120
|
+
invar map --top 10 # Most-referenced symbols
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
```toml
|
|
126
|
+
[tool.invar.guard]
|
|
127
|
+
core_paths = ["src/myapp/core"]
|
|
128
|
+
shell_paths = ["src/myapp/shell"]
|
|
129
|
+
exclude_doctest_lines = true # Don't count doctests in function size
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
*Protocol v3.26 | [Guide](docs/INVAR-GUIDE.md) | [Examples](.invar/examples/)*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Template files for invar init command."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Invar Protocol Configuration for Aider
|
|
2
|
+
# Follow the Invar Protocol in INVAR.md
|
|
3
|
+
|
|
4
|
+
# Auto-read protocol files at session start
|
|
5
|
+
read:
|
|
6
|
+
- INVAR.md
|
|
7
|
+
- .invar/examples/core_example.py
|
|
8
|
+
- .invar/examples/shell_example.py
|
|
9
|
+
- .invar/context.md
|
|
10
|
+
|
|
11
|
+
# System prompt addition
|
|
12
|
+
system-prompt: |
|
|
13
|
+
Follow the Invar Protocol in INVAR.md.
|
|
14
|
+
|
|
15
|
+
Before writing code, execute:
|
|
16
|
+
1. invar_guard (changed=true) - or: invar guard --changed
|
|
17
|
+
2. invar_map (top=10) - or: invar map --top 10
|
|
18
|
+
|
|
19
|
+
Use MCP tools if available, otherwise use CLI commands.
|
|
20
|
+
|
|
21
|
+
Workflow (ICIDIV):
|
|
22
|
+
- Intent: What? Core or Shell?
|
|
23
|
+
- Contract: @pre/@post + doctests BEFORE code
|
|
24
|
+
- Inspect: invar_sig (or: invar sig <file>)
|
|
25
|
+
- Design: Decompose first
|
|
26
|
+
- Implement: Pass your doctests
|
|
27
|
+
- Verify: invar_guard (or: invar guard)
|
|
28
|
+
|
|
29
|
+
Task complete only when final invar_guard passes.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Project Context
|
|
2
|
+
|
|
3
|
+
*Last updated: [DATE]*
|
|
4
|
+
|
|
5
|
+
## Current State
|
|
6
|
+
|
|
7
|
+
- Phase: [current development phase]
|
|
8
|
+
- Working on: [current task or feature]
|
|
9
|
+
- Blockers: None
|
|
10
|
+
|
|
11
|
+
## Recent Decisions
|
|
12
|
+
|
|
13
|
+
1. [Decision summary] - [Brief rationale]
|
|
14
|
+
|
|
15
|
+
## Lessons Learned
|
|
16
|
+
|
|
17
|
+
1. [Pitfall or issue] → [Solution or workaround]
|
|
18
|
+
|
|
19
|
+
## Documentation Structure
|
|
20
|
+
|
|
21
|
+
| File | Owner | Edit? |
|
|
22
|
+
|------|-------|-------|
|
|
23
|
+
| INVAR.md | Invar | No — use `invar update` |
|
|
24
|
+
| CLAUDE.md | User | Yes |
|
|
25
|
+
| .invar/context.md | User | Yes (this file) |
|
|
26
|
+
| .invar/examples/ | Invar | No |
|
|
27
|
+
|
|
28
|
+
**Decision rule:** Is this Invar protocol or project-specific?
|
|
29
|
+
- Protocol content → Already in INVAR.md, don't duplicate
|
|
30
|
+
- Project-specific → Add to CLAUDE.md or here
|
|
31
|
+
|
|
32
|
+
## Technical Debt
|
|
33
|
+
|
|
34
|
+
*Run `invar guard` to check current status.*
|
|
35
|
+
|
|
36
|
+
| File | Warning | Priority |
|
|
37
|
+
|------|---------|----------|
|
|
38
|
+
| (none) | — | — |
|
|
39
|
+
|
|
40
|
+
## Key Files
|
|
41
|
+
|
|
42
|
+
| File | Purpose |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| INVAR.md | Protocol reference (Invar-managed) |
|
|
45
|
+
| CLAUDE.md | Project guide (customize freely) |
|
|
46
|
+
| src/core/ | Pure business logic |
|
|
47
|
+
| src/shell/ | I/O adapters |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
*Update this file when: completing major tasks, making design decisions, discovering pitfalls.*
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Invar Protocol
|
|
2
|
+
|
|
3
|
+
Follow the Invar Protocol in INVAR.md — includes Session Start, ICIDIV workflow, and Task Completion requirements.
|
|
4
|
+
|
|
5
|
+
## Cursor-Specific: Entry Verification
|
|
6
|
+
|
|
7
|
+
Before writing any code, execute and show output from:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
invar_guard (changed=true) # Check existing violations
|
|
11
|
+
invar_map (top=10) # Understand code structure
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use MCP tools if available, otherwise use CLI commands:
|
|
15
|
+
- `invar guard --changed`
|
|
16
|
+
- `invar map --top 10`
|
|
17
|
+
|
|
18
|
+
Then read .invar/examples/ and .invar/context.md.
|
|
19
|
+
|
|
20
|
+
Skipping these steps leads to non-compliant code.
|
|
21
|
+
|
|
22
|
+
## Quick Reference
|
|
23
|
+
|
|
24
|
+
- Core (`**/core/**`): @pre/@post contracts, doctests, pure (no I/O)
|
|
25
|
+
- Shell (`**/shell/**`): Result[T, E] return type
|
|
26
|
+
- Workflow: Intent → Contract → Inspect → Design → Implement → Verify
|
|
27
|
+
- Contract before Implement. No exceptions.
|
|
28
|
+
- Task complete only when final invar_guard passes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Invar Examples
|
|
2
|
+
|
|
3
|
+
Reference examples for the Invar Protocol. These are managed by Invar.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Content |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| [contracts.py](contracts.py) | @pre/@post patterns, doctest best practices |
|
|
10
|
+
| [core_shell.py](core_shell.py) | Core/Shell separation patterns |
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Read these when you need:
|
|
15
|
+
- Contract pattern reference
|
|
16
|
+
- Core vs Shell decision guidance
|
|
17
|
+
- Doctest formatting examples
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
*Managed by Invar. Do not edit — use `invar update` to sync.*
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Invar Contract Examples
|
|
3
|
+
|
|
4
|
+
Reference patterns for @pre/@post contracts and doctests.
|
|
5
|
+
Managed by Invar - do not edit directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from deal import post, pre
|
|
9
|
+
|
|
10
|
+
# =============================================================================
|
|
11
|
+
# GOOD: Complete Contract
|
|
12
|
+
# =============================================================================
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pre(lambda price, discount: price > 0 and 0 <= discount <= 1)
|
|
16
|
+
@post(lambda result: result >= 0)
|
|
17
|
+
def discounted_price(price: float, discount: float) -> float:
|
|
18
|
+
"""
|
|
19
|
+
Apply discount to price.
|
|
20
|
+
|
|
21
|
+
>>> discounted_price(100.0, 0.2)
|
|
22
|
+
80.0
|
|
23
|
+
>>> discounted_price(100.0, 0) # Edge: no discount
|
|
24
|
+
100.0
|
|
25
|
+
>>> discounted_price(100.0, 1) # Edge: full discount
|
|
26
|
+
0.0
|
|
27
|
+
"""
|
|
28
|
+
return price * (1 - discount)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# GOOD: List Processing with Length Constraint
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pre(lambda items: len(items) > 0)
|
|
37
|
+
@post(lambda result: result >= 0)
|
|
38
|
+
def average(items: list[float]) -> float:
|
|
39
|
+
"""
|
|
40
|
+
Calculate average of non-empty list.
|
|
41
|
+
|
|
42
|
+
>>> average([1, 2, 3])
|
|
43
|
+
2.0
|
|
44
|
+
>>> average([5]) # Edge: single element
|
|
45
|
+
5.0
|
|
46
|
+
>>> average([0, 0, 0]) # Edge: all zeros
|
|
47
|
+
0.0
|
|
48
|
+
"""
|
|
49
|
+
return sum(items) / len(items)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# GOOD: Dict Comparison in Doctests
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pre(lambda data: isinstance(data, dict))
|
|
58
|
+
@post(lambda result: isinstance(result, dict))
|
|
59
|
+
def normalize_keys(data: dict[str, int]) -> dict[str, int]:
|
|
60
|
+
"""
|
|
61
|
+
Lowercase all keys.
|
|
62
|
+
|
|
63
|
+
# GOOD: Use sorted() for deterministic output
|
|
64
|
+
>>> sorted(normalize_keys({'A': 1, 'B': 2}).items())
|
|
65
|
+
[('a', 1), ('b', 2)]
|
|
66
|
+
|
|
67
|
+
# GOOD: Or use equality comparison
|
|
68
|
+
>>> normalize_keys({'X': 10}) == {'x': 10}
|
|
69
|
+
True
|
|
70
|
+
"""
|
|
71
|
+
return {k.lower(): v for k, v in data.items()}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# =============================================================================
|
|
75
|
+
# BAD: Incomplete Contract (anti-pattern)
|
|
76
|
+
# =============================================================================
|
|
77
|
+
|
|
78
|
+
# DON'T: Empty contract tells nothing
|
|
79
|
+
# @pre(lambda: True)
|
|
80
|
+
# @post(lambda result: True)
|
|
81
|
+
# def process(x): ... # noqa: ERA001
|
|
82
|
+
|
|
83
|
+
# DON'T: Missing edge cases in doctests
|
|
84
|
+
# def divide(a, b):
|
|
85
|
+
# """
|
|
86
|
+
# >>> divide(10, 2)
|
|
87
|
+
# 5.0
|
|
88
|
+
# # Missing: what about b=0?
|
|
89
|
+
# """
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# GOOD: Multiple @pre for Clarity
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pre(lambda start, end: start >= 0)
|
|
98
|
+
@pre(lambda start, end: end >= start)
|
|
99
|
+
@post(lambda result: result >= 0)
|
|
100
|
+
def range_size(start: int, end: int) -> int:
|
|
101
|
+
"""
|
|
102
|
+
Calculate size of range [start, end).
|
|
103
|
+
|
|
104
|
+
>>> range_size(0, 10)
|
|
105
|
+
10
|
|
106
|
+
>>> range_size(5, 5) # Edge: empty range
|
|
107
|
+
0
|
|
108
|
+
>>> range_size(0, 1) # Edge: single element
|
|
109
|
+
1
|
|
110
|
+
"""
|
|
111
|
+
return end - start
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Invar Core/Shell Separation Examples
|
|
3
|
+
|
|
4
|
+
Reference patterns for Core vs Shell architecture.
|
|
5
|
+
Managed by Invar - do not edit directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from deal import post, pre
|
|
11
|
+
from returns.result import Failure, Result, Success
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# CORE: Pure Logic (no I/O)
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Location: src/*/core/
|
|
17
|
+
# Requirements: @pre/@post, doctests, no I/O imports
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pre(lambda content: isinstance(content, str))
|
|
22
|
+
@post(lambda result: isinstance(result, list))
|
|
23
|
+
def parse_lines(content: str) -> list[str]:
|
|
24
|
+
"""
|
|
25
|
+
Parse content into non-empty lines.
|
|
26
|
+
|
|
27
|
+
>>> parse_lines("a\\nb\\nc")
|
|
28
|
+
['a', 'b', 'c']
|
|
29
|
+
>>> parse_lines("")
|
|
30
|
+
[]
|
|
31
|
+
>>> parse_lines(" \\n ") # Edge: whitespace only
|
|
32
|
+
[]
|
|
33
|
+
"""
|
|
34
|
+
return [line.strip() for line in content.split("\n") if line.strip()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pre(lambda items: isinstance(items, list))
|
|
38
|
+
@post(lambda result: isinstance(result, dict))
|
|
39
|
+
def count_items(items: list[str]) -> dict[str, int]:
|
|
40
|
+
"""
|
|
41
|
+
Count occurrences of each item.
|
|
42
|
+
|
|
43
|
+
>>> sorted(count_items(['a', 'b', 'a']).items())
|
|
44
|
+
[('a', 2), ('b', 1)]
|
|
45
|
+
>>> count_items([])
|
|
46
|
+
{}
|
|
47
|
+
"""
|
|
48
|
+
counts: dict[str, int] = {}
|
|
49
|
+
for item in items:
|
|
50
|
+
counts[item] = counts.get(item, 0) + 1
|
|
51
|
+
return counts
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# SHELL: I/O Operations
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Location: src/*/shell/
|
|
58
|
+
# Requirements: Result[T, E] return type, calls Core for logic
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_file(path: Path) -> Result[str, str]:
|
|
63
|
+
"""
|
|
64
|
+
Read file content.
|
|
65
|
+
|
|
66
|
+
Shell handles I/O, returns Result for error handling.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
return Success(path.read_text())
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
return Failure(f"File not found: {path}")
|
|
72
|
+
except PermissionError:
|
|
73
|
+
return Failure(f"Permission denied: {path}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def count_lines_in_file(path: Path) -> Result[dict[str, int], str]:
|
|
77
|
+
"""
|
|
78
|
+
Count lines in file - demonstrates Core/Shell integration.
|
|
79
|
+
|
|
80
|
+
Shell reads file → Core parses content → Shell returns result.
|
|
81
|
+
"""
|
|
82
|
+
# Shell: I/O operation
|
|
83
|
+
content_result = read_file(path)
|
|
84
|
+
|
|
85
|
+
if isinstance(content_result, Failure):
|
|
86
|
+
return content_result
|
|
87
|
+
|
|
88
|
+
content = content_result.unwrap()
|
|
89
|
+
|
|
90
|
+
# Core: Pure logic (no I/O)
|
|
91
|
+
lines = parse_lines(content)
|
|
92
|
+
counts = count_items(lines)
|
|
93
|
+
|
|
94
|
+
# Shell: Return result
|
|
95
|
+
return Success(counts)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# =============================================================================
|
|
99
|
+
# ANTI-PATTERNS
|
|
100
|
+
# =============================================================================
|
|
101
|
+
|
|
102
|
+
# DON'T: I/O in Core
|
|
103
|
+
# def parse_file(path: Path): # BAD: Path in Core
|
|
104
|
+
# content = path.read_text() # BAD: I/O in Core # noqa: ERA001
|
|
105
|
+
# return parse_lines(content) # noqa: ERA001
|
|
106
|
+
|
|
107
|
+
# DO: Core receives content, not paths
|
|
108
|
+
# def parse_content(content: str): # GOOD: receives data
|
|
109
|
+
# return parse_lines(content) # noqa: ERA001
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# DON'T: Missing Result in Shell
|
|
113
|
+
# def load_config(path: Path) -> dict: # BAD: no Result type
|
|
114
|
+
# return json.loads(path.read_text()) # Exceptions not handled # noqa: ERA001
|
|
115
|
+
|
|
116
|
+
# DO: Return Result[T, E]
|
|
117
|
+
# def load_config(path: Path) -> Result[dict, str]: # GOOD
|
|
118
|
+
# try: # noqa: ERA001
|
|
119
|
+
# return Success(json.loads(path.read_text())) # noqa: ERA001
|
|
120
|
+
# except Exception as e: # noqa: ERA001
|
|
121
|
+
# return Failure(str(e)) # noqa: ERA001
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Invar Pre-commit Hooks
|
|
2
|
+
# Install: pre-commit install
|
|
3
|
+
#
|
|
4
|
+
# Default runs full verification (static + doctests + CrossHair + Hypothesis).
|
|
5
|
+
# Incremental mode makes this fast: ~5s first commit, ~2s subsequent (cached).
|
|
6
|
+
#
|
|
7
|
+
# Smart Guard: Detects rule-affecting file changes and runs full guard when needed.
|
|
8
|
+
# Structure Protection: Warns if INVAR.md is modified directly.
|
|
9
|
+
|
|
10
|
+
repos:
|
|
11
|
+
- repo: local
|
|
12
|
+
hooks:
|
|
13
|
+
# Warn if INVAR.md is modified directly (use --no-verify if intentional)
|
|
14
|
+
- id: invar-md-protected
|
|
15
|
+
name: INVAR.md Protection
|
|
16
|
+
entry: bash -c 'if git diff --cached --name-only | grep -q "^INVAR.md$"; then echo "Warning - INVAR.md was modified directly. Use git commit --no-verify if intentional."; exit 1; fi'
|
|
17
|
+
language: system
|
|
18
|
+
pass_filenames: false
|
|
19
|
+
stages: [pre-commit]
|
|
20
|
+
|
|
21
|
+
# Invar Guard (full verification by default)
|
|
22
|
+
# Uses incremental mode: only verifies changed files, caches results
|
|
23
|
+
# Smart mode: runs full guard when rule files change, --changed otherwise
|
|
24
|
+
- id: invar-guard
|
|
25
|
+
name: Invar Guard
|
|
26
|
+
entry: bash -c '
|
|
27
|
+
RULE_FILES="rule_meta.py rules.py contracts.py purity.py pyproject.toml"
|
|
28
|
+
STAGED=$(git diff --cached --name-only)
|
|
29
|
+
FULL=false
|
|
30
|
+
for f in $RULE_FILES; do
|
|
31
|
+
if echo "$STAGED" | grep -q "$f"; then FULL=true; break; fi
|
|
32
|
+
done
|
|
33
|
+
if [ "$FULL" = true ]; then
|
|
34
|
+
echo "⚠️ Rule change detected - running FULL guard"
|
|
35
|
+
invar guard
|
|
36
|
+
else
|
|
37
|
+
invar guard --changed
|
|
38
|
+
fi
|
|
39
|
+
'
|
|
40
|
+
language: python
|
|
41
|
+
additional_dependencies: ['invar-tools']
|
|
42
|
+
pass_filenames: false
|
|
43
|
+
stages: [pre-commit]
|
|
44
|
+
types: [python]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Protocol Change Proposal
|
|
2
|
+
|
|
3
|
+
> Copy this template to `YYYY-MM-DD-title.md` when proposing changes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Metadata
|
|
8
|
+
|
|
9
|
+
- **Date:** YYYY-MM-DD
|
|
10
|
+
- **Author:** [Agent/Human]
|
|
11
|
+
- **Status:** Draft | Pending Approval | Approved | Rejected
|
|
12
|
+
- **Layer:** L1 (Protocol) | L2 (Project)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Trigger
|
|
17
|
+
|
|
18
|
+
What problem or friction caused this proposal?
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Describe the specific situation that revealed a gap in the protocol.
|
|
22
|
+
Include concrete examples if possible.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Proposed Change
|
|
28
|
+
|
|
29
|
+
What specific change is proposed?
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Be precise. Show before/after if applicable.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Evidence
|
|
38
|
+
|
|
39
|
+
What experience supports this change?
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
- What happened that demonstrates the need?
|
|
43
|
+
- How many times did this issue occur?
|
|
44
|
+
- What was the impact (time lost, bugs introduced, etc.)?
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Impact Analysis
|
|
50
|
+
|
|
51
|
+
What else needs to change if this is approved?
|
|
52
|
+
|
|
53
|
+
- [ ] INVAR.md sections affected
|
|
54
|
+
- [ ] CLAUDE.md updates needed
|
|
55
|
+
- [ ] Template files to update
|
|
56
|
+
- [ ] Code changes required
|
|
57
|
+
- [ ] Other documentation
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Alternatives Considered
|
|
62
|
+
|
|
63
|
+
What other approaches were considered and why were they rejected?
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
1. Alternative A: ...
|
|
67
|
+
Rejected because: ...
|
|
68
|
+
|
|
69
|
+
2. Alternative B: ...
|
|
70
|
+
Rejected because: ...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Approval
|
|
76
|
+
|
|
77
|
+
**For Layer 1 changes only:**
|
|
78
|
+
|
|
79
|
+
- [ ] Human has reviewed this proposal
|
|
80
|
+
- [ ] Human explicitly approves: _____________ (signature/date)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Implementation Checklist
|
|
85
|
+
|
|
86
|
+
After approval:
|
|
87
|
+
|
|
88
|
+
- [ ] Update INVAR.md
|
|
89
|
+
- [ ] Update version number
|
|
90
|
+
- [ ] Sync templates
|
|
91
|
+
- [ ] Update related documentation
|
|
92
|
+
- [ ] Add to version history
|
|
93
|
+
- [ ] Commit with clear message
|