invar-tools 1.11.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/mcp/handlers.py +28 -0
- invar/mcp/server.py +64 -14
- invar/node_tools/ts-query.js +396 -0
- invar/shell/commands/guard.py +24 -0
- invar/shell/commands/perception.py +302 -6
- invar/shell/py_refs.py +156 -0
- invar/shell/ts_compiler.py +238 -0
- invar/templates/examples/typescript/patterns.md +193 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/METADATA +25 -7
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/RECORD +15 -11
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.11.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/NOTICE +0 -0
invar/mcp/handlers.py
CHANGED
|
@@ -114,6 +114,34 @@ async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
|
114
114
|
return await _execute_command(cmd)
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
# @shell_orchestration: MCP handler - orchestrates refs command execution
|
|
118
|
+
# @invar:allow shell_result: MCP handler for refs tool
|
|
119
|
+
async def _run_refs(args: dict[str, Any]) -> list[TextContent]:
|
|
120
|
+
"""Run invar refs command.
|
|
121
|
+
|
|
122
|
+
DX-78: Find all references to a symbol.
|
|
123
|
+
Target format: "path/to/file.py::symbol" or "path/to/file.ts::symbol"
|
|
124
|
+
"""
|
|
125
|
+
target = args.get("target", "")
|
|
126
|
+
if not target:
|
|
127
|
+
return [TextContent(type="text", text="Error: 'target' parameter is required")]
|
|
128
|
+
|
|
129
|
+
# Parse target to validate file path
|
|
130
|
+
if "::" not in target:
|
|
131
|
+
return [TextContent(type="text", text="Error: Invalid target format. Use 'file::symbol'")]
|
|
132
|
+
|
|
133
|
+
file_part, _symbol = target.rsplit("::", 1)
|
|
134
|
+
is_valid, error = _validate_path(file_part)
|
|
135
|
+
if not is_valid:
|
|
136
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
137
|
+
|
|
138
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "refs"]
|
|
139
|
+
cmd.append(target)
|
|
140
|
+
cmd.append("--json")
|
|
141
|
+
|
|
142
|
+
return await _execute_command(cmd)
|
|
143
|
+
|
|
144
|
+
|
|
117
145
|
# DX-76: Document query handlers
|
|
118
146
|
# @shell_orchestration: MCP handler - calls shell layer directly
|
|
119
147
|
# @shell_complexity: MCP input validation + result handling
|
invar/mcp/server.py
CHANGED
|
@@ -26,27 +26,42 @@ from invar.mcp.handlers import (
|
|
|
26
26
|
_run_doc_toc,
|
|
27
27
|
_run_guard,
|
|
28
28
|
_run_map,
|
|
29
|
+
_run_refs,
|
|
29
30
|
_run_sig,
|
|
30
31
|
)
|
|
31
32
|
from invar.shell.subprocess_env import should_respawn
|
|
32
33
|
|
|
33
|
-
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26 + DX-76)
|
|
34
|
+
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26 + DX-76 + DX-78)
|
|
34
35
|
INVAR_INSTRUCTIONS = """
|
|
35
36
|
## Invar Tool Usage (MANDATORY)
|
|
36
37
|
|
|
37
38
|
This project uses Invar for all code verification and analysis.
|
|
38
39
|
The following rules are MANDATORY, not suggestions.
|
|
39
40
|
|
|
40
|
-
###
|
|
41
|
+
### Check-In (REQUIRED)
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
Your first message MUST display:
|
|
44
|
+
```
|
|
45
|
+
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
46
|
+
```
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
**Actions:** Read `.invar/context.md`, then show status.
|
|
49
|
+
**Do NOT run guard at Check-In.**
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
Run guard only when:
|
|
52
|
+
- Entering VALIDATE phase of USBV workflow
|
|
53
|
+
- User explicitly requests verification
|
|
54
|
+
- After making code changes
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
### Tool x Language Support
|
|
57
|
+
|
|
58
|
+
| Tool | Python | TypeScript | Notes |
|
|
59
|
+
|------|--------|------------|-------|
|
|
60
|
+
| `invar_guard` | ✅ Full | ⚠️ Partial | TS: tsc + eslint + vitest |
|
|
61
|
+
| `invar_sig` | ✅ Full | ✅ Full | TS: TS Compiler API |
|
|
62
|
+
| `invar_map` | ✅ Full | ✅ Full | TS: With reference counts |
|
|
63
|
+
| `invar_refs` | ✅ Full | ✅ Full | Cross-file reference finding |
|
|
64
|
+
| `invar_doc_*` | ✅ Full | ✅ Full | Language-agnostic |
|
|
50
65
|
|
|
51
66
|
### Tool Substitution Rules (ENFORCED)
|
|
52
67
|
|
|
@@ -56,6 +71,7 @@ Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
|
56
71
|
| Symbolic verification | `Bash("crosshair ...")` | `invar_guard` (included by default) |
|
|
57
72
|
| Understand file structure | `Read` entire .py file | `invar_sig` |
|
|
58
73
|
| Find entry points | `Grep` for "def " | `invar_map` |
|
|
74
|
+
| Find symbol references | Manual grep | `invar_refs` |
|
|
59
75
|
| View document structure | `Read` entire .md file | `invar_doc_toc` |
|
|
60
76
|
| Read document section | `Read` with manual line counting | `invar_doc_read` |
|
|
61
77
|
| Read multiple sections | Multiple `invar_doc_read` calls | `invar_doc_read_many` |
|
|
@@ -91,8 +107,8 @@ Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
|
91
107
|
### Task Completion
|
|
92
108
|
|
|
93
109
|
A task is complete ONLY when:
|
|
94
|
-
-
|
|
95
|
-
- Final `invar_guard` passed
|
|
110
|
+
- Check-In displayed at session start
|
|
111
|
+
- Final `invar_guard` passed (in VALIDATE phase)
|
|
96
112
|
- User requirement satisfied
|
|
97
113
|
|
|
98
114
|
### Why This Matters
|
|
@@ -105,12 +121,12 @@ A task is complete ONLY when:
|
|
|
105
121
|
### Correct Usage Examples
|
|
106
122
|
|
|
107
123
|
```
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
# Check-In (REQUIRED at session start)
|
|
125
|
+
# Display: ✓ Check-In: Invar | main | clean
|
|
126
|
+
# Then read .invar/context.md
|
|
111
127
|
|
|
112
|
-
#
|
|
113
|
-
|
|
128
|
+
# Explore codebase (when needed)
|
|
129
|
+
invar_map(top=10)
|
|
114
130
|
|
|
115
131
|
# Understand a file's structure
|
|
116
132
|
invar_sig(target="src/invar/core/parser.py")
|
|
@@ -120,6 +136,9 @@ invar_doc_toc(file="docs/proposals/DX-76.md")
|
|
|
120
136
|
|
|
121
137
|
# Read specific section
|
|
122
138
|
invar_doc_read(file="docs/proposals/DX-76.md", section="phase-a")
|
|
139
|
+
|
|
140
|
+
# VALIDATE phase: Verify code after changes
|
|
141
|
+
invar_guard(changed=true)
|
|
123
142
|
```
|
|
124
143
|
|
|
125
144
|
IMPORTANT: Using Bash commands for Invar operations bypasses
|
|
@@ -194,6 +213,35 @@ def _get_map_tool() -> Tool:
|
|
|
194
213
|
)
|
|
195
214
|
|
|
196
215
|
|
|
216
|
+
|
|
217
|
+
# @shell_orchestration: MCP tool factory - creates tool definition for framework
|
|
218
|
+
# @invar:allow shell_result: MCP tool factory for refs command
|
|
219
|
+
def _get_refs_tool() -> Tool:
|
|
220
|
+
"""Define the invar_refs tool.
|
|
221
|
+
|
|
222
|
+
DX-78: Cross-file reference finding.
|
|
223
|
+
"""
|
|
224
|
+
return Tool(
|
|
225
|
+
name="invar_refs",
|
|
226
|
+
title="Find References",
|
|
227
|
+
description=(
|
|
228
|
+
"Find all references to a symbol. "
|
|
229
|
+
"Supports Python (via jedi) and TypeScript (via TS Compiler API). "
|
|
230
|
+
"Use this to understand symbol usage across the codebase."
|
|
231
|
+
),
|
|
232
|
+
inputSchema={
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"target": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Target format: 'file.py::symbol' or 'file.ts::symbol'",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
"required": ["target"],
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
197
245
|
# DX-76: Document query tools
|
|
198
246
|
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
199
247
|
# @invar:allow shell_result: MCP tool factory for doc_toc command
|
|
@@ -417,6 +465,7 @@ def create_server() -> Server:
|
|
|
417
465
|
_get_guard_tool(),
|
|
418
466
|
_get_sig_tool(),
|
|
419
467
|
_get_map_tool(),
|
|
468
|
+
_get_refs_tool(), # DX-78: Reference finding
|
|
420
469
|
# DX-76: Document query tools
|
|
421
470
|
_get_doc_toc_tool(),
|
|
422
471
|
_get_doc_read_tool(),
|
|
@@ -434,6 +483,7 @@ def create_server() -> Server:
|
|
|
434
483
|
"invar_guard": _run_guard,
|
|
435
484
|
"invar_sig": _run_sig,
|
|
436
485
|
"invar_map": _run_map,
|
|
486
|
+
"invar_refs": _run_refs, # DX-78: Reference finding
|
|
437
487
|
# DX-76: Document query handlers
|
|
438
488
|
"invar_doc_toc": _run_doc_toc,
|
|
439
489
|
"invar_doc_read": _run_doc_read,
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript Compiler API query tool.
|
|
4
|
+
* Single-shot execution: runs query, outputs JSON, exits.
|
|
5
|
+
* No persistent process, no orphan risk.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node ts-query.js '{"command": "sig", "file": "src/auth.ts"}'
|
|
9
|
+
* node ts-query.js '{"command": "map", "path": ".", "top": 10}'
|
|
10
|
+
* node ts-query.js '{"command": "refs", "file": "src/auth.ts", "line": 10, "column": 5}'
|
|
11
|
+
*
|
|
12
|
+
* Part of DX-78: TypeScript Compiler Integration.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ts = require('typescript');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// Parse query from command line
|
|
20
|
+
let query;
|
|
21
|
+
try {
|
|
22
|
+
query = JSON.parse(process.argv[2] || '{}');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(JSON.stringify({
|
|
25
|
+
error: 'Invalid JSON input',
|
|
26
|
+
message: e.message
|
|
27
|
+
}));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find tsconfig.json and create a TypeScript program.
|
|
33
|
+
*/
|
|
34
|
+
function createProgram(projectPath) {
|
|
35
|
+
const configPath = ts.findConfigFile(
|
|
36
|
+
projectPath,
|
|
37
|
+
ts.sys.fileExists,
|
|
38
|
+
'tsconfig.json'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!configPath) {
|
|
42
|
+
console.error(JSON.stringify({ error: 'tsconfig.json not found' }));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
47
|
+
if (configFile.error) {
|
|
48
|
+
console.error(JSON.stringify({ error: 'Failed to read tsconfig.json' }));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
53
|
+
configFile.config,
|
|
54
|
+
ts.sys,
|
|
55
|
+
path.dirname(configPath)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return ts.createProgram(parsed.fileNames, parsed.options);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get class/function members recursively.
|
|
63
|
+
*/
|
|
64
|
+
function getMembers(node, checker, sourceFile) {
|
|
65
|
+
const members = [];
|
|
66
|
+
|
|
67
|
+
if (ts.isClassDeclaration(node) && node.members) {
|
|
68
|
+
for (const member of node.members) {
|
|
69
|
+
if (ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member)) {
|
|
70
|
+
const name = member.name ? member.name.getText(sourceFile) : '<anonymous>';
|
|
71
|
+
const symbol = checker.getSymbolAtLocation(member.name || member);
|
|
72
|
+
let signature = '';
|
|
73
|
+
|
|
74
|
+
if (symbol) {
|
|
75
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, member);
|
|
76
|
+
signature = checker.typeToString(type);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(member.pos);
|
|
80
|
+
members.push({
|
|
81
|
+
name,
|
|
82
|
+
kind: ts.isMethodDeclaration(member) ? 'method' : 'property',
|
|
83
|
+
signature,
|
|
84
|
+
line: pos.line + 1
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return members;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract JSDoc @pre/@post comments.
|
|
95
|
+
*/
|
|
96
|
+
function extractContracts(node, sourceFile) {
|
|
97
|
+
const contracts = { pre: [], post: [] };
|
|
98
|
+
const jsDocs = ts.getJSDocTags(node);
|
|
99
|
+
|
|
100
|
+
for (const tag of jsDocs) {
|
|
101
|
+
const tagName = tag.tagName.getText();
|
|
102
|
+
const comment = typeof tag.comment === 'string'
|
|
103
|
+
? tag.comment
|
|
104
|
+
: tag.comment?.map(c => c.text).join('') || '';
|
|
105
|
+
|
|
106
|
+
if (tagName === 'pre') {
|
|
107
|
+
contracts.pre.push(comment);
|
|
108
|
+
} else if (tagName === 'post') {
|
|
109
|
+
contracts.post.push(comment);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return contracts;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Command: sig - Extract signatures from a file.
|
|
118
|
+
*/
|
|
119
|
+
function outputSignatures(filePath) {
|
|
120
|
+
const projectPath = path.dirname(filePath);
|
|
121
|
+
const program = createProgram(projectPath);
|
|
122
|
+
const checker = program.getTypeChecker();
|
|
123
|
+
const sourceFile = program.getSourceFile(path.resolve(filePath));
|
|
124
|
+
|
|
125
|
+
if (!sourceFile) {
|
|
126
|
+
console.log(JSON.stringify({ file: filePath, symbols: [], error: 'File not found in program' }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const symbols = [];
|
|
131
|
+
|
|
132
|
+
function visit(node) {
|
|
133
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
134
|
+
const name = node.name.getText(sourceFile);
|
|
135
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
136
|
+
const type = symbol ? checker.getTypeOfSymbolAtLocation(symbol, node) : null;
|
|
137
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
138
|
+
const contracts = extractContracts(node, sourceFile);
|
|
139
|
+
|
|
140
|
+
symbols.push({
|
|
141
|
+
name,
|
|
142
|
+
kind: 'function',
|
|
143
|
+
signature: type ? checker.typeToString(type) : '',
|
|
144
|
+
line: pos.line + 1,
|
|
145
|
+
contracts
|
|
146
|
+
});
|
|
147
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
148
|
+
const name = node.name.getText(sourceFile);
|
|
149
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
150
|
+
const members = getMembers(node, checker, sourceFile);
|
|
151
|
+
|
|
152
|
+
symbols.push({
|
|
153
|
+
name,
|
|
154
|
+
kind: 'class',
|
|
155
|
+
signature: `class ${name}`,
|
|
156
|
+
line: pos.line + 1,
|
|
157
|
+
members
|
|
158
|
+
});
|
|
159
|
+
} else if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
160
|
+
const name = node.name.getText(sourceFile);
|
|
161
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
162
|
+
|
|
163
|
+
symbols.push({
|
|
164
|
+
name,
|
|
165
|
+
kind: 'interface',
|
|
166
|
+
signature: `interface ${name}`,
|
|
167
|
+
line: pos.line + 1
|
|
168
|
+
});
|
|
169
|
+
} else if (ts.isTypeAliasDeclaration(node) && node.name) {
|
|
170
|
+
const name = node.name.getText(sourceFile);
|
|
171
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
172
|
+
|
|
173
|
+
symbols.push({
|
|
174
|
+
name,
|
|
175
|
+
kind: 'type',
|
|
176
|
+
signature: `type ${name}`,
|
|
177
|
+
line: pos.line + 1
|
|
178
|
+
});
|
|
179
|
+
} else if (ts.isVariableStatement(node)) {
|
|
180
|
+
for (const decl of node.declarationList.declarations) {
|
|
181
|
+
if (ts.isIdentifier(decl.name)) {
|
|
182
|
+
const name = decl.name.getText(sourceFile);
|
|
183
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
184
|
+
const type = symbol ? checker.getTypeOfSymbolAtLocation(symbol, decl) : null;
|
|
185
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(decl.pos);
|
|
186
|
+
|
|
187
|
+
// Only include exported or significant declarations
|
|
188
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
189
|
+
const isFunctionLike = decl.initializer && (
|
|
190
|
+
ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)
|
|
191
|
+
);
|
|
192
|
+
if (isExported || isFunctionLike) {
|
|
193
|
+
symbols.push({
|
|
194
|
+
name,
|
|
195
|
+
kind: 'const',
|
|
196
|
+
signature: type ? checker.typeToString(type) : '',
|
|
197
|
+
line: pos.line + 1
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ts.forEachChild(node, visit);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
visit(sourceFile);
|
|
208
|
+
console.log(JSON.stringify({ file: filePath, symbols }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Command: map - Get symbol map with reference counts.
|
|
213
|
+
*/
|
|
214
|
+
function outputSymbolMap(projectPath, topN) {
|
|
215
|
+
const program = createProgram(projectPath);
|
|
216
|
+
const checker = program.getTypeChecker();
|
|
217
|
+
const allSymbols = [];
|
|
218
|
+
|
|
219
|
+
// Collect all symbols from all source files
|
|
220
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
221
|
+
// Skip node_modules and declaration files
|
|
222
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const relativePath = path.relative(projectPath, sourceFile.fileName);
|
|
227
|
+
|
|
228
|
+
function visit(node) {
|
|
229
|
+
let symbolInfo = null;
|
|
230
|
+
|
|
231
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
232
|
+
const name = node.name.getText(sourceFile);
|
|
233
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
234
|
+
symbolInfo = { name, kind: 'function', file: relativePath, line: pos.line + 1 };
|
|
235
|
+
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
236
|
+
const name = node.name.getText(sourceFile);
|
|
237
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
238
|
+
symbolInfo = { name, kind: 'class', file: relativePath, line: pos.line + 1 };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (symbolInfo) {
|
|
242
|
+
allSymbols.push(symbolInfo);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ts.forEachChild(node, visit);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
visit(sourceFile);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Sort by kind priority, then name
|
|
252
|
+
const kindOrder = { 'function': 0, 'class': 1, 'interface': 2, 'type': 3, 'const': 4 };
|
|
253
|
+
allSymbols.sort((a, b) => {
|
|
254
|
+
const orderA = kindOrder[a.kind] ?? 99;
|
|
255
|
+
const orderB = kindOrder[b.kind] ?? 99;
|
|
256
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
257
|
+
return a.name.localeCompare(b.name);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Limit to topN
|
|
261
|
+
const result = topN > 0 ? allSymbols.slice(0, topN) : allSymbols;
|
|
262
|
+
|
|
263
|
+
console.log(JSON.stringify({
|
|
264
|
+
path: projectPath,
|
|
265
|
+
total: allSymbols.length,
|
|
266
|
+
symbols: result
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Command: refs - Find all references to symbol at position.
|
|
272
|
+
*/
|
|
273
|
+
function outputReferences(filePath, line, column) {
|
|
274
|
+
const projectPath = path.dirname(filePath);
|
|
275
|
+
const configPath = ts.findConfigFile(projectPath, ts.sys.fileExists, 'tsconfig.json');
|
|
276
|
+
|
|
277
|
+
if (!configPath) {
|
|
278
|
+
console.log(JSON.stringify({ error: 'tsconfig.json not found', references: [] }));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Create language service for find references
|
|
283
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
284
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
285
|
+
configFile.config,
|
|
286
|
+
ts.sys,
|
|
287
|
+
path.dirname(configPath)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const files = {};
|
|
291
|
+
for (const fileName of parsed.fileNames) {
|
|
292
|
+
try {
|
|
293
|
+
files[fileName] = {
|
|
294
|
+
version: 0,
|
|
295
|
+
text: fs.readFileSync(fileName, 'utf-8')
|
|
296
|
+
};
|
|
297
|
+
} catch (e) {
|
|
298
|
+
// Skip files that can't be read (may be deleted or permissions issue)
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const servicesHost = {
|
|
304
|
+
getScriptFileNames: () => parsed.fileNames,
|
|
305
|
+
getScriptVersion: (fileName) => files[fileName]?.version.toString() || '0',
|
|
306
|
+
getScriptSnapshot: (fileName) => {
|
|
307
|
+
if (!files[fileName]) {
|
|
308
|
+
try {
|
|
309
|
+
const text = fs.readFileSync(fileName, 'utf-8');
|
|
310
|
+
files[fileName] = { version: 0, text };
|
|
311
|
+
} catch (e) {
|
|
312
|
+
// File doesn't exist or can't be read
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return ts.ScriptSnapshot.fromString(files[fileName].text);
|
|
317
|
+
},
|
|
318
|
+
getCurrentDirectory: () => path.dirname(configPath),
|
|
319
|
+
getCompilationSettings: () => parsed.options,
|
|
320
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
321
|
+
fileExists: ts.sys.fileExists,
|
|
322
|
+
readFile: ts.sys.readFile,
|
|
323
|
+
readDirectory: ts.sys.readDirectory,
|
|
324
|
+
directoryExists: ts.sys.directoryExists,
|
|
325
|
+
getDirectories: ts.sys.getDirectories,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry());
|
|
329
|
+
|
|
330
|
+
// Convert line/column to position
|
|
331
|
+
const absolutePath = path.resolve(filePath);
|
|
332
|
+
const sourceFile = services.getProgram()?.getSourceFile(absolutePath);
|
|
333
|
+
|
|
334
|
+
if (!sourceFile) {
|
|
335
|
+
console.log(JSON.stringify({ error: 'File not found', references: [] }));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const position = sourceFile.getPositionOfLineAndCharacter(line - 1, column);
|
|
340
|
+
|
|
341
|
+
// Find references
|
|
342
|
+
const refs = services.findReferences(absolutePath, position);
|
|
343
|
+
const references = [];
|
|
344
|
+
|
|
345
|
+
if (refs) {
|
|
346
|
+
for (const refGroup of refs) {
|
|
347
|
+
for (const ref of refGroup.references) {
|
|
348
|
+
const refFile = services.getProgram()?.getSourceFile(ref.fileName);
|
|
349
|
+
if (refFile) {
|
|
350
|
+
const startPos = refFile.getLineAndCharacterOfPosition(ref.textSpan.start);
|
|
351
|
+
const lineText = refFile.text.split('\n')[startPos.line]?.trim() || '';
|
|
352
|
+
|
|
353
|
+
references.push({
|
|
354
|
+
file: path.relative(projectPath, ref.fileName),
|
|
355
|
+
line: startPos.line + 1,
|
|
356
|
+
column: startPos.character,
|
|
357
|
+
context: lineText,
|
|
358
|
+
isDefinition: ref.isDefinition || false
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(JSON.stringify({
|
|
366
|
+
file: filePath,
|
|
367
|
+
line,
|
|
368
|
+
column,
|
|
369
|
+
references
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Route to appropriate command
|
|
374
|
+
switch (query.command) {
|
|
375
|
+
case 'sig':
|
|
376
|
+
outputSignatures(query.file);
|
|
377
|
+
break;
|
|
378
|
+
case 'map':
|
|
379
|
+
outputSymbolMap(query.path || '.', query.top || 10);
|
|
380
|
+
break;
|
|
381
|
+
case 'refs':
|
|
382
|
+
outputReferences(query.file, query.line, query.column);
|
|
383
|
+
break;
|
|
384
|
+
default:
|
|
385
|
+
console.error(JSON.stringify({
|
|
386
|
+
error: `Unknown command: ${query.command}`,
|
|
387
|
+
usage: {
|
|
388
|
+
sig: { file: 'path/to/file.ts' },
|
|
389
|
+
map: { path: '.', top: 10 },
|
|
390
|
+
refs: { file: 'path/to/file.ts', line: 10, column: 5 }
|
|
391
|
+
}
|
|
392
|
+
}));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
process.exit(0);
|
invar/shell/commands/guard.py
CHANGED
|
@@ -480,6 +480,30 @@ def sig_command(
|
|
|
480
480
|
raise typer.Exit(1)
|
|
481
481
|
|
|
482
482
|
|
|
483
|
+
# @invar:allow entry_point_too_thick: Multi-language ref finding with examples
|
|
484
|
+
@app.command("refs")
|
|
485
|
+
def refs_command(
|
|
486
|
+
target: str = typer.Argument(..., help="file.py::symbol or file.ts::symbol"),
|
|
487
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
488
|
+
) -> None:
|
|
489
|
+
"""Find all references to a symbol.
|
|
490
|
+
|
|
491
|
+
DX-78: Supports Python (via jedi) and TypeScript (via TS Compiler API).
|
|
492
|
+
|
|
493
|
+
Examples:
|
|
494
|
+
invar refs src/auth.py::AuthService
|
|
495
|
+
invar refs src/auth.ts::validateToken
|
|
496
|
+
"""
|
|
497
|
+
from invar.shell.commands.perception import run_refs
|
|
498
|
+
|
|
499
|
+
# Auto-detect agent mode
|
|
500
|
+
use_json = json_output or _detect_agent_mode()
|
|
501
|
+
result = run_refs(target, use_json)
|
|
502
|
+
if isinstance(result, Failure):
|
|
503
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
504
|
+
raise typer.Exit(1)
|
|
505
|
+
|
|
506
|
+
|
|
483
507
|
# @invar:allow entry_point_too_thick: Rules display with filtering and dual output modes
|
|
484
508
|
@app.command()
|
|
485
509
|
def rules(
|