invar-tools 1.11.0__py3-none-any.whl → 1.14.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.
@@ -0,0 +1,238 @@
1
+ """TypeScript Compiler API wrapper (single-shot subprocess).
2
+
3
+ DX-78: Provides Python interface to ts-query.js for TypeScript analysis.
4
+
5
+ Architecture:
6
+ - Single-shot subprocess: starts, runs query, exits
7
+ - No persistent process, no orphan risk
8
+ - Falls back to regex parser if Node.js unavailable
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from returns.result import Failure, Result, Success
20
+
21
+
22
+ @dataclass
23
+ class TSSymbolInfo:
24
+ """TypeScript symbol information from Compiler API."""
25
+
26
+ name: str
27
+ kind: str
28
+ signature: str
29
+ line: int
30
+ file: str = ""
31
+ contracts: dict[str, list[str]] | None = None
32
+ members: list[dict[str, Any]] | None = None
33
+
34
+
35
+ @dataclass
36
+ class TSReference:
37
+ """A reference to a TypeScript symbol."""
38
+
39
+ file: str
40
+ line: int
41
+ column: int
42
+ context: str
43
+ is_definition: bool = False
44
+
45
+
46
+ def _find_ts_query_js() -> Path:
47
+ """Find the ts-query.js script bundled with invar-tools."""
48
+ # Look relative to this file's location
49
+ this_dir = Path(__file__).parent.parent
50
+ ts_query_path = this_dir / "node_tools" / "ts-query.js"
51
+
52
+ if ts_query_path.exists():
53
+ return ts_query_path
54
+
55
+ # Fallback: check if installed globally or in node_modules
56
+ raise FileNotFoundError("ts-query.js not found")
57
+
58
+
59
+ # @shell_complexity: Project root discovery with parent traversal
60
+ def _find_tsconfig_root(file_path: Path) -> Path:
61
+ """Find the project root containing tsconfig.json."""
62
+ current = file_path.parent if file_path.is_file() else file_path
63
+
64
+ while current != current.parent:
65
+ if (current / "tsconfig.json").exists():
66
+ return current
67
+ current = current.parent
68
+
69
+ # Fallback to file's directory
70
+ return file_path.parent if file_path.is_file() else file_path
71
+
72
+
73
+ # @shell_complexity: Subprocess orchestration with error handling and JSON parsing
74
+ def query_typescript(
75
+ project_root: Path,
76
+ command: str,
77
+ **params: Any,
78
+ ) -> Result[dict[str, Any], str]:
79
+ """Run ts-query.js and return parsed result.
80
+
81
+ Single-shot subprocess: starts, runs query, exits.
82
+ No persistent process, no orphan risk.
83
+
84
+ Args:
85
+ project_root: Project root containing tsconfig.json
86
+ command: Query command (sig, map, refs)
87
+ **params: Command-specific parameters
88
+
89
+ Returns:
90
+ Parsed JSON result or error message
91
+ """
92
+ try:
93
+ ts_query_path = _find_ts_query_js()
94
+ except FileNotFoundError:
95
+ return Failure("ts-query.js not found. Install Node.js to use TypeScript tools.")
96
+
97
+ query = {"command": command, **params}
98
+
99
+ try:
100
+ result = subprocess.run(
101
+ ["node", str(ts_query_path), json.dumps(query)],
102
+ cwd=str(project_root),
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=30, # Safety timeout
106
+ )
107
+
108
+ if result.returncode != 0:
109
+ error_msg = result.stderr.strip() if result.stderr else "Unknown error"
110
+ return Failure(f"ts-query failed: {error_msg}")
111
+
112
+ output = json.loads(result.stdout)
113
+
114
+ # Check for error in output
115
+ if "error" in output:
116
+ return Failure(output["error"])
117
+
118
+ return Success(output)
119
+
120
+ except subprocess.TimeoutExpired:
121
+ return Failure("TypeScript query timed out (30s)")
122
+ except FileNotFoundError:
123
+ return Failure(
124
+ "Node.js not found.\n\n"
125
+ "To use TypeScript tools, install Node.js:\n"
126
+ "- macOS: brew install node\n"
127
+ "- Ubuntu: apt install nodejs\n"
128
+ "- Windows: https://nodejs.org/"
129
+ )
130
+ except json.JSONDecodeError as e:
131
+ return Failure(f"Invalid JSON from ts-query: {e}")
132
+ except Exception as e:
133
+ return Failure(f"TypeScript query error: {e}")
134
+
135
+
136
+ def run_sig_typescript(file_path: Path) -> Result[list[TSSymbolInfo], str]:
137
+ """Get signatures for TypeScript file using Compiler API.
138
+
139
+ Args:
140
+ file_path: Path to TypeScript file
141
+
142
+ Returns:
143
+ List of symbol information or error message
144
+ """
145
+ project_root = _find_tsconfig_root(file_path)
146
+ result = query_typescript(project_root, "sig", file=str(file_path))
147
+
148
+ if isinstance(result, Failure):
149
+ return result
150
+
151
+ data = result.unwrap()
152
+ symbols = []
153
+
154
+ for sym in data.get("symbols", []):
155
+ symbols.append(
156
+ TSSymbolInfo(
157
+ name=sym.get("name", ""),
158
+ kind=sym.get("kind", ""),
159
+ signature=sym.get("signature", ""),
160
+ line=sym.get("line", 0),
161
+ file=str(file_path),
162
+ contracts=sym.get("contracts"),
163
+ members=sym.get("members"),
164
+ )
165
+ )
166
+
167
+ return Success(symbols)
168
+
169
+
170
+ def run_map_typescript(path: Path, top_n: int) -> Result[dict[str, Any], str]:
171
+ """Get symbol map with reference counts for TypeScript project.
172
+
173
+ Args:
174
+ path: Project path to scan
175
+ top_n: Maximum number of symbols to return
176
+
177
+ Returns:
178
+ Symbol map data or error message
179
+ """
180
+ return query_typescript(path, "map", path=str(path), top=top_n)
181
+
182
+
183
+ def run_refs_typescript(
184
+ file_path: Path, line: int, column: int
185
+ ) -> Result[list[TSReference], str]:
186
+ """Find all references to symbol at position.
187
+
188
+ Args:
189
+ file_path: File containing the symbol
190
+ line: 1-based line number
191
+ column: 0-based column number
192
+
193
+ Returns:
194
+ List of references or error message
195
+ """
196
+ project_root = _find_tsconfig_root(file_path)
197
+ result = query_typescript(
198
+ project_root,
199
+ "refs",
200
+ file=str(file_path),
201
+ line=line,
202
+ column=column,
203
+ )
204
+
205
+ if isinstance(result, Failure):
206
+ return result
207
+
208
+ data = result.unwrap()
209
+ references = []
210
+
211
+ for ref in data.get("references", []):
212
+ references.append(
213
+ TSReference(
214
+ file=ref.get("file", ""),
215
+ line=ref.get("line", 0),
216
+ column=ref.get("column", 0),
217
+ context=ref.get("context", ""),
218
+ is_definition=ref.get("isDefinition", False),
219
+ )
220
+ )
221
+
222
+ return Success(references)
223
+
224
+
225
+ def is_typescript_available() -> bool:
226
+ """Check if TypeScript Compiler API tools are available."""
227
+ try:
228
+ _find_ts_query_js()
229
+ # Also check if Node.js is available
230
+ result = subprocess.run(
231
+ ["node", "--version"],
232
+ capture_output=True,
233
+ text=True,
234
+ timeout=5,
235
+ )
236
+ return result.returncode == 0
237
+ except (FileNotFoundError, subprocess.TimeoutExpired):
238
+ return False
@@ -0,0 +1,110 @@
1
+ ### Document Tools (DX-76)
2
+
3
+ | I want to... | Use |
4
+ |--------------|-----|
5
+ | View document structure | `{% if syntax == "mcp" %}invar_doc_toc(file="<file>"){% else %}invar doc toc <file> [--format text]{% endif %}` |
6
+ | Read specific section | `{% if syntax == "mcp" %}invar_doc_read(file="<file>", section="<section>"){% else %}invar doc read <file> <section>{% endif %}` |
7
+ | Search sections by title | `{% if syntax == "mcp" %}invar_doc_find(file="<file>", pattern="<pattern>"){% else %}invar doc find <pattern> <files...>{% endif %}` |
8
+ | Replace section content | `{% if syntax == "mcp" %}invar_doc_replace(file="<file>", section="<section>"){% else %}invar doc replace <file> <section>{% endif %}` |
9
+ | Insert new section | `{% if syntax == "mcp" %}invar_doc_insert(file="<file>", anchor="<anchor>"){% else %}invar doc insert <file> <anchor>{% endif %}` |
10
+ | Delete section | `{% if syntax == "mcp" %}invar_doc_delete(file="<file>", section="<section>"){% else %}invar doc delete <file> <section>{% endif %}` |
11
+
12
+ **Section addressing:** slug path (`requirements/auth`), fuzzy (`auth`), index (`#0/#1`), line (`@48`)
13
+
14
+ ## Tool Selection
15
+
16
+ ### Calling Methods (Priority Order)
17
+
18
+ Invar tools can be called in 3 ways. **Try in order:**
19
+
20
+ 1. **MCP tools** (Claude Code with MCP enabled)
21
+ - Direct function calls: `invar_guard()`, `invar_sig()`, etc.
22
+ - No Bash wrapper needed
23
+
24
+ 2. **CLI command** (if `invar` installed in PATH)
25
+ - Via Bash: `invar guard`, `invar sig`, etc.
26
+ - Install: `pip install invar-tools`
27
+
28
+ 3. **uvx fallback** (always available, no install needed)
29
+ - Via Bash: `uvx invar-tools guard`, `uvx invar-tools sig`, etc.
30
+
31
+ ---
32
+
33
+ ### Parameter Reference
34
+
35
+ **guard** - Verify code quality
36
+ ```{% if syntax == "mcp" %}python
37
+ # MCP
38
+ invar_guard() # Check changed files (default)
39
+ invar_guard(changed=False) # Check all files{% else %}bash
40
+ # CLI
41
+ invar guard # Check changed files (default)
42
+ invar guard --all # Check all files{% endif %}
43
+ ```
44
+
45
+ **sig** - Show function signatures and contracts
46
+ ```{% if syntax == "mcp" %}python
47
+ # MCP
48
+ invar_sig(target="src/foo.py"){% else %}bash
49
+ # CLI
50
+ invar sig src/foo.py
51
+ invar sig src/foo.py::function_name{% endif %}
52
+ ```
53
+
54
+ **map** - Find entry points
55
+ ```{% if syntax == "mcp" %}python
56
+ # MCP
57
+ invar_map(path=".", top=10){% else %}bash
58
+ # CLI
59
+ invar map [path] --top 10{% endif %}
60
+ ```
61
+
62
+ **refs** - Find all references to a symbol
63
+ ```{% if syntax == "mcp" %}python
64
+ # MCP
65
+ invar_refs(target="src/foo.py::MyClass"){% else %}bash
66
+ # CLI
67
+ invar refs src/foo.py::MyClass{% endif %}
68
+ ```
69
+
70
+ **doc*** - Document tools
71
+ ```{% if syntax == "mcp" %}python
72
+ # MCP
73
+ invar_doc_toc(file="docs/spec.md")
74
+ invar_doc_read(file="docs/spec.md", section="intro"){% else %}bash
75
+ # CLI
76
+ invar doc toc docs/spec.md
77
+ invar doc read docs/spec.md intro{% endif %}
78
+ ```
79
+
80
+ ---
81
+
82
+ ### Quick Examples
83
+
84
+ ```{% if syntax == "mcp" %}python
85
+ # Verify after changes (all three methods identical)
86
+ invar_guard() # MCP
87
+ bash("invar guard") # CLI
88
+ bash("uvx invar-tools guard") # uvx
89
+
90
+ # Full project check
91
+ invar_guard(changed=False) # MCP
92
+ bash("invar guard --all") # CLI
93
+
94
+ # See function contracts
95
+ invar_sig(target="src/core/parser.py")
96
+ bash("invar sig src/core/parser.py"){% else %}bash
97
+ # Verify after changes (all three methods identical)
98
+ invar guard # CLI
99
+ uvx invar-tools guard # uvx
100
+
101
+ # Full project check
102
+ invar guard --all # CLI
103
+ uvx invar-tools guard --all # uvx
104
+
105
+ # See function contracts
106
+ invar sig src/core/parser.py
107
+ uvx invar-tools sig src/core/parser.py{% endif %}
108
+ ```
109
+
110
+ **Note**: All three methods now have identical default behavior.
@@ -23,6 +23,8 @@
23
23
  {% include "claude-md/typescript/quick-reference.md" %}
24
24
  {% endif %}
25
25
 
26
+ {% include "claude-md/universal/tool-selection.md" %}
27
+
26
28
  {% include "claude-md/universal/workflow.md" %}
27
29
 
28
30
  ---
@@ -0,0 +1,193 @@
1
+ # TypeScript Patterns for Agents
2
+
3
+ Reference patterns for AI agents working with TypeScript under Invar Protocol.
4
+
5
+ ## Tool × Feature Matrix
6
+
7
+ | Feature | TypeScript Pattern | Tool Command |
8
+ |---------|-------------------|--------------|
9
+ | Signatures | `function name(params): Return` | `invar sig file.ts` |
10
+ | Contracts | `@pre`, `@post` JSDoc + Zod | `invar sig file.ts` |
11
+ | References | Cross-file symbol usage | `invar refs file.ts::Symbol` |
12
+ | Verification | tsc + eslint + vitest | `invar guard` |
13
+ | Document nav | Markdown structure | `invar doc toc file.md` |
14
+
15
+ ---
16
+
17
+ ## Pattern 1: Preconditions with Zod
18
+
19
+ ```typescript
20
+ import { z } from 'zod';
21
+
22
+ // Schema IS the precondition
23
+ const UserInputSchema = z.object({
24
+ email: z.string().email(),
25
+ age: z.number().int().positive().max(150),
26
+ });
27
+
28
+ type UserInput = z.infer<typeof UserInputSchema>;
29
+
30
+ /**
31
+ * Create a user with validated input.
32
+ * @pre UserInputSchema.parse(input) succeeds
33
+ * @post result.id is set
34
+ */
35
+ function createUser(input: UserInput): User {
36
+ // Zod already validated - safe to use
37
+ return { id: generateId(), ...input };
38
+ }
39
+ ```
40
+
41
+ **Agent workflow:**
42
+ 1. Define Zod schema FIRST (the @pre)
43
+ 2. Derive TypeScript type from schema
44
+ 3. Implement function body
45
+ 4. Zod validates at runtime
46
+
47
+ ---
48
+
49
+ ## Pattern 2: Postconditions
50
+
51
+ ```typescript
52
+ /**
53
+ * Calculate discount price.
54
+ * @pre price > 0 && discount >= 0 && discount <= 1
55
+ * @post result >= 0 && result <= price
56
+ */
57
+ function applyDiscount(price: number, discount: number): number {
58
+ const result = price * (1 - discount);
59
+
60
+ // Postcondition check (development only)
61
+ console.assert(result >= 0 && result <= price,
62
+ `Postcondition failed: ${result}`);
63
+
64
+ return result;
65
+ }
66
+ ```
67
+
68
+ **Note:** Unlike Python's `@post` decorator, TypeScript postconditions
69
+ are documented in JSDoc and checked manually or via assertion.
70
+
71
+ ---
72
+
73
+ ## Pattern 3: Core/Shell Separation
74
+
75
+ ```typescript
76
+ // ─── Core (Pure) ───
77
+ // No I/O, no side effects, only data transformations
78
+
79
+ function validateEmail(email: string): boolean {
80
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
81
+ }
82
+
83
+ function calculateTotal(items: CartItem[]): number {
84
+ return items.reduce((sum, item) => sum + item.price * item.qty, 0);
85
+ }
86
+
87
+ // ─── Shell (I/O) ───
88
+ // All external interactions, returns Result<T, E>
89
+
90
+ import { Result, ok, err } from 'neverthrow';
91
+
92
+ async function fetchUser(id: string): Promise<Result<User, ApiError>> {
93
+ try {
94
+ const response = await fetch(`/api/users/${id}`);
95
+ if (!response.ok) return err({ code: response.status });
96
+ return ok(await response.json());
97
+ } catch (e) {
98
+ return err({ code: 500, message: String(e) });
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Agent checklist:**
104
+ - [ ] Core functions: No imports from `fs`, `http`, `fetch`, etc.
105
+ - [ ] Shell functions: Return `Result<T, E>` for fallible operations
106
+ - [ ] Dependency injection: Pass data to Core, not paths
107
+
108
+ ---
109
+
110
+ ## Pattern 4: Exhaustive Switch
111
+
112
+ ```typescript
113
+ type Status = 'pending' | 'approved' | 'rejected';
114
+
115
+ function getStatusMessage(status: Status): string {
116
+ switch (status) {
117
+ case 'pending': return 'Waiting for review';
118
+ case 'approved': return 'Request approved';
119
+ case 'rejected': return 'Request denied';
120
+ default:
121
+ // TypeScript ensures this is never reached
122
+ const _exhaustive: never = status;
123
+ throw new Error(`Unknown status: ${_exhaustive}`);
124
+ }
125
+ }
126
+ ```
127
+
128
+ **Why:** Adding a new status forces handling in all switches.
129
+
130
+ ---
131
+
132
+ ## Pattern 5: Branded Types
133
+
134
+ ```typescript
135
+ // Nominal typing for semantic safety
136
+ type UserId = string & { readonly __brand: 'UserId' };
137
+ type OrderId = string & { readonly __brand: 'OrderId' };
138
+
139
+ function createUserId(id: string): UserId {
140
+ return id as UserId;
141
+ }
142
+
143
+ // Compiler prevents mixing IDs
144
+ function getUser(id: UserId): User { ... }
145
+ function getOrder(id: OrderId): Order { ... }
146
+
147
+ getUser(orderId); // ❌ Type error: OrderId is not UserId
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Tool Usage Examples
153
+
154
+ ### View signatures
155
+
156
+ ```bash
157
+ $ invar sig src/auth.ts
158
+ src/auth.ts
159
+ function validateToken(token: string): boolean
160
+ @pre token.length > 0
161
+ @post result indicates valid JWT
162
+
163
+ class AuthService
164
+ method login(email: string, password: string): Promise<Result<Token, AuthError>>
165
+ method logout(): Promise<void>
166
+ ```
167
+
168
+ ### Find references
169
+
170
+ ```bash
171
+ $ invar refs src/auth.ts::validateToken
172
+ src/auth.ts:15 — Definition
173
+ src/routes/api.ts:42 — if (validateToken(req.headers.auth)) {
174
+ src/middleware/auth.ts:18 — const isValid = validateToken(token);
175
+ tests/auth.test.ts:8 — expect(validateToken('invalid')).toBe(false);
176
+ ```
177
+
178
+ ### Verify code
179
+
180
+ ```bash
181
+ $ invar guard
182
+ TypeScript Guard Report
183
+ ========================================
184
+ [PASS] tsc --noEmit (no type errors)
185
+ [PASS] eslint (0 errors, 2 warnings)
186
+ [PASS] vitest (24 tests passed)
187
+ ----------------------------------------
188
+ Guard passed.
189
+ ```
190
+
191
+ ---
192
+
193
+ *Managed by Invar - regenerated on `invar update`*
@@ -64,6 +64,11 @@ extensions = { action = "preserve" }
64
64
  ".claude/skills/propose/SKILL.md" = { src = "skills/propose/SKILL.md.jinja", type = "jinja" }
65
65
  ".claude/skills/review/SKILL.md" = { src = "skills/review/SKILL.md.jinja", type = "jinja" }
66
66
 
67
+ # DX-79: Invar usage feedback skill
68
+ ".claude/skills/invar-reflect/SKILL.md" = { src = "skills/invar-reflect/SKILL.md", type = "copy" }
69
+ ".claude/skills/invar-reflect/template.md" = { src = "skills/invar-reflect/template.md", type = "copy" }
70
+ ".claude/skills/invar-reflect/CONFIG.md" = { src = "skills/invar-reflect/CONFIG.md", type = "copy" }
71
+
67
72
  # Commands (Jinja2 templates for language-specific content)
68
73
  ".claude/commands/audit.md" = { src = "commands/audit.md.jinja", type = "jinja" }
69
74
  ".claude/commands/guard.md" = { src = "commands/guard.md", type = "copy" }
@@ -136,4 +141,7 @@ create_only = [
136
141
  ".pre-commit-config.yaml",
137
142
  ".claude/commands/audit.md",
138
143
  ".claude/commands/guard.md",
144
+ ".claude/skills/invar-reflect/SKILL.md",
145
+ ".claude/skills/invar-reflect/template.md",
146
+ ".claude/skills/invar-reflect/CONFIG.md",
139
147
  ]
@@ -1,9 +1,9 @@
1
1
  ## Commands (Python)
2
2
 
3
3
  ```bash
4
- invar guard # Full: static + doctests + CrossHair + Hypothesis
4
+ invar guard # Check git-modified files (fast, default)
5
+ invar guard --all # Check entire project (CI, release)
5
6
  invar guard --static # Static only (quick debug, ~0.5s)
6
- invar guard --changed # Modified files only
7
7
  invar guard --coverage # Collect branch coverage
8
8
  invar guard -c # Contract coverage only (DX-63)
9
9
  invar sig <file> # Show contracts + signatures
@@ -11,6 +11,9 @@ invar map --top 10 # Most-referenced symbols
11
11
  invar rules # List all rules with detection/hints (JSON)
12
12
  ```
13
13
 
14
+ **Default behavior**: Checks git-modified files for fast feedback during development.
15
+ Use `--all` for comprehensive checks before release.
16
+
14
17
  ## Configuration (Python)
15
18
 
16
19
  ```toml
@@ -2,15 +2,18 @@
2
2
 
3
3
  ```bash
4
4
  # Verification (Python CLI - works for TypeScript)
5
- invar guard # Full: tsc + eslint + vitest + ts-analyzer
5
+ invar guard # Check git-modified files (fast, default)
6
+ invar guard --all # Check entire project (CI, release)
6
7
  invar guard --json # Agent-friendly v2.0 JSON output
7
- invar guard --changed # Modified files only
8
8
 
9
9
  # Analysis
10
10
  invar sig <file> # Show function signatures
11
11
  invar map --top 10 # Most-referenced symbols
12
12
  ```
13
13
 
14
+ **Default behavior**: Checks git-modified files for fast feedback during development.
15
+ Use `--all` for comprehensive checks before release.
16
+
14
17
  ## Guard Output (v2.0 JSON)
15
18
 
16
19
  ```json