invar-tools 1.8.0__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 (110) 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/ts_parsers.py +286 -0
  16. invar/core/ts_sig_parser.py +307 -0
  17. invar/node_tools/MANIFEST +7 -0
  18. invar/node_tools/__init__.py +51 -0
  19. invar/node_tools/fc-runner/cli.js +77 -0
  20. invar/node_tools/quick-check/cli.js +28 -0
  21. invar/node_tools/ts-analyzer/cli.js +480 -0
  22. invar/shell/claude_hooks.py +35 -12
  23. invar/shell/commands/guard.py +36 -1
  24. invar/shell/commands/init.py +82 -3
  25. invar/shell/commands/perception.py +157 -33
  26. invar/shell/commands/skill.py +187 -0
  27. invar/shell/commands/template_sync.py +65 -13
  28. invar/shell/commands/uninstall.py +60 -12
  29. invar/shell/commands/update.py +6 -14
  30. invar/shell/contract_coverage.py +1 -0
  31. invar/shell/fs.py +66 -13
  32. invar/shell/pi_hooks.py +6 -0
  33. invar/shell/prove/guard_ts.py +899 -0
  34. invar/shell/skill_manager.py +353 -0
  35. invar/shell/template_engine.py +28 -4
  36. invar/shell/templates.py +4 -4
  37. invar/templates/claude-md/python/critical-rules.md +33 -0
  38. invar/templates/claude-md/python/quick-reference.md +24 -0
  39. invar/templates/claude-md/typescript/critical-rules.md +40 -0
  40. invar/templates/claude-md/typescript/quick-reference.md +24 -0
  41. invar/templates/claude-md/universal/check-in.md +25 -0
  42. invar/templates/claude-md/universal/skills.md +73 -0
  43. invar/templates/claude-md/universal/workflow.md +55 -0
  44. invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
  45. invar/templates/config/AGENT.md.jinja +58 -0
  46. invar/templates/config/CLAUDE.md.jinja +16 -209
  47. invar/templates/config/context.md.jinja +19 -0
  48. invar/templates/examples/{README.md → python/README.md} +2 -0
  49. invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
  50. invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
  51. invar/templates/examples/python/core_shell.py +227 -0
  52. invar/templates/examples/python/functional.py +613 -0
  53. invar/templates/examples/typescript/README.md +31 -0
  54. invar/templates/examples/typescript/contracts.ts +163 -0
  55. invar/templates/examples/typescript/core_shell.ts +374 -0
  56. invar/templates/examples/typescript/functional.ts +601 -0
  57. invar/templates/examples/typescript/workflow.md +95 -0
  58. invar/templates/hooks/PostToolUse.sh.jinja +10 -1
  59. invar/templates/hooks/PreToolUse.sh.jinja +38 -0
  60. invar/templates/hooks/Stop.sh.jinja +1 -1
  61. invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
  62. invar/templates/hooks/pi/invar.ts.jinja +9 -0
  63. invar/templates/manifest.toml +7 -6
  64. invar/templates/onboard/assessment.md.jinja +214 -0
  65. invar/templates/onboard/patterns/python.md +347 -0
  66. invar/templates/onboard/patterns/typescript.md +452 -0
  67. invar/templates/onboard/roadmap.md.jinja +168 -0
  68. invar/templates/protocol/INVAR.md.jinja +51 -0
  69. invar/templates/protocol/python/architecture-examples.md +41 -0
  70. invar/templates/protocol/python/contracts-syntax.md +56 -0
  71. invar/templates/protocol/python/markers.md +44 -0
  72. invar/templates/protocol/python/tools.md +24 -0
  73. invar/templates/protocol/python/troubleshooting.md +38 -0
  74. invar/templates/protocol/typescript/architecture-examples.md +52 -0
  75. invar/templates/protocol/typescript/contracts-syntax.md +73 -0
  76. invar/templates/protocol/typescript/markers.md +48 -0
  77. invar/templates/protocol/typescript/tools.md +65 -0
  78. invar/templates/protocol/typescript/troubleshooting.md +104 -0
  79. invar/templates/protocol/universal/architecture.md +36 -0
  80. invar/templates/protocol/universal/completion.md +14 -0
  81. invar/templates/protocol/universal/contracts-concept.md +37 -0
  82. invar/templates/protocol/universal/header.md +17 -0
  83. invar/templates/protocol/universal/session.md +17 -0
  84. invar/templates/protocol/universal/six-laws.md +10 -0
  85. invar/templates/protocol/universal/usbv.md +14 -0
  86. invar/templates/protocol/universal/visible-workflow.md +25 -0
  87. invar/templates/skills/develop/SKILL.md.jinja +39 -3
  88. invar/templates/skills/extensions/_registry.yaml +93 -0
  89. invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
  90. invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
  91. invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
  92. invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
  93. invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
  94. invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
  95. invar/templates/skills/extensions/security/SKILL.md +382 -0
  96. invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
  97. invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
  98. invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
  99. invar/templates/skills/review/SKILL.md.jinja +331 -71
  100. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/METADATA +304 -12
  101. invar_tools-1.10.0.dist-info/RECORD +173 -0
  102. invar/templates/examples/core_shell.py +0 -127
  103. invar/templates/protocol/INVAR.md +0 -310
  104. invar_tools-1.8.0.dist-info/RECORD +0 -116
  105. /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
  106. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
  107. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
  108. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
  109. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
  110. {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,307 @@
1
+ """TypeScript Signature Extraction (Core).
2
+
3
+ Pure logic for extracting function/class signatures from TypeScript source.
4
+ Part of LX-06 TypeScript tooling support.
5
+
6
+ Note: This is a regex-based MVP. Phase 2 can upgrade to tree-sitter for
7
+ more robust parsing.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass
14
+ from typing import Literal
15
+
16
+ from deal import post, pre
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class TSSymbol:
21
+ """Represents a TypeScript symbol (function, class, interface, type)."""
22
+
23
+ name: str
24
+ kind: Literal["function", "class", "interface", "type", "const", "method"]
25
+ signature: str
26
+ line: int
27
+ docstring: str | None = None
28
+
29
+
30
+ # Regex patterns for TypeScript constructs
31
+ # Note: These are simplified patterns suitable for common cases
32
+ _FUNCTION_PATTERN = re.compile(
33
+ r"^\s*(?:@\w+(?:\([^)]*\))?\s*\n\s*)*" # Optional decorators
34
+ r"(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*"
35
+ r"(<[^>]*>)?" # Optional generics
36
+ r"\(([^)]*)\)" # Parameters
37
+ r"(?:\s*:\s*([^\n{]+))?" # Optional return type
38
+ r"\s*\{",
39
+ re.MULTILINE,
40
+ )
41
+
42
+ _ARROW_FUNCTION_PATTERN = re.compile(
43
+ r"^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*"
44
+ r"(?::\s*[^=]+)?\s*=\s*"
45
+ r"(?:async\s+)?\([^)]*\)\s*"
46
+ r"(?::\s*[^\n=>]+)?\s*=>",
47
+ re.MULTILINE,
48
+ )
49
+
50
+ _CLASS_PATTERN = re.compile(
51
+ r"^\s*(?:@\w+(?:\([^)]*\))?\s*\n\s*)*" # Optional decorators
52
+ r"(?:export\s+)?(?:abstract\s+)?class\s+(\w+)"
53
+ r"(?:<[^{]*>)?" # Optional generics (allow nested <> by stopping at {)
54
+ r"(?:\s+extends\s+[^\s{]+)?" # Optional extends
55
+ r"(?:\s+implements\s+[^\s{]+)?" # Optional implements
56
+ r"\s*\{",
57
+ re.MULTILINE,
58
+ )
59
+
60
+ _INTERFACE_PATTERN = re.compile(
61
+ r"^\s*(?:export\s+)?interface\s+(\w+)"
62
+ r"(?:<[^{]*>)?" # Optional generics (allow nested <> by stopping at {)
63
+ r"(?:\s+extends\s+[^\s{]+)?" # Optional extends
64
+ r"\s*\{",
65
+ re.MULTILINE,
66
+ )
67
+
68
+ _TYPE_ALIAS_PATTERN = re.compile(
69
+ r"^\s*(?:export\s+)?type\s+(\w+)"
70
+ r"(?:<[^=]*>)?" # Optional generics (allow nested <> by stopping at =)
71
+ r"\s*=",
72
+ re.MULTILINE,
73
+ )
74
+
75
+ _JSDOC_PATTERN = re.compile(
76
+ r"/\*\*\s*(.*?)\s*\*/",
77
+ re.DOTALL,
78
+ )
79
+
80
+
81
+ # @invar:allow function_size: Regex extraction inherently repetitive per TS construct type
82
+ @pre(lambda source: source is not None) # Accepts any string including empty
83
+ @post(lambda result: all(s.line > 0 for s in result)) # All symbols have valid line numbers
84
+ def extract_ts_signatures(source: str) -> list[TSSymbol]:
85
+ """Extract TypeScript symbols from source code.
86
+
87
+ Args:
88
+ source: TypeScript source code.
89
+
90
+ Returns:
91
+ List of TSSymbol objects representing functions, classes, etc.
92
+
93
+ >>> code = '''
94
+ ... function greet(name: string): string {
95
+ ... return `Hello, ${name}`;
96
+ ... }
97
+ ... '''
98
+ >>> symbols = extract_ts_signatures(code)
99
+ >>> len(symbols)
100
+ 1
101
+ >>> symbols[0].name
102
+ 'greet'
103
+ >>> symbols[0].kind
104
+ 'function'
105
+
106
+ >>> code2 = '''
107
+ ... export class User {
108
+ ... constructor(public name: string) {}
109
+ ... }
110
+ ... '''
111
+ >>> symbols2 = extract_ts_signatures(code2)
112
+ >>> symbols2[0].name
113
+ 'User'
114
+ >>> symbols2[0].kind
115
+ 'class'
116
+ """
117
+ # Early return for empty source (CrossHair-friendly)
118
+ if not source:
119
+ return []
120
+
121
+ symbols: list[TSSymbol] = []
122
+ lines = source.split("\n")
123
+
124
+ # Find JSDoc comments for association
125
+ jsdoc_positions: dict[int, str] = {}
126
+ for match in _JSDOC_PATTERN.finditer(source):
127
+ # Find line number of end of JSDoc
128
+ end_pos = match.end()
129
+ line_num = source[:end_pos].count("\n")
130
+ jsdoc_positions[line_num] = match.group(1).strip()
131
+
132
+ def get_jsdoc(line: int) -> str | None:
133
+ """Get JSDoc for a symbol at given line."""
134
+ # Check previous lines for JSDoc
135
+ for i in range(max(0, line - 5), line):
136
+ if i in jsdoc_positions:
137
+ return jsdoc_positions[i]
138
+ return None
139
+
140
+ def get_line_number(pos: int) -> int:
141
+ """Convert character position to line number (1-indexed)."""
142
+ return source[:pos].count("\n") + 1
143
+
144
+ # Extract functions
145
+ for match in _FUNCTION_PATTERN.finditer(source):
146
+ name = match.group(1)
147
+ # Find actual function line (skip decorators)
148
+ match_text = match.group(0)
149
+ func_keyword_pos = match_text.find("function ")
150
+ actual_start = match.start() + func_keyword_pos if func_keyword_pos >= 0 else match.start()
151
+ line = get_line_number(actual_start)
152
+ # Use line content for signature (handles complex types better than regex groups)
153
+ line_content = lines[line - 1].strip() if line <= len(lines) else ""
154
+ signature = line_content.rstrip("{").rstrip()
155
+ symbols.append(
156
+ TSSymbol(
157
+ name=name,
158
+ kind="function",
159
+ signature=signature,
160
+ line=line,
161
+ docstring=get_jsdoc(line - 1),
162
+ )
163
+ )
164
+
165
+ # Extract arrow functions (const declarations)
166
+ for match in _ARROW_FUNCTION_PATTERN.finditer(source):
167
+ name = match.group(1)
168
+ line = get_line_number(match.start())
169
+ # For arrow functions, extract the full line as signature approximation
170
+ line_content = lines[line - 1].strip() if line <= len(lines) else ""
171
+ signature = line_content.rstrip("{").rstrip()
172
+ symbols.append(
173
+ TSSymbol(
174
+ name=name,
175
+ kind="const",
176
+ signature=signature,
177
+ line=line,
178
+ docstring=get_jsdoc(line - 1),
179
+ )
180
+ )
181
+
182
+ # Extract classes
183
+ for match in _CLASS_PATTERN.finditer(source):
184
+ name = match.group(1)
185
+ # Find actual class line (skip decorators)
186
+ match_text = match.group(0)
187
+ class_keyword_pos = match_text.find("class ")
188
+ actual_start = match.start() + class_keyword_pos if class_keyword_pos >= 0 else match.start()
189
+ line = get_line_number(actual_start)
190
+ line_content = lines[line - 1].strip() if line <= len(lines) else ""
191
+ signature = line_content.rstrip("{").rstrip()
192
+ symbols.append(
193
+ TSSymbol(
194
+ name=name,
195
+ kind="class",
196
+ signature=signature,
197
+ line=line,
198
+ docstring=get_jsdoc(line - 1),
199
+ )
200
+ )
201
+
202
+ # Extract interfaces
203
+ for match in _INTERFACE_PATTERN.finditer(source):
204
+ name = match.group(1)
205
+ line = get_line_number(match.start())
206
+ line_content = lines[line - 1].strip() if line <= len(lines) else ""
207
+ signature = line_content.rstrip("{").rstrip()
208
+ symbols.append(
209
+ TSSymbol(
210
+ name=name,
211
+ kind="interface",
212
+ signature=signature,
213
+ line=line,
214
+ docstring=get_jsdoc(line - 1),
215
+ )
216
+ )
217
+
218
+ # Extract type aliases
219
+ for match in _TYPE_ALIAS_PATTERN.finditer(source):
220
+ name = match.group(1)
221
+ line = get_line_number(match.start())
222
+ line_content = lines[line - 1].strip() if line <= len(lines) else ""
223
+ # Include the full type definition
224
+ signature = line_content.rstrip(";").strip()
225
+ symbols.append(
226
+ TSSymbol(
227
+ name=name,
228
+ kind="type",
229
+ signature=signature,
230
+ line=line,
231
+ docstring=get_jsdoc(line - 1),
232
+ )
233
+ )
234
+
235
+ # Sort by line number
236
+ symbols.sort(key=lambda s: s.line)
237
+
238
+ return symbols
239
+
240
+
241
+ @pre(lambda symbols, file_path="": all(s.line > 0 for s in symbols)) # All symbols have valid line numbers
242
+ @post(lambda result: "file" in result and "symbols" in result)
243
+ def format_ts_signatures_json(
244
+ symbols: list[TSSymbol], file_path: str = ""
245
+ ) -> dict:
246
+ """Format TypeScript symbols as JSON output.
247
+
248
+ Args:
249
+ symbols: List of TSSymbol objects.
250
+ file_path: Source file path.
251
+
252
+ Returns:
253
+ JSON-serializable dictionary.
254
+
255
+ >>> symbols = [TSSymbol("foo", "function", "function foo(): void", 1)]
256
+ >>> result = format_ts_signatures_json(symbols, "test.ts")
257
+ >>> result["file"]
258
+ 'test.ts'
259
+ >>> len(result["symbols"])
260
+ 1
261
+ """
262
+ return {
263
+ "file": file_path,
264
+ "symbols": [
265
+ {
266
+ "name": s.name,
267
+ "kind": s.kind,
268
+ "signature": s.signature,
269
+ "line": s.line,
270
+ "docstring": s.docstring,
271
+ }
272
+ for s in symbols
273
+ ],
274
+ }
275
+
276
+
277
+ @pre(lambda symbols, file_path="": all(s.line > 0 for s in symbols)) # All symbols have valid line numbers
278
+ @post(lambda result: len(result) > 0) # Always produces output (at least header)
279
+ def format_ts_signatures_text(
280
+ symbols: list[TSSymbol], file_path: str = ""
281
+ ) -> str:
282
+ """Format TypeScript symbols as human-readable text.
283
+
284
+ Args:
285
+ symbols: List of TSSymbol objects.
286
+ file_path: Source file path.
287
+
288
+ Returns:
289
+ Formatted text output.
290
+
291
+ >>> symbols = [TSSymbol("foo", "function", "function foo(): void", 1)]
292
+ >>> text = format_ts_signatures_text(symbols, "test.ts")
293
+ >>> "foo" in text
294
+ True
295
+ """
296
+ lines = [f"# {file_path}" if file_path else "# TypeScript Signatures", ""]
297
+
298
+ for symbol in symbols:
299
+ lines.append(f"[{symbol.kind}] {symbol.name} (line {symbol.line})")
300
+ lines.append(f" {symbol.signature}")
301
+ if symbol.docstring:
302
+ # Truncate long docstrings
303
+ doc = symbol.docstring[:100] + "..." if len(symbol.docstring) > 100 else symbol.docstring
304
+ lines.append(f" /** {doc} */")
305
+ lines.append("")
306
+
307
+ return "\n".join(lines)
@@ -0,0 +1,7 @@
1
+ # Embedded Node.js tools
2
+ # Auto-generated by scripts/embed_node_tools.py
3
+ # Do not edit manually
4
+
5
+ fc-runner
6
+ quick-check
7
+ ts-analyzer
@@ -0,0 +1,51 @@
1
+ """Embedded Node.js tools for TypeScript verification.
2
+
3
+ This module provides access to @invar/* Node tools that are embedded
4
+ in the Python package for zero-configuration TypeScript support.
5
+
6
+ The tools are built from typescript/packages/* and copied here during
7
+ the Python package build process. Users who install invar-tools via pip
8
+ get these tools automatically without needing npm.
9
+
10
+ Workflow:
11
+ 1. Developer: typescript/packages/*/src/ -> pnpm build -> dist/
12
+ 2. Release: scripts/embed_node_tools.py copies dist/ -> src/invar/node_tools/
13
+ 3. User: pip install invar-tools (includes node_tools/)
14
+ """
15
+
16
+ from pathlib import Path
17
+
18
+
19
+ # @invar:allow shell_result: Simple path lookup, None is clear return value
20
+ def get_tool_path(tool_name: str) -> Path | None:
21
+ """Get path to an embedded Node tool.
22
+
23
+ Args:
24
+ tool_name: Tool name without @invar/ prefix (e.g., "ts-analyzer")
25
+
26
+ Returns:
27
+ Path to cli.js if embedded, None otherwise.
28
+
29
+ Examples:
30
+ >>> # When tools are embedded (after running embed script)
31
+ >>> path = get_tool_path("ts-analyzer")
32
+ >>> # Returns: Path(".../node_tools/ts-analyzer/cli.js") or None
33
+ """
34
+ tool_dir = Path(__file__).parent / tool_name
35
+ cli_js = tool_dir / "cli.js"
36
+ return cli_js if cli_js.exists() else None
37
+
38
+
39
+ # @invar:allow shell_result: Directory listing for embedded tools discovery
40
+ def list_available_tools() -> list[str]:
41
+ """List all embedded tools.
42
+
43
+ Returns:
44
+ List of tool names that have cli.js available.
45
+ """
46
+ tools_dir = Path(__file__).parent
47
+ available = []
48
+ for subdir in tools_dir.iterdir():
49
+ if subdir.is_dir() and (subdir / "cli.js").exists():
50
+ available.append(subdir.name)
51
+ return sorted(available)