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
@@ -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)