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.
- invar/core/feedback.py +110 -0
- invar/mcp/handlers.py +28 -0
- invar/mcp/server.py +64 -14
- invar/node_tools/ts-query.js +396 -0
- invar/shell/claude_hooks.py +49 -0
- invar/shell/commands/feedback.py +258 -0
- invar/shell/commands/guard.py +33 -1
- invar/shell/commands/init.py +51 -1
- invar/shell/commands/perception.py +302 -6
- invar/shell/py_refs.py +156 -0
- invar/shell/ts_compiler.py +238 -0
- invar/templates/claude-md/universal/tool-selection.md +110 -0
- invar/templates/config/CLAUDE.md.jinja +2 -0
- invar/templates/examples/typescript/patterns.md +193 -0
- invar/templates/manifest.toml +8 -0
- invar/templates/protocol/python/tools.md +5 -2
- invar/templates/protocol/typescript/tools.md +5 -2
- invar/templates/skills/invar-reflect/CONFIG.md +355 -0
- invar/templates/skills/invar-reflect/SKILL.md +466 -0
- invar/templates/skills/invar-reflect/template.md +343 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/METADATA +25 -7
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/RECORD +27 -17
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.14.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -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.
|
|
@@ -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`*
|
invar/templates/manifest.toml
CHANGED
|
@@ -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 #
|
|
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 #
|
|
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
|