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.
- invar/__init__.py +8 -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/template_helpers.py +32 -0
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +307 -0
- 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/guard.py +36 -1
- invar/shell/commands/init.py +133 -7
- 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 +77 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/fs.py +66 -13
- invar/shell/pi_hooks.py +213 -0
- invar/shell/prove/guard_ts.py +899 -0
- invar/shell/skill_manager.py +353 -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 +256 -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 +82 -0
- invar/templates/manifest.toml +8 -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 +98 -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/investigate/SKILL.md.jinja +15 -0
- invar/templates/skills/propose/SKILL.md.jinja +33 -0
- invar/templates/skills/review/SKILL.md.jinja +346 -71
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/METADATA +326 -19
- invar_tools-1.10.0.dist-info/RECORD +173 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.7.1.dist-info/RECORD +0 -112
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
invar/__init__.py
CHANGED
|
@@ -32,6 +32,7 @@ from invar_runtime import (
|
|
|
32
32
|
NoNone,
|
|
33
33
|
Percentage,
|
|
34
34
|
Positive,
|
|
35
|
+
RelationViolation,
|
|
35
36
|
ResourceWarning,
|
|
36
37
|
Sorted,
|
|
37
38
|
SortedNonEmpty,
|
|
@@ -42,8 +43,11 @@ from invar_runtime import (
|
|
|
42
43
|
must_use,
|
|
43
44
|
post,
|
|
44
45
|
pre,
|
|
46
|
+
relates,
|
|
47
|
+
relates_multi,
|
|
45
48
|
skip_property_test,
|
|
46
49
|
strategy,
|
|
50
|
+
to_post_contract,
|
|
47
51
|
)
|
|
48
52
|
|
|
49
53
|
__all__ = [
|
|
@@ -60,6 +64,7 @@ __all__ = [
|
|
|
60
64
|
"NonNegative",
|
|
61
65
|
"Percentage",
|
|
62
66
|
"Positive",
|
|
67
|
+
"RelationViolation",
|
|
63
68
|
"ResourceWarning",
|
|
64
69
|
"Sorted",
|
|
65
70
|
"SortedNonEmpty",
|
|
@@ -70,6 +75,9 @@ __all__ = [
|
|
|
70
75
|
"must_use",
|
|
71
76
|
"post",
|
|
72
77
|
"pre",
|
|
78
|
+
"relates",
|
|
79
|
+
"relates_multi",
|
|
73
80
|
"skip_property_test",
|
|
74
81
|
"strategy",
|
|
82
|
+
"to_post_contract",
|
|
75
83
|
]
|
invar/core/language.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Language detection for multi-language support.
|
|
2
|
+
|
|
3
|
+
This module provides language detection based on project marker files.
|
|
4
|
+
Part of LX-05 language-agnostic architecture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from deal import post, pre
|
|
8
|
+
|
|
9
|
+
# Supported languages for Invar verification
|
|
10
|
+
SUPPORTED_LANGUAGES: frozenset[str] = frozenset({"python", "typescript"})
|
|
11
|
+
|
|
12
|
+
# Future languages (detected but not yet supported)
|
|
13
|
+
FUTURE_LANGUAGES: frozenset[str] = frozenset({"rust", "go"})
|
|
14
|
+
|
|
15
|
+
# All detectable languages
|
|
16
|
+
ALL_DETECTABLE: frozenset[str] = SUPPORTED_LANGUAGES | FUTURE_LANGUAGES
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pre(lambda markers: isinstance(markers, (set, frozenset)))
|
|
20
|
+
@post(lambda result: result in ALL_DETECTABLE)
|
|
21
|
+
def detect_language_from_markers(markers: frozenset[str]) -> str:
|
|
22
|
+
"""Detect project language from marker file names (pure logic).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
markers: Set of marker file names present in the project root.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Detected language identifier string.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> detect_language_from_markers(frozenset({"pyproject.toml"}))
|
|
32
|
+
'python'
|
|
33
|
+
|
|
34
|
+
>>> detect_language_from_markers(frozenset({"setup.py", "README.md"}))
|
|
35
|
+
'python'
|
|
36
|
+
|
|
37
|
+
>>> detect_language_from_markers(frozenset({"tsconfig.json"}))
|
|
38
|
+
'typescript'
|
|
39
|
+
|
|
40
|
+
>>> detect_language_from_markers(frozenset({"package.json"}))
|
|
41
|
+
'typescript'
|
|
42
|
+
|
|
43
|
+
>>> detect_language_from_markers(frozenset({"Cargo.toml"}))
|
|
44
|
+
'rust'
|
|
45
|
+
|
|
46
|
+
>>> detect_language_from_markers(frozenset({"go.mod"}))
|
|
47
|
+
'go'
|
|
48
|
+
|
|
49
|
+
>>> detect_language_from_markers(frozenset()) # Empty defaults to python
|
|
50
|
+
'python'
|
|
51
|
+
|
|
52
|
+
>>> detect_language_from_markers(frozenset({"README.md"})) # Unknown defaults to python
|
|
53
|
+
'python'
|
|
54
|
+
"""
|
|
55
|
+
# Detection order matters - first match wins
|
|
56
|
+
if "pyproject.toml" in markers or "setup.py" in markers:
|
|
57
|
+
return "python"
|
|
58
|
+
if "tsconfig.json" in markers or "package.json" in markers:
|
|
59
|
+
return "typescript"
|
|
60
|
+
if "Cargo.toml" in markers:
|
|
61
|
+
return "rust"
|
|
62
|
+
if "go.mod" in markers:
|
|
63
|
+
return "go"
|
|
64
|
+
return "python" # Default
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pre(lambda lang: isinstance(lang, str) and len(lang) > 0)
|
|
68
|
+
@post(lambda result: isinstance(result, bool))
|
|
69
|
+
def is_supported(lang: str) -> bool:
|
|
70
|
+
"""Check if a language is currently supported for verification.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
lang: Language identifier to check.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if the language has full Invar support.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> is_supported("python")
|
|
80
|
+
True
|
|
81
|
+
>>> is_supported("typescript")
|
|
82
|
+
True
|
|
83
|
+
>>> is_supported("rust")
|
|
84
|
+
False
|
|
85
|
+
>>> is_supported("go")
|
|
86
|
+
False
|
|
87
|
+
"""
|
|
88
|
+
return lang in SUPPORTED_LANGUAGES
|
invar/core/models.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# @invar:allow file_size: LX-10 added layer types and functions, extraction planned
|
|
1
2
|
"""
|
|
2
3
|
Pydantic models for Invar.
|
|
3
4
|
|
|
@@ -7,6 +8,7 @@ No I/O operations allowed.
|
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
11
|
+
from dataclasses import dataclass
|
|
10
12
|
from enum import Enum
|
|
11
13
|
from typing import Literal
|
|
12
14
|
|
|
@@ -31,6 +33,47 @@ class Severity(str, Enum):
|
|
|
31
33
|
SUGGEST = "suggest" # DX-61: Functional pattern suggestions
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
class CodeLayer(str, Enum):
|
|
37
|
+
"""Code layer for differentiated size limits (LX-10)."""
|
|
38
|
+
|
|
39
|
+
CORE = "core"
|
|
40
|
+
SHELL = "shell"
|
|
41
|
+
TESTS = "tests"
|
|
42
|
+
DEFAULT = "default"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class LayerLimits:
|
|
47
|
+
"""Size limits for a specific code layer (LX-10).
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> limits = LayerLimits(max_file_lines=500, max_function_lines=50)
|
|
51
|
+
>>> limits.max_file_lines
|
|
52
|
+
500
|
|
53
|
+
>>> limits.max_function_lines
|
|
54
|
+
50
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
max_file_lines: int
|
|
58
|
+
max_function_lines: int
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# LX-10: Hardcoded layer limits (no config needed)
|
|
62
|
+
PYTHON_LAYER_LIMITS: dict[CodeLayer, LayerLimits] = {
|
|
63
|
+
CodeLayer.CORE: LayerLimits(500, 50),
|
|
64
|
+
CodeLayer.SHELL: LayerLimits(700, 100),
|
|
65
|
+
CodeLayer.TESTS: LayerLimits(1000, 200),
|
|
66
|
+
CodeLayer.DEFAULT: LayerLimits(600, 80),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
TYPESCRIPT_LAYER_LIMITS: dict[CodeLayer, LayerLimits] = {
|
|
70
|
+
CodeLayer.CORE: LayerLimits(650, 65),
|
|
71
|
+
CodeLayer.SHELL: LayerLimits(910, 130),
|
|
72
|
+
CodeLayer.TESTS: LayerLimits(1300, 260),
|
|
73
|
+
CodeLayer.DEFAULT: LayerLimits(780, 104),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
34
77
|
class Contract(BaseModel):
|
|
35
78
|
"""A contract (precondition or postcondition) on a function."""
|
|
36
79
|
|
|
@@ -72,6 +115,69 @@ class FileInfo(BaseModel):
|
|
|
72
115
|
source: str = "" # Original source code for advanced analysis
|
|
73
116
|
|
|
74
117
|
|
|
118
|
+
# LX-10: Layer detection functions
|
|
119
|
+
@pre(lambda file_info: file_info is not None and hasattr(file_info, "path"))
|
|
120
|
+
@post(lambda result: result in CodeLayer)
|
|
121
|
+
def get_layer(file_info: FileInfo) -> CodeLayer:
|
|
122
|
+
"""
|
|
123
|
+
Determine code layer from FileInfo classification.
|
|
124
|
+
|
|
125
|
+
Uses existing is_core/is_shell fields. Tests detection via path.
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
>>> get_layer(FileInfo(path="src/core/logic.py", lines=10, is_core=True))
|
|
129
|
+
<CodeLayer.CORE: 'core'>
|
|
130
|
+
>>> get_layer(FileInfo(path="src/shell/cli.py", lines=10, is_shell=True))
|
|
131
|
+
<CodeLayer.SHELL: 'shell'>
|
|
132
|
+
>>> get_layer(FileInfo(path="tests/test_foo.py", lines=10))
|
|
133
|
+
<CodeLayer.TESTS: 'tests'>
|
|
134
|
+
>>> get_layer(FileInfo(path="src/utils.py", lines=10))
|
|
135
|
+
<CodeLayer.DEFAULT: 'default'>
|
|
136
|
+
>>> # Edge: "test_" must be at filename start, not anywhere in path
|
|
137
|
+
>>> get_layer(FileInfo(path="src/contest_utils.py", lines=10))
|
|
138
|
+
<CodeLayer.DEFAULT: 'default'>
|
|
139
|
+
>>> get_layer(FileInfo(path="src/foo_test.py", lines=10))
|
|
140
|
+
<CodeLayer.TESTS: 'tests'>
|
|
141
|
+
"""
|
|
142
|
+
# Tests: path-based (no is_tests field exists)
|
|
143
|
+
path_lower = file_info.path.replace("\\", "/").lower()
|
|
144
|
+
filename = path_lower.rsplit("/", 1)[-1] # Extract filename
|
|
145
|
+
# Match: /tests/ dir, /test/ dir, test_*.py files, *_test.py files
|
|
146
|
+
if (
|
|
147
|
+
"/tests/" in path_lower
|
|
148
|
+
or "/test/" in path_lower
|
|
149
|
+
or filename.startswith("test_")
|
|
150
|
+
or filename.endswith("_test.py")
|
|
151
|
+
):
|
|
152
|
+
return CodeLayer.TESTS
|
|
153
|
+
|
|
154
|
+
# Core/Shell: use existing classification
|
|
155
|
+
if file_info.is_core:
|
|
156
|
+
return CodeLayer.CORE
|
|
157
|
+
if file_info.is_shell:
|
|
158
|
+
return CodeLayer.SHELL
|
|
159
|
+
|
|
160
|
+
return CodeLayer.DEFAULT
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pre(lambda layer, language="python": isinstance(layer, CodeLayer) and language in ("python", "typescript"))
|
|
164
|
+
@post(lambda result: result.max_file_lines > 0 and result.max_function_lines > 0)
|
|
165
|
+
def get_limits(layer: CodeLayer, language: str = "python") -> LayerLimits:
|
|
166
|
+
"""
|
|
167
|
+
Get size limits for layer and language.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
>>> get_limits(CodeLayer.CORE).max_function_lines
|
|
171
|
+
50
|
|
172
|
+
>>> get_limits(CodeLayer.SHELL).max_function_lines
|
|
173
|
+
100
|
|
174
|
+
>>> get_limits(CodeLayer.CORE, "typescript").max_function_lines
|
|
175
|
+
65
|
|
176
|
+
"""
|
|
177
|
+
limits = TYPESCRIPT_LAYER_LIMITS if language == "typescript" else PYTHON_LAYER_LIMITS
|
|
178
|
+
return limits[layer]
|
|
179
|
+
|
|
180
|
+
|
|
75
181
|
class Violation(BaseModel):
|
|
76
182
|
"""A rule violation found by Guard."""
|
|
77
183
|
|
invar/core/patterns/detector.py
CHANGED
|
@@ -27,6 +27,7 @@ class PatternDetector(Protocol):
|
|
|
27
27
|
Detectors analyze AST nodes and return suggestions with confidence levels.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
+
# @invar:allow missing_doctest: Abstract property - no executable implementation
|
|
30
31
|
@property
|
|
31
32
|
@abstractmethod
|
|
32
33
|
@post(lambda result: result in PatternID)
|
|
@@ -34,6 +35,7 @@ class PatternDetector(Protocol):
|
|
|
34
35
|
"""Unique identifier for this pattern."""
|
|
35
36
|
...
|
|
36
37
|
|
|
38
|
+
# @invar:allow missing_doctest: Abstract property - no executable implementation
|
|
37
39
|
@property
|
|
38
40
|
@abstractmethod
|
|
39
41
|
@post(lambda result: result in Priority)
|
|
@@ -41,6 +43,7 @@ class PatternDetector(Protocol):
|
|
|
41
43
|
"""Priority tier (P0 or P1)."""
|
|
42
44
|
...
|
|
43
45
|
|
|
46
|
+
# @invar:allow missing_doctest: Abstract property - no executable implementation
|
|
44
47
|
@property
|
|
45
48
|
@abstractmethod
|
|
46
49
|
@post(lambda result: len(result) > 0)
|
|
@@ -48,6 +51,7 @@ class PatternDetector(Protocol):
|
|
|
48
51
|
"""Human-readable description of the pattern."""
|
|
49
52
|
...
|
|
50
53
|
|
|
54
|
+
# @invar:allow missing_doctest: Abstract method - no executable implementation
|
|
51
55
|
@abstractmethod
|
|
52
56
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
|
53
57
|
def detect(self, tree: ast.AST, file_path: str) -> list[PatternSuggestion]:
|
|
@@ -140,7 +144,8 @@ class BaseDetector:
|
|
|
140
144
|
return f"{left} | {right}"
|
|
141
145
|
else:
|
|
142
146
|
# Python 3.9+ always has ast.unparse (project requires 3.11+)
|
|
143
|
-
|
|
147
|
+
result = ast.unparse(annotation)
|
|
148
|
+
return result if result else "<unknown>"
|
|
144
149
|
|
|
145
150
|
@pre(lambda self, params, type_name: len(type_name) > 0)
|
|
146
151
|
@post(lambda result: result >= 0)
|
|
@@ -51,19 +51,31 @@ class ExhaustiveMatchDetector(BaseDetector):
|
|
|
51
51
|
@property
|
|
52
52
|
@post(lambda result: result == PatternID.EXHAUSTIVE)
|
|
53
53
|
def pattern_id(self) -> PatternID:
|
|
54
|
-
"""Unique identifier for this pattern.
|
|
54
|
+
"""Unique identifier for this pattern.
|
|
55
|
+
|
|
56
|
+
>>> ExhaustiveMatchDetector().pattern_id
|
|
57
|
+
<PatternID.EXHAUSTIVE: 'exhaustive'>
|
|
58
|
+
"""
|
|
55
59
|
return PatternID.EXHAUSTIVE
|
|
56
60
|
|
|
57
61
|
@property
|
|
58
62
|
@post(lambda result: result == Priority.P0)
|
|
59
63
|
def priority(self) -> Priority:
|
|
60
|
-
"""Priority tier.
|
|
64
|
+
"""Priority tier.
|
|
65
|
+
|
|
66
|
+
>>> ExhaustiveMatchDetector().priority
|
|
67
|
+
<Priority.P0: 'P0'>
|
|
68
|
+
"""
|
|
61
69
|
return Priority.P0
|
|
62
70
|
|
|
63
71
|
@property
|
|
64
72
|
@post(lambda result: len(result) > 0)
|
|
65
73
|
def description(self) -> str:
|
|
66
|
-
"""Human-readable description.
|
|
74
|
+
"""Human-readable description.
|
|
75
|
+
|
|
76
|
+
>>> len(ExhaustiveMatchDetector().description) > 0
|
|
77
|
+
True
|
|
78
|
+
"""
|
|
67
79
|
return "Use assert_never for exhaustive enum matching"
|
|
68
80
|
|
|
69
81
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
|
@@ -48,19 +48,31 @@ class LiteralDetector(BaseDetector):
|
|
|
48
48
|
@property
|
|
49
49
|
@post(lambda result: result == PatternID.LITERAL)
|
|
50
50
|
def pattern_id(self) -> PatternID:
|
|
51
|
-
"""Unique identifier for this pattern.
|
|
51
|
+
"""Unique identifier for this pattern.
|
|
52
|
+
|
|
53
|
+
>>> LiteralDetector().pattern_id
|
|
54
|
+
<PatternID.LITERAL: 'literal'>
|
|
55
|
+
"""
|
|
52
56
|
return PatternID.LITERAL
|
|
53
57
|
|
|
54
58
|
@property
|
|
55
59
|
@post(lambda result: result == Priority.P0)
|
|
56
60
|
def priority(self) -> Priority:
|
|
57
|
-
"""Priority tier.
|
|
61
|
+
"""Priority tier.
|
|
62
|
+
|
|
63
|
+
>>> LiteralDetector().priority
|
|
64
|
+
<Priority.P0: 'P0'>
|
|
65
|
+
"""
|
|
58
66
|
return Priority.P0
|
|
59
67
|
|
|
60
68
|
@property
|
|
61
69
|
@post(lambda result: len(result) > 0)
|
|
62
70
|
def description(self) -> str:
|
|
63
|
-
"""Human-readable description.
|
|
71
|
+
"""Human-readable description.
|
|
72
|
+
|
|
73
|
+
>>> len(LiteralDetector().description) > 0
|
|
74
|
+
True
|
|
75
|
+
"""
|
|
64
76
|
return "Use Literal type for finite value sets"
|
|
65
77
|
|
|
66
78
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
|
@@ -50,19 +50,31 @@ class NewTypeDetector(BaseDetector):
|
|
|
50
50
|
@property
|
|
51
51
|
@post(lambda result: result == PatternID.NEWTYPE)
|
|
52
52
|
def pattern_id(self) -> PatternID:
|
|
53
|
-
"""Unique identifier for this pattern.
|
|
53
|
+
"""Unique identifier for this pattern.
|
|
54
|
+
|
|
55
|
+
>>> NewTypeDetector().pattern_id
|
|
56
|
+
<PatternID.NEWTYPE: 'newtype'>
|
|
57
|
+
"""
|
|
54
58
|
return PatternID.NEWTYPE
|
|
55
59
|
|
|
56
60
|
@property
|
|
57
61
|
@post(lambda result: result == Priority.P0)
|
|
58
62
|
def priority(self) -> Priority:
|
|
59
|
-
"""Priority tier.
|
|
63
|
+
"""Priority tier.
|
|
64
|
+
|
|
65
|
+
>>> NewTypeDetector().priority
|
|
66
|
+
<Priority.P0: 'P0'>
|
|
67
|
+
"""
|
|
60
68
|
return Priority.P0
|
|
61
69
|
|
|
62
70
|
@property
|
|
63
71
|
@post(lambda result: len(result) > 0)
|
|
64
72
|
def description(self) -> str:
|
|
65
|
-
"""Human-readable description.
|
|
73
|
+
"""Human-readable description.
|
|
74
|
+
|
|
75
|
+
>>> len(NewTypeDetector().description) > 0
|
|
76
|
+
True
|
|
77
|
+
"""
|
|
66
78
|
return "Use NewType for semantic clarity with multiple same-type parameters"
|
|
67
79
|
|
|
68
80
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
|
@@ -47,19 +47,31 @@ class NonEmptyDetector(BaseDetector):
|
|
|
47
47
|
@property
|
|
48
48
|
@post(lambda result: result == PatternID.NONEMPTY)
|
|
49
49
|
def pattern_id(self) -> PatternID:
|
|
50
|
-
"""Unique identifier for this pattern.
|
|
50
|
+
"""Unique identifier for this pattern.
|
|
51
|
+
|
|
52
|
+
>>> NonEmptyDetector().pattern_id
|
|
53
|
+
<PatternID.NONEMPTY: 'nonempty'>
|
|
54
|
+
"""
|
|
51
55
|
return PatternID.NONEMPTY
|
|
52
56
|
|
|
53
57
|
@property
|
|
54
58
|
@post(lambda result: result == Priority.P0)
|
|
55
59
|
def priority(self) -> Priority:
|
|
56
|
-
"""Priority tier.
|
|
60
|
+
"""Priority tier.
|
|
61
|
+
|
|
62
|
+
>>> NonEmptyDetector().priority
|
|
63
|
+
<Priority.P0: 'P0'>
|
|
64
|
+
"""
|
|
57
65
|
return Priority.P0
|
|
58
66
|
|
|
59
67
|
@property
|
|
60
68
|
@post(lambda result: len(result) > 0)
|
|
61
69
|
def description(self) -> str:
|
|
62
|
-
"""Human-readable description.
|
|
70
|
+
"""Human-readable description.
|
|
71
|
+
|
|
72
|
+
>>> len(NonEmptyDetector().description) > 0
|
|
73
|
+
True
|
|
74
|
+
"""
|
|
63
75
|
return "Use NonEmpty type for compile-time non-empty guarantees"
|
|
64
76
|
|
|
65
77
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
|
@@ -55,19 +55,31 @@ class ValidationDetector(BaseDetector):
|
|
|
55
55
|
@property
|
|
56
56
|
@post(lambda result: result == PatternID.VALIDATION)
|
|
57
57
|
def pattern_id(self) -> PatternID:
|
|
58
|
-
"""Unique identifier for this pattern.
|
|
58
|
+
"""Unique identifier for this pattern.
|
|
59
|
+
|
|
60
|
+
>>> ValidationDetector().pattern_id
|
|
61
|
+
<PatternID.VALIDATION: 'validation'>
|
|
62
|
+
"""
|
|
59
63
|
return PatternID.VALIDATION
|
|
60
64
|
|
|
61
65
|
@property
|
|
62
66
|
@post(lambda result: result == Priority.P0)
|
|
63
67
|
def priority(self) -> Priority:
|
|
64
|
-
"""Priority tier.
|
|
68
|
+
"""Priority tier.
|
|
69
|
+
|
|
70
|
+
>>> ValidationDetector().priority
|
|
71
|
+
<Priority.P0: 'P0'>
|
|
72
|
+
"""
|
|
65
73
|
return Priority.P0
|
|
66
74
|
|
|
67
75
|
@property
|
|
68
76
|
@post(lambda result: len(result) > 0)
|
|
69
77
|
def description(self) -> str:
|
|
70
|
-
"""Human-readable description.
|
|
78
|
+
"""Human-readable description.
|
|
79
|
+
|
|
80
|
+
>>> len(ValidationDetector().description) > 0
|
|
81
|
+
True
|
|
82
|
+
"""
|
|
71
83
|
return "Use error accumulation instead of fail-fast validation"
|
|
72
84
|
|
|
73
85
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
invar/core/patterns/registry.py
CHANGED
|
@@ -53,7 +53,11 @@ class PatternRegistry:
|
|
|
53
53
|
@property
|
|
54
54
|
@post(lambda result: len(result) > 0)
|
|
55
55
|
def detectors(self) -> list[PatternDetector]:
|
|
56
|
-
"""Get all registered detectors.
|
|
56
|
+
"""Get all registered detectors.
|
|
57
|
+
|
|
58
|
+
>>> len(PatternRegistry().detectors) >= 5
|
|
59
|
+
True
|
|
60
|
+
"""
|
|
57
61
|
return self._detectors
|
|
58
62
|
|
|
59
63
|
@post(lambda result: result is not None)
|
invar/core/patterns/types.py
CHANGED
|
@@ -149,7 +149,11 @@ class DetectionResult:
|
|
|
149
149
|
@property
|
|
150
150
|
@post(lambda result: isinstance(result, bool))
|
|
151
151
|
def has_suggestions(self) -> bool:
|
|
152
|
-
"""Check if any suggestions were found.
|
|
152
|
+
"""Check if any suggestions were found.
|
|
153
|
+
|
|
154
|
+
>>> DetectionResult(file="test.py", suggestions=[], patterns_checked=[]).has_suggestions
|
|
155
|
+
False
|
|
156
|
+
"""
|
|
153
157
|
return len(self.suggestions) > 0
|
|
154
158
|
|
|
155
159
|
@pre(lambda self, min_confidence: min_confidence in Confidence)
|
invar/core/property_gen.py
CHANGED
|
@@ -368,6 +368,7 @@ def _skip_result(name: str, reason: str) -> PropertyTestResult:
|
|
|
368
368
|
_SKIP_PATTERNS = (
|
|
369
369
|
"Nothing", "NoSuchExample", "filter_too_much", "Could not resolve",
|
|
370
370
|
"validation error", "missing", "positional argument", "Unable to satisfy",
|
|
371
|
+
"has no attribute 'check'", # invar_runtime contracts, not deal contracts
|
|
371
372
|
)
|
|
372
373
|
|
|
373
374
|
|
|
@@ -377,6 +378,9 @@ def _handle_test_exception(
|
|
|
377
378
|
err_str: str, func_name: str, max_examples: int
|
|
378
379
|
) -> PropertyTestResult:
|
|
379
380
|
"""Handle exception from property test, returning skip or failure result."""
|
|
381
|
+
# Check for invar_runtime contracts (deal.cases requires deal contracts)
|
|
382
|
+
if "has no attribute 'check'" in err_str:
|
|
383
|
+
return _skip_result(func_name, "Skipped: uses invar_runtime (deal.cases requires deal contracts)")
|
|
380
384
|
if any(p in err_str for p in _SKIP_PATTERNS):
|
|
381
385
|
return _skip_result(func_name, "Skipped: untestable types")
|
|
382
386
|
seed = _extract_hypothesis_seed(err_str)
|