invar-tools 1.7.1__py3-none-any.whl → 1.10.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 (113) hide show
  1. invar/__init__.py +8 -0
  2. invar/core/language.py +88 -0
  3. invar/core/models.py +106 -0
  4. invar/core/patterns/detector.py +6 -1
  5. invar/core/patterns/p0_exhaustive.py +15 -3
  6. invar/core/patterns/p0_literal.py +15 -3
  7. invar/core/patterns/p0_newtype.py +15 -3
  8. invar/core/patterns/p0_nonempty.py +15 -3
  9. invar/core/patterns/p0_validation.py +15 -3
  10. invar/core/patterns/registry.py +5 -1
  11. invar/core/patterns/types.py +5 -1
  12. invar/core/property_gen.py +4 -0
  13. invar/core/rules.py +84 -18
  14. invar/core/sync_helpers.py +27 -1
  15. invar/core/template_helpers.py +32 -0
  16. invar/core/ts_parsers.py +286 -0
  17. invar/core/ts_sig_parser.py +307 -0
  18. invar/node_tools/MANIFEST +7 -0
  19. invar/node_tools/__init__.py +51 -0
  20. invar/node_tools/fc-runner/cli.js +77 -0
  21. invar/node_tools/quick-check/cli.js +28 -0
  22. invar/node_tools/ts-analyzer/cli.js +480 -0
  23. invar/shell/claude_hooks.py +35 -12
  24. invar/shell/commands/guard.py +36 -1
  25. invar/shell/commands/init.py +133 -7
  26. invar/shell/commands/perception.py +157 -33
  27. invar/shell/commands/skill.py +187 -0
  28. invar/shell/commands/template_sync.py +65 -13
  29. invar/shell/commands/uninstall.py +77 -12
  30. invar/shell/commands/update.py +6 -14
  31. invar/shell/contract_coverage.py +1 -0
  32. invar/shell/fs.py +66 -13
  33. invar/shell/pi_hooks.py +213 -0
  34. invar/shell/prove/guard_ts.py +899 -0
  35. invar/shell/skill_manager.py +353 -0
  36. invar/shell/template_engine.py +28 -4
  37. invar/shell/templates.py +4 -4
  38. invar/templates/claude-md/python/critical-rules.md +33 -0
  39. invar/templates/claude-md/python/quick-reference.md +24 -0
  40. invar/templates/claude-md/typescript/critical-rules.md +40 -0
  41. invar/templates/claude-md/typescript/quick-reference.md +24 -0
  42. invar/templates/claude-md/universal/check-in.md +25 -0
  43. invar/templates/claude-md/universal/skills.md +73 -0
  44. invar/templates/claude-md/universal/workflow.md +55 -0
  45. invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
  46. invar/templates/config/AGENT.md.jinja +256 -0
  47. invar/templates/config/CLAUDE.md.jinja +16 -209
  48. invar/templates/config/context.md.jinja +19 -0
  49. invar/templates/examples/{README.md → python/README.md} +2 -0
  50. invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
  51. invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
  52. invar/templates/examples/python/core_shell.py +227 -0
  53. invar/templates/examples/python/functional.py +613 -0
  54. invar/templates/examples/typescript/README.md +31 -0
  55. invar/templates/examples/typescript/contracts.ts +163 -0
  56. invar/templates/examples/typescript/core_shell.ts +374 -0
  57. invar/templates/examples/typescript/functional.ts +601 -0
  58. invar/templates/examples/typescript/workflow.md +95 -0
  59. invar/templates/hooks/PostToolUse.sh.jinja +10 -1
  60. invar/templates/hooks/PreToolUse.sh.jinja +38 -0
  61. invar/templates/hooks/Stop.sh.jinja +1 -1
  62. invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
  63. invar/templates/hooks/pi/invar.ts.jinja +82 -0
  64. invar/templates/manifest.toml +8 -6
  65. invar/templates/onboard/assessment.md.jinja +214 -0
  66. invar/templates/onboard/patterns/python.md +347 -0
  67. invar/templates/onboard/patterns/typescript.md +452 -0
  68. invar/templates/onboard/roadmap.md.jinja +168 -0
  69. invar/templates/protocol/INVAR.md.jinja +51 -0
  70. invar/templates/protocol/python/architecture-examples.md +41 -0
  71. invar/templates/protocol/python/contracts-syntax.md +56 -0
  72. invar/templates/protocol/python/markers.md +44 -0
  73. invar/templates/protocol/python/tools.md +24 -0
  74. invar/templates/protocol/python/troubleshooting.md +38 -0
  75. invar/templates/protocol/typescript/architecture-examples.md +52 -0
  76. invar/templates/protocol/typescript/contracts-syntax.md +73 -0
  77. invar/templates/protocol/typescript/markers.md +48 -0
  78. invar/templates/protocol/typescript/tools.md +65 -0
  79. invar/templates/protocol/typescript/troubleshooting.md +104 -0
  80. invar/templates/protocol/universal/architecture.md +36 -0
  81. invar/templates/protocol/universal/completion.md +14 -0
  82. invar/templates/protocol/universal/contracts-concept.md +37 -0
  83. invar/templates/protocol/universal/header.md +17 -0
  84. invar/templates/protocol/universal/session.md +17 -0
  85. invar/templates/protocol/universal/six-laws.md +10 -0
  86. invar/templates/protocol/universal/usbv.md +14 -0
  87. invar/templates/protocol/universal/visible-workflow.md +25 -0
  88. invar/templates/skills/develop/SKILL.md.jinja +98 -3
  89. invar/templates/skills/extensions/_registry.yaml +93 -0
  90. invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
  91. invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
  92. invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
  93. invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
  94. invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
  95. invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
  96. invar/templates/skills/extensions/security/SKILL.md +382 -0
  97. invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
  98. invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
  99. invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
  100. invar/templates/skills/investigate/SKILL.md.jinja +15 -0
  101. invar/templates/skills/propose/SKILL.md.jinja +33 -0
  102. invar/templates/skills/review/SKILL.md.jinja +346 -71
  103. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/METADATA +326 -19
  104. invar_tools-1.10.0.dist-info/RECORD +173 -0
  105. invar/templates/examples/core_shell.py +0 -127
  106. invar/templates/protocol/INVAR.md +0 -310
  107. invar_tools-1.7.1.dist-info/RECORD +0 -112
  108. /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
  109. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
  110. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
  111. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
  112. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
  113. {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,13 +1,14 @@
1
+ # ruff: noqa: ERA001
1
2
  """
2
3
  Invar Contract Examples
3
4
 
4
5
  Reference patterns for @pre/@post contracts and doctests.
5
6
  Managed by Invar - do not edit directly.
6
7
  """
8
+ # @invar:allow partial_contract: Educational file showing multiple @pre pattern
7
9
 
8
- # For lambda-based contracts, use deal directly
9
- # invar_runtime.pre/post are for Contract objects (NonEmpty, IsInstance, etc.)
10
- from deal import post, pre
10
+ # invar_runtime supports both lambda and Contract objects
11
+ from invar_runtime import post, pre
11
12
 
12
13
  # =============================================================================
13
14
  # GOOD: Complete Contract
@@ -80,7 +81,7 @@ def normalize_keys(data: dict[str, int]) -> dict[str, int]:
80
81
  # DON'T: Empty contract tells nothing
81
82
  # @pre(lambda: True)
82
83
  # @post(lambda result: True)
83
- # def process(x): ... # noqa: ERA001
84
+ # def process(x): ...
84
85
 
85
86
  # DON'T: Missing edge cases in doctests
86
87
  # def divide(a, b):
@@ -111,3 +112,79 @@ def range_size(start: int, end: int) -> int:
111
112
  1
112
113
  """
113
114
  return end - start
115
+
116
+
117
+ # =============================================================================
118
+ # CRITICAL: Default Parameters in Contracts
119
+ # =============================================================================
120
+ # Lambda signatures MUST include ALL parameters, including defaults!
121
+ # This is a common mistake that causes silent contract failures.
122
+
123
+
124
+ # DON'T: Missing default parameter in lambda
125
+ # @pre(lambda x: x >= 0) # BAD: 'y' is missing!
126
+ # def calc_bad(x: int, y: int = 0) -> int:
127
+ # return x + y
128
+
129
+
130
+ # DO: Include all parameters with their defaults
131
+ @pre(lambda x, y=0: x >= 0 and y >= 0)
132
+ @post(lambda result: result >= 0)
133
+ def calculate_with_default(x: int, y: int = 0) -> int:
134
+ """
135
+ Calculate sum with optional y.
136
+
137
+ The lambda MUST include y=0 to match the function signature.
138
+
139
+ >>> calculate_with_default(5)
140
+ 5
141
+ >>> calculate_with_default(5, 3)
142
+ 8
143
+ >>> calculate_with_default(0, 0) # Edge: both zero
144
+ 0
145
+ """
146
+ return x + y
147
+
148
+
149
+ # =============================================================================
150
+ # CRITICAL: @post Cannot Access Parameters
151
+ # =============================================================================
152
+ # @post only receives the return value, not the original parameters!
153
+
154
+
155
+ # DON'T: Reference parameters in @post
156
+ # @post(lambda result: result > x) # BAD: 'x' is not available!
157
+ # def double_bad(x: int) -> int:
158
+ # return x * 2
159
+
160
+
161
+ # DO: @post only validates the result itself
162
+ @pre(lambda x: x >= 0)
163
+ @post(lambda result: result >= 0) # GOOD: only uses 'result'
164
+ def double_positive(x: int) -> int:
165
+ """
166
+ Double a positive number.
167
+
168
+ @post can only validate result properties, not relationships to inputs.
169
+
170
+ >>> double_positive(5)
171
+ 10
172
+ >>> double_positive(0) # Edge: zero
173
+ 0
174
+ """
175
+ return x * 2
176
+
177
+
178
+ # =============================================================================
179
+ # Decorator Order with @pre/@post
180
+ # =============================================================================
181
+ # When combining with other decorators, @pre/@post should be closest to function.
182
+
183
+
184
+ # DO: @pre/@post closest to function
185
+ # @other_decorator
186
+ # @pre(lambda x: x > 0)
187
+ # @post(lambda result: result > 0)
188
+ # def my_func(x: int) -> int: ...
189
+
190
+ # This ensures contracts run BEFORE other decorators modify behavior.
@@ -0,0 +1,227 @@
1
+ # ruff: noqa: ERA001
2
+ """
3
+ Invar Core/Shell Separation Examples
4
+
5
+ Reference patterns for Core vs Shell architecture.
6
+ Managed by Invar - do not edit directly.
7
+ """
8
+ # @invar:allow forbidden_import: Example file demonstrates Shell pattern with pathlib
9
+ # @invar:allow missing_contract: Shell functions intentionally without contracts to show pattern
10
+ # @invar:allow contract_quality_ratio: Educational file with Shell examples
11
+
12
+ from pathlib import Path
13
+
14
+ # invar_runtime supports both lambda and Contract objects
15
+ from invar_runtime import post, pre
16
+ from returns.result import Failure, Result, Success
17
+
18
+ # =============================================================================
19
+ # CORE: Pure Logic (no I/O)
20
+ # =============================================================================
21
+ # Location: src/*/core/
22
+ # Requirements: @pre/@post, doctests, no I/O imports
23
+ # =============================================================================
24
+
25
+
26
+ # @invar:allow shell_result: Example file - demonstrates Core pattern
27
+ # @shell_orchestration: Example file - demonstrates Core pattern
28
+ @pre(lambda content: content is not None) # Accepts any string including empty
29
+ @post(lambda result: all(line.strip() == line and line for line in result)) # No whitespace, non-empty
30
+ def parse_lines(content: str) -> list[str]:
31
+ """
32
+ Parse content into non-empty lines.
33
+
34
+ >>> parse_lines("a\\nb\\nc")
35
+ ['a', 'b', 'c']
36
+ >>> parse_lines("")
37
+ []
38
+ >>> parse_lines(" \\n ") # Edge: whitespace only
39
+ []
40
+ """
41
+ return [line.strip() for line in content.split("\n") if line.strip()]
42
+
43
+
44
+ # @invar:allow shell_result: Example file - demonstrates Core pattern
45
+ # @shell_orchestration: Example file - demonstrates Core pattern
46
+ @pre(lambda items: all(isinstance(i, str) for i in items)) # All items must be strings
47
+ @post(lambda result: all(v > 0 for v in result.values())) # All counts are positive
48
+ def count_items(items: list[str]) -> dict[str, int]:
49
+ """
50
+ Count occurrences of each item.
51
+
52
+ >>> sorted(count_items(['a', 'b', 'a']).items())
53
+ [('a', 2), ('b', 1)]
54
+ >>> count_items([])
55
+ {}
56
+ """
57
+ counts: dict[str, int] = {}
58
+ for item in items:
59
+ counts[item] = counts.get(item, 0) + 1
60
+ return counts
61
+
62
+
63
+ # =============================================================================
64
+ # SHELL: I/O Operations
65
+ # =============================================================================
66
+ # Location: src/*/shell/
67
+ # Requirements: Result[T, E] return type, calls Core for logic
68
+ # =============================================================================
69
+
70
+
71
+ def read_file(path: Path) -> Result[str, str]:
72
+ """
73
+ Read file content.
74
+
75
+ Shell handles I/O, returns Result for error handling.
76
+ """
77
+ try:
78
+ return Success(path.read_text())
79
+ except FileNotFoundError:
80
+ return Failure(f"File not found: {path}")
81
+ except PermissionError:
82
+ return Failure(f"Permission denied: {path}")
83
+
84
+
85
+ def count_lines_in_file(path: Path) -> Result[dict[str, int], str]:
86
+ """
87
+ Count lines in file - demonstrates Core/Shell integration.
88
+
89
+ Shell reads file → Core parses content → Shell returns result.
90
+ """
91
+ # Shell: I/O operation
92
+ content_result = read_file(path)
93
+
94
+ if isinstance(content_result, Failure):
95
+ return content_result
96
+
97
+ content = content_result.unwrap()
98
+
99
+ # Core: Pure logic (no I/O)
100
+ lines = parse_lines(content)
101
+ counts = count_items(lines)
102
+
103
+ # Shell: Return result
104
+ return Success(counts)
105
+
106
+
107
+ # =============================================================================
108
+ # ANTI-PATTERNS
109
+ # =============================================================================
110
+
111
+ # DON'T: I/O in Core
112
+ # def parse_file(path: Path): # BAD: Path in Core
113
+ # content = path.read_text() # BAD: I/O in Core
114
+ # return parse_lines(content)
115
+
116
+ # DO: Core receives content, not paths
117
+ # def parse_content(content: str): # GOOD: receives data
118
+ # return parse_lines(content)
119
+
120
+
121
+ # DON'T: Missing Result in Shell
122
+ # def load_config(path: Path) -> dict: # BAD: no Result type
123
+ # return json.loads(path.read_text()) # Exceptions not handled
124
+
125
+ # DO: Return Result[T, E]
126
+ # def load_config(path: Path) -> Result[dict, str]: # GOOD
127
+ # try:
128
+ # return Success(json.loads(path.read_text()))
129
+ # except Exception as e:
130
+ # return Failure(str(e))
131
+
132
+
133
+ # =============================================================================
134
+ # FastAPI Integration Pattern
135
+ # =============================================================================
136
+ # Demonstrates how to use Result with FastAPI endpoints.
137
+ # Shell (API handler) → Core (business logic) → Shell (HTTP response)
138
+
139
+ # NOTE: This is pseudocode - requires FastAPI to be installed
140
+ # from fastapi import FastAPI, HTTPException
141
+ # from pydantic import BaseModel
142
+
143
+ # class UserResponse(BaseModel):
144
+ # id: str
145
+ # name: str
146
+
147
+ # app = FastAPI()
148
+
149
+ # CORE: Pure business logic
150
+ # @pre(lambda user_id: len(user_id) > 0)
151
+ # @post(lambda result: result.get("name") is not None)
152
+ # def get_user_data(user_id: str) -> dict:
153
+ # """Core function - pure logic, receives string ID."""
154
+ # # Business logic here (no HTTP, no database I/O)
155
+ # return {"id": user_id, "name": "Demo User"}
156
+
157
+ # SHELL: I/O layer - database
158
+ # def fetch_user_from_db(user_id: str) -> Result[dict, str]:
159
+ # """Shell function - database I/O, returns Result."""
160
+ # try:
161
+ # # In real code: query database
162
+ # if user_id == "not-found":
163
+ # return Failure("User not found")
164
+ # return Success({"id": user_id, "name": "DB User"})
165
+ # except Exception as e:
166
+ # return Failure(f"Database error: {e}")
167
+
168
+ # SHELL: I/O layer - HTTP endpoint
169
+ # @app.get("/users/{user_id}", response_model=UserResponse)
170
+ # def get_user_endpoint(user_id: str):
171
+ # """
172
+ # FastAPI endpoint - Shell layer.
173
+ #
174
+ # Pattern: Shell → Core → Shell
175
+ # 1. Shell receives HTTP request
176
+ # 2. Core processes business logic
177
+ # 3. Shell converts Result to HTTP response
178
+ # """
179
+ # result = fetch_user_from_db(user_id)
180
+ #
181
+ # # Convert Result to HTTP response
182
+ # match result:
183
+ # case Success(user):
184
+ # return UserResponse(**user)
185
+ # case Failure(error):
186
+ # if "not found" in error.lower():
187
+ # raise HTTPException(status_code=404, detail=error)
188
+ # raise HTTPException(status_code=500, detail=error)
189
+
190
+
191
+ # =============================================================================
192
+ # Result → HTTP Response Mapping
193
+ # =============================================================================
194
+ # Common pattern for converting Result errors to HTTP status codes:
195
+ #
196
+ # | Error Type | HTTP Status | When to Use |
197
+ # |-----------------|-------------|--------------------------------|
198
+ # | NotFoundError | 404 | Resource doesn't exist |
199
+ # | ValidationError | 400 | Invalid input from client |
200
+ # | AuthError | 401/403 | Authentication/authorization |
201
+ # | ConflictError | 409 | Resource state conflict |
202
+ # | InternalError | 500 | Unexpected server error |
203
+ #
204
+ # Example error types:
205
+ # @dataclass(frozen=True)
206
+ # class NotFoundError:
207
+ # resource: str
208
+ # id: str
209
+ #
210
+ # @dataclass(frozen=True)
211
+ # class ValidationError:
212
+ # field: str
213
+ # message: str
214
+ #
215
+ # AppError = NotFoundError | ValidationError | str
216
+ #
217
+ # def result_to_response(result: Result[T, AppError]) -> T:
218
+ # """Convert Result to HTTP response or raise appropriate HTTPException."""
219
+ # match result:
220
+ # case Success(value):
221
+ # return value
222
+ # case Failure(NotFoundError(resource, id)):
223
+ # raise HTTPException(404, f"{resource} {id} not found")
224
+ # case Failure(ValidationError(field, message)):
225
+ # raise HTTPException(400, f"Invalid {field}: {message}")
226
+ # case Failure(error):
227
+ # raise HTTPException(500, str(error))