invar-tools 1.10.0__py3-none-any.whl → 1.12.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/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/ts_sig_parser.py +6 -3
- invar/mcp/handlers.py +436 -0
- invar/mcp/server.py +351 -156
- invar/node_tools/ts-query.js +396 -0
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +29 -0
- invar/shell/commands/init.py +72 -13
- invar/shell/commands/perception.py +302 -6
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +15 -14
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +13 -10
- invar/shell/py_refs.py +156 -0
- invar/shell/skill_manager.py +17 -15
- invar/shell/ts_compiler.py +238 -0
- invar/templates/examples/typescript/patterns.md +193 -0
- invar/templates/skills/develop/SKILL.md.jinja +46 -0
- invar/templates/skills/review/SKILL.md.jinja +205 -493
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/METADATA +58 -8
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/RECORD +27 -18
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/NOTICE +0 -0
invar/shell/py_refs.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Python reference finding using jedi.
|
|
2
|
+
|
|
3
|
+
DX-78: Provides cross-file reference finding for Python symbols.
|
|
4
|
+
Shell module: Uses jedi library for I/O-based symbol analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import jedi
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Reference:
|
|
17
|
+
"""A reference to a Python symbol."""
|
|
18
|
+
|
|
19
|
+
file: Path
|
|
20
|
+
line: int
|
|
21
|
+
column: int
|
|
22
|
+
context: str
|
|
23
|
+
is_definition: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# @shell_complexity: Reference finding with jedi library and error handling
|
|
27
|
+
def find_references(
|
|
28
|
+
file_path: Path,
|
|
29
|
+
line: int,
|
|
30
|
+
column: int,
|
|
31
|
+
project_root: Path | None = None,
|
|
32
|
+
) -> list[Reference]:
|
|
33
|
+
"""Find all references to symbol at position using jedi.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: File containing the symbol
|
|
37
|
+
line: 1-based line number
|
|
38
|
+
column: 0-based column number
|
|
39
|
+
project_root: Project root for cross-file resolution
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of references found
|
|
43
|
+
|
|
44
|
+
>>> from pathlib import Path
|
|
45
|
+
>>> import tempfile, os
|
|
46
|
+
>>> # Test with a simple Python file
|
|
47
|
+
>>> with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
48
|
+
... _ = f.write('def hello():\\n pass\\n')
|
|
49
|
+
... temp_file = Path(f.name)
|
|
50
|
+
>>> # Find references returns a list (may be empty if jedi not configured)
|
|
51
|
+
>>> refs = find_references(temp_file, 1, 4)
|
|
52
|
+
>>> isinstance(refs, list)
|
|
53
|
+
True
|
|
54
|
+
>>> os.unlink(temp_file)
|
|
55
|
+
"""
|
|
56
|
+
source = file_path.read_text(encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
project = None
|
|
59
|
+
if project_root:
|
|
60
|
+
project = jedi.Project(path=str(project_root))
|
|
61
|
+
|
|
62
|
+
script = jedi.Script(source, path=str(file_path), project=project)
|
|
63
|
+
refs = script.get_references(line, column)
|
|
64
|
+
|
|
65
|
+
results: list[Reference] = []
|
|
66
|
+
for ref in refs:
|
|
67
|
+
# Skip builtins (no module_path)
|
|
68
|
+
if not ref.module_path:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
line_code = ref.get_line_code()
|
|
72
|
+
context = line_code.strip() if line_code else ""
|
|
73
|
+
|
|
74
|
+
results.append(
|
|
75
|
+
Reference(
|
|
76
|
+
file=Path(ref.module_path),
|
|
77
|
+
line=ref.line,
|
|
78
|
+
column=ref.column,
|
|
79
|
+
context=context,
|
|
80
|
+
is_definition=ref.is_definition(),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# @shell_complexity: Symbol search using jedi library
|
|
88
|
+
def find_symbol_position(file_path: Path, symbol_name: str) -> tuple[int, int] | None:
|
|
89
|
+
"""Find the position of a symbol definition in a file.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
file_path: File to search
|
|
93
|
+
symbol_name: Name of the symbol to find
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (line, column) or None if not found
|
|
97
|
+
|
|
98
|
+
>>> from pathlib import Path
|
|
99
|
+
>>> import tempfile, os
|
|
100
|
+
>>> # Test finding a function definition
|
|
101
|
+
>>> with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
102
|
+
... _ = f.write('def test_func():\\n return 42\\n')
|
|
103
|
+
... temp_file = Path(f.name)
|
|
104
|
+
>>> pos = find_symbol_position(temp_file, "test_func")
|
|
105
|
+
>>> isinstance(pos, tuple) or pos is None # Returns tuple or None
|
|
106
|
+
True
|
|
107
|
+
>>> os.unlink(temp_file)
|
|
108
|
+
"""
|
|
109
|
+
source = file_path.read_text(encoding="utf-8")
|
|
110
|
+
script = jedi.Script(source, path=str(file_path))
|
|
111
|
+
|
|
112
|
+
# Get all names defined in the file
|
|
113
|
+
names = script.get_names(all_scopes=True)
|
|
114
|
+
|
|
115
|
+
for name in names:
|
|
116
|
+
if name.name == symbol_name and name.is_definition():
|
|
117
|
+
return (name.line, name.column)
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# @shell_complexity: Combines symbol position lookup and reference finding
|
|
123
|
+
def find_all_references_to_symbol(
|
|
124
|
+
file_path: Path,
|
|
125
|
+
symbol_name: str,
|
|
126
|
+
project_root: Path | None = None,
|
|
127
|
+
) -> list[Reference]:
|
|
128
|
+
"""Find all references to a named symbol.
|
|
129
|
+
|
|
130
|
+
Convenience function that combines find_symbol_position and find_references.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
file_path: File containing the symbol definition
|
|
134
|
+
symbol_name: Name of the symbol
|
|
135
|
+
project_root: Project root for cross-file resolution
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of references found
|
|
139
|
+
|
|
140
|
+
>>> from pathlib import Path
|
|
141
|
+
>>> import tempfile, os
|
|
142
|
+
>>> # Test finding all references to a symbol
|
|
143
|
+
>>> with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
144
|
+
... _ = f.write('def greet():\\n pass\\ngreet()\\n')
|
|
145
|
+
... temp_file = Path(f.name)
|
|
146
|
+
>>> refs = find_all_references_to_symbol(temp_file, "greet")
|
|
147
|
+
>>> isinstance(refs, list) # Returns list of references
|
|
148
|
+
True
|
|
149
|
+
>>> os.unlink(temp_file)
|
|
150
|
+
"""
|
|
151
|
+
position = find_symbol_position(file_path, symbol_name)
|
|
152
|
+
if position is None:
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
line, column = position
|
|
156
|
+
return find_references(file_path, line, column, project_root)
|
invar/shell/skill_manager.py
CHANGED
|
@@ -11,6 +11,7 @@ DX-71: Simplified to idempotent `add` command with region merge.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import re
|
|
14
15
|
import shutil
|
|
15
16
|
from dataclasses import dataclass
|
|
16
17
|
from pathlib import Path
|
|
@@ -35,12 +36,15 @@ CORE_SKILLS = {"develop", "review", "investigate", "propose", "guard", "audit"}
|
|
|
35
36
|
|
|
36
37
|
# @shell_orchestration: Validation helper used only by shell add_skill/remove_skill
|
|
37
38
|
def _is_valid_skill_name(name: str) -> bool:
|
|
38
|
-
"""Validate skill name to prevent path traversal attacks."""
|
|
39
|
-
# Block path traversal characters
|
|
40
|
-
if ".." in name or "/" in name or "\\" in name:
|
|
39
|
+
"""Validate skill name to prevent path traversal and filesystem attacks."""
|
|
40
|
+
# Block path traversal characters and null bytes
|
|
41
|
+
if ".." in name or "/" in name or "\\" in name or "\x00" in name:
|
|
41
42
|
return False
|
|
42
|
-
#
|
|
43
|
-
|
|
43
|
+
# Block special names that could cause issues
|
|
44
|
+
if name in (".", ""):
|
|
45
|
+
return False
|
|
46
|
+
# Must not start with dot or underscore
|
|
47
|
+
return not name.startswith(".") and not name.startswith("_")
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
def _merge_md_file(src: Path, dst: Path) -> tuple[bool, str]:
|
|
@@ -74,10 +78,10 @@ def _merge_md_file(src: Path, dst: Path) -> tuple[bool, str]:
|
|
|
74
78
|
shutil.copy2(src, dst)
|
|
75
79
|
return False, "Updated"
|
|
76
80
|
|
|
77
|
-
except
|
|
78
|
-
# On parse error, preserve existing file - don't silently lose user data
|
|
79
|
-
#
|
|
80
|
-
return False, "Skipped (merge failed
|
|
81
|
+
except (OSError, UnicodeDecodeError, ValueError, KeyError) as e:
|
|
82
|
+
# On I/O or parse error, preserve existing file - don't silently lose user data
|
|
83
|
+
# Include error details for debugging
|
|
84
|
+
return False, f"Skipped (merge failed: {type(e).__name__}: {e})"
|
|
81
85
|
|
|
82
86
|
|
|
83
87
|
@dataclass
|
|
@@ -109,7 +113,7 @@ def load_registry() -> Result[dict, str]:
|
|
|
109
113
|
content = registry_path.read_text()
|
|
110
114
|
data = yaml.safe_load(content)
|
|
111
115
|
return Success(data)
|
|
112
|
-
except
|
|
116
|
+
except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
|
|
113
117
|
return Failure(f"Failed to parse registry: {e}")
|
|
114
118
|
|
|
115
119
|
|
|
@@ -245,7 +249,7 @@ def add_skill(
|
|
|
245
249
|
result_msg = "updated" if is_update else "installed"
|
|
246
250
|
return Success(f"Skill '{skill_name}' {result_msg} successfully")
|
|
247
251
|
|
|
248
|
-
except
|
|
252
|
+
except (OSError, shutil.Error) as e:
|
|
249
253
|
# Clean up on failure (only for fresh install)
|
|
250
254
|
# M3 note: Updates that fail mid-way may leave directory in partial state.
|
|
251
255
|
# This is acceptable because: (1) user extensions are preserved via merge,
|
|
@@ -258,8 +262,6 @@ def add_skill(
|
|
|
258
262
|
|
|
259
263
|
def has_user_extensions(skill_dir: Path) -> bool:
|
|
260
264
|
"""Check if SKILL.md has user content in extensions region."""
|
|
261
|
-
import re
|
|
262
|
-
|
|
263
265
|
skill_md = skill_dir / "SKILL.md"
|
|
264
266
|
if not skill_md.exists():
|
|
265
267
|
return False
|
|
@@ -283,7 +285,7 @@ def has_user_extensions(skill_dir: Path) -> bool:
|
|
|
283
285
|
|
|
284
286
|
# Check if any non-whitespace content remains
|
|
285
287
|
return bool(cleaned.strip())
|
|
286
|
-
except
|
|
288
|
+
except (ValueError, KeyError):
|
|
287
289
|
# Parse error - assume extensions exist (safe default)
|
|
288
290
|
return True
|
|
289
291
|
|
|
@@ -334,7 +336,7 @@ def remove_skill(
|
|
|
334
336
|
try:
|
|
335
337
|
shutil.rmtree(dest_dir)
|
|
336
338
|
return Success(f"Skill '{skill_name}' removed successfully")
|
|
337
|
-
except
|
|
339
|
+
except (OSError, shutil.Error) as e:
|
|
338
340
|
return Failure(f"Failed to remove skill: {e}")
|
|
339
341
|
|
|
340
342
|
|
|
@@ -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,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`*
|