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 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
- ### Session Start (REQUIRED)
41
+ ### Check-In (REQUIRED)
41
42
 
42
- Before writing ANY code, you MUST execute:
43
+ Your first message MUST display:
44
+ ```
45
+ ✓ Check-In: [project] | [branch] | [clean/dirty]
46
+ ```
43
47
 
44
- 1. `invar_guard(changed=true)` Check existing violations
45
- 2. `invar_map(top=10)` Understand code structure
48
+ **Actions:** Read `.invar/context.md`, then show status.
49
+ **Do NOT run guard at Check-In.**
46
50
 
47
- Then read `.invar/examples/` and `.invar/context.md` for project context.
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
- **Skipping Session Start Non-compliant code → Task failure.**
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
- - Session Start executed (invar_guard + invar_map)
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
- # Session Start (REQUIRED before any code)
109
- invar_guard(changed=true)
110
- invar_map(top=10)
124
+ # Check-In (REQUIRED at session start)
125
+ # Display: ✓ Check-In: Invar | main | clean
126
+ # Then read .invar/context.md
111
127
 
112
- # Verify code after changes (full verification by default)
113
- invar_guard(changed=true)
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);
@@ -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(