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/shell/pi_hooks.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pi Coding Agent hooks for Invar.
|
|
3
|
+
|
|
4
|
+
LX-04: Full feature parity with Claude Code hooks.
|
|
5
|
+
- pytest/crosshair blocking via tool_call
|
|
6
|
+
- Protocol injection via pi.send() for long conversations
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from jinja2 import Environment, FileSystemLoader
|
|
17
|
+
from returns.result import Failure, Result, Success
|
|
18
|
+
|
|
19
|
+
from invar.core.language import detect_language_from_markers
|
|
20
|
+
from invar.core.template_helpers import escape_for_js_template
|
|
21
|
+
from invar.shell.claude_hooks import detect_syntax, get_invar_md_content
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
|
|
26
|
+
# Pi hooks directory
|
|
27
|
+
PI_HOOKS_DIR = ".pi/hooks"
|
|
28
|
+
PROTOCOL_VERSION = "5.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_pi_templates_path() -> Path:
|
|
32
|
+
"""Get the path to Pi hook templates."""
|
|
33
|
+
return Path(__file__).parent.parent / "templates" / "hooks" / "pi"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# @shell_complexity: Template rendering with protocol escaping
|
|
37
|
+
def generate_pi_hook_content(project_path: Path) -> Result[str, str]:
|
|
38
|
+
"""Generate Pi hook content from template."""
|
|
39
|
+
templates_path = get_pi_templates_path()
|
|
40
|
+
template_file = "invar.ts.jinja"
|
|
41
|
+
|
|
42
|
+
if not (templates_path / template_file).exists():
|
|
43
|
+
return Failure(f"Template not found: {template_file}")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
env = Environment(
|
|
47
|
+
loader=FileSystemLoader(str(templates_path)),
|
|
48
|
+
keep_trailing_newline=True,
|
|
49
|
+
)
|
|
50
|
+
template = env.get_template(template_file)
|
|
51
|
+
|
|
52
|
+
# Determine guard command based on syntax
|
|
53
|
+
syntax = detect_syntax(project_path)
|
|
54
|
+
guard_cmd = "invar_guard" if syntax == "mcp" else "invar guard"
|
|
55
|
+
|
|
56
|
+
# Detect project language from marker files
|
|
57
|
+
markers = frozenset(f.name for f in project_path.iterdir() if f.is_file())
|
|
58
|
+
language = detect_language_from_markers(markers)
|
|
59
|
+
|
|
60
|
+
# Get and escape protocol content for JS template literal
|
|
61
|
+
protocol_content = get_invar_md_content(project_path)
|
|
62
|
+
protocol_escaped = escape_for_js_template(protocol_content)
|
|
63
|
+
|
|
64
|
+
# Build context for template
|
|
65
|
+
context = {
|
|
66
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
67
|
+
"generated_date": datetime.now().strftime("%Y-%m-%d"),
|
|
68
|
+
"guard_cmd": guard_cmd,
|
|
69
|
+
"language": language,
|
|
70
|
+
"invar_protocol_escaped": protocol_escaped,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
content = template.render(**context)
|
|
74
|
+
return Success(content)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return Failure(f"Failed to generate Pi hook: {e}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def install_pi_hooks(
|
|
80
|
+
project_path: Path,
|
|
81
|
+
console: Console,
|
|
82
|
+
) -> Result[list[str], str]:
|
|
83
|
+
"""
|
|
84
|
+
Install Pi hooks for Invar.
|
|
85
|
+
|
|
86
|
+
Creates .pi/hooks/invar.ts with:
|
|
87
|
+
- pytest/crosshair blocking
|
|
88
|
+
- Protocol injection for long conversations
|
|
89
|
+
"""
|
|
90
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
91
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
console.print("\n[bold]Installing Pi hooks (LX-04)...[/bold]")
|
|
94
|
+
console.print(" Hooks will:")
|
|
95
|
+
console.print(" ✓ Block pytest/crosshair → redirect to invar guard")
|
|
96
|
+
console.print(" ✓ Refresh protocol in long conversations")
|
|
97
|
+
console.print("")
|
|
98
|
+
|
|
99
|
+
result = generate_pi_hook_content(project_path)
|
|
100
|
+
if isinstance(result, Failure):
|
|
101
|
+
console.print(f" [red]Failed:[/red] {result.failure()}")
|
|
102
|
+
return Failure(result.failure())
|
|
103
|
+
|
|
104
|
+
content = result.unwrap()
|
|
105
|
+
hook_file = hooks_dir / "invar.ts"
|
|
106
|
+
hook_file.write_text(content)
|
|
107
|
+
|
|
108
|
+
console.print(f" [green]Created[/green] {PI_HOOKS_DIR}/invar.ts")
|
|
109
|
+
console.print("\n [bold green]✓ Pi hooks installed[/bold green]")
|
|
110
|
+
console.print(" [dim]Requires: Pi coding agent with hooks support[/dim]")
|
|
111
|
+
console.print(" [yellow]⚠ Restart Pi session for hooks to take effect[/yellow]")
|
|
112
|
+
|
|
113
|
+
return Success(["invar.ts"])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# @shell_complexity: Version detection and conditional update logic
|
|
117
|
+
def sync_pi_hooks(
|
|
118
|
+
project_path: Path,
|
|
119
|
+
console: Console,
|
|
120
|
+
) -> Result[list[str], str]:
|
|
121
|
+
"""
|
|
122
|
+
Update Pi hooks with current INVAR.md content.
|
|
123
|
+
|
|
124
|
+
Called during `invar init` to ensure hooks stay in sync with protocol.
|
|
125
|
+
Only updates if Pi hooks are already installed.
|
|
126
|
+
"""
|
|
127
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
128
|
+
hook_file = hooks_dir / "invar.ts"
|
|
129
|
+
|
|
130
|
+
if not hook_file.exists():
|
|
131
|
+
return Success([]) # No hooks installed, nothing to sync
|
|
132
|
+
|
|
133
|
+
# Check version in existing hook
|
|
134
|
+
try:
|
|
135
|
+
existing_content = hook_file.read_text()
|
|
136
|
+
version_match = re.search(r"Protocol: v([\d.]+)", existing_content)
|
|
137
|
+
old_version = version_match.group(1) if version_match else "unknown"
|
|
138
|
+
|
|
139
|
+
if old_version != PROTOCOL_VERSION:
|
|
140
|
+
console.print(f"[cyan]Updating Pi hooks: v{old_version} → v{PROTOCOL_VERSION}[/cyan]")
|
|
141
|
+
else:
|
|
142
|
+
console.print("[dim]Refreshing Pi hooks...[/dim]")
|
|
143
|
+
except OSError:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
result = generate_pi_hook_content(project_path)
|
|
147
|
+
if isinstance(result, Failure):
|
|
148
|
+
console.print(f" [yellow]Warning:[/yellow] Failed to generate Pi hook: {result.failure()}")
|
|
149
|
+
return Failure(result.failure())
|
|
150
|
+
|
|
151
|
+
content = result.unwrap()
|
|
152
|
+
hook_file.write_text(content)
|
|
153
|
+
console.print("[green]✓[/green] Pi hooks synced")
|
|
154
|
+
|
|
155
|
+
return Success(["invar.ts"])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def remove_pi_hooks(
|
|
159
|
+
project_path: Path,
|
|
160
|
+
console: Console,
|
|
161
|
+
) -> Result[None, str]:
|
|
162
|
+
"""Remove Pi hooks."""
|
|
163
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
164
|
+
hook_file = hooks_dir / "invar.ts"
|
|
165
|
+
|
|
166
|
+
if hook_file.exists():
|
|
167
|
+
hook_file.unlink()
|
|
168
|
+
console.print(f" [red]Removed[/red] {PI_HOOKS_DIR}/invar.ts")
|
|
169
|
+
|
|
170
|
+
# Remove directory if empty
|
|
171
|
+
try:
|
|
172
|
+
hooks_dir.rmdir()
|
|
173
|
+
console.print(f" [red]Removed[/red] {PI_HOOKS_DIR}/")
|
|
174
|
+
except OSError:
|
|
175
|
+
pass # Directory not empty, keep it
|
|
176
|
+
|
|
177
|
+
console.print("[bold green]✓ Pi hooks removed[/bold green]")
|
|
178
|
+
else:
|
|
179
|
+
console.print("[dim]No Pi hooks installed[/dim]")
|
|
180
|
+
|
|
181
|
+
return Success(None)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def pi_hooks_status(
|
|
185
|
+
project_path: Path,
|
|
186
|
+
console: Console,
|
|
187
|
+
) -> Result[dict[str, str], str]:
|
|
188
|
+
"""Check status of Pi hooks."""
|
|
189
|
+
hooks_dir = project_path / PI_HOOKS_DIR
|
|
190
|
+
hook_file = hooks_dir / "invar.ts"
|
|
191
|
+
|
|
192
|
+
status: dict[str, str] = {}
|
|
193
|
+
|
|
194
|
+
if not hook_file.exists():
|
|
195
|
+
console.print("[dim]No Pi hooks installed[/dim]")
|
|
196
|
+
return Success({"status": "not_installed"})
|
|
197
|
+
|
|
198
|
+
status["status"] = "installed"
|
|
199
|
+
|
|
200
|
+
# Try to get version
|
|
201
|
+
try:
|
|
202
|
+
content = hook_file.read_text()
|
|
203
|
+
match = re.search(r"Protocol: v([\d.]+)", content)
|
|
204
|
+
if match:
|
|
205
|
+
version = match.group(1)
|
|
206
|
+
status["version"] = version
|
|
207
|
+
console.print(f"[green]✓ Pi hooks installed (v{version})[/green]")
|
|
208
|
+
else:
|
|
209
|
+
console.print("[green]✓ Pi hooks installed[/green]")
|
|
210
|
+
except OSError:
|
|
211
|
+
console.print("[green]✓ Pi hooks installed[/green]")
|
|
212
|
+
|
|
213
|
+
return Success(status)
|