specvector 0.0.1 → 0.1.0

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.
package/README.md CHANGED
@@ -2,29 +2,155 @@
2
2
 
3
3
  > Context-aware AI code review using Model Context Protocol (MCP)
4
4
 
5
- 🚧 **Coming Soon** — This package is under active development.
6
-
7
5
  ## What is SpecVector?
8
6
 
9
- SpecVector is an open-source AI code review tool that connects to your internal systems (Linear, ADRs, database schemas) to provide context-aware feedback on pull requests.
7
+ SpecVector is an open-source AI code review tool that explores your codebase to provide context-aware feedback on pull requests.
8
+
9
+ Unlike generic AI review tools that only see the diff, SpecVector:
10
+
11
+ - **Reads related files** to understand context
12
+ - **Searches for patterns** to find usages
13
+ - **Explores project structure** to understand architecture
14
+ - **Uses tool calling** for agentic code exploration
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Install
19
+
20
+ ```bash
21
+ # Clone and install
22
+ git clone https://github.com/nedlink/specvector.git
23
+ cd specvector
24
+ bun install
25
+ ```
26
+
27
+ ### 2. Set up API Key
28
+
29
+ ```bash
30
+ # Create .env file
31
+ cp .env.example .env
32
+
33
+ # Add your OpenRouter API key
34
+ echo "OPENROUTER_API_KEY=your-key-here" >> .env
35
+ ```
36
+
37
+ Get a key at [openrouter.ai](https://openrouter.ai)
38
+
39
+ ### 3. Run a Review
40
+
41
+ ```bash
42
+ # Review a PR (dry run - no posting)
43
+ cd /path/to/repo-with-pr
44
+ bun run /path/to/specvector/src/index.ts review 123 --dry-run
45
+
46
+ # Or with mock review (no LLM calls)
47
+ bun run /path/to/specvector/src/index.ts review 123 --mock --dry-run
48
+ ```
49
+
50
+ ## CLI Usage
51
+
52
+ ```
53
+ SpecVector CLI v0.1.0
54
+ Context-aware AI code review
55
+
56
+ USAGE:
57
+ specvector review <pr-number> Review a pull request
58
+ specvector review <pr-number> --dry-run Preview review without posting
59
+ specvector review <pr-number> --mock Use mock review (no LLM)
60
+ specvector --help Show this help
61
+ specvector --version Show version
10
62
 
11
- Unlike generic AI review tools that only see the diff, SpecVector understands:
63
+ ENVIRONMENT:
64
+ OPENROUTER_API_KEY API key for OpenRouter
65
+ SPECVECTOR_PROVIDER LLM provider (openrouter or ollama)
66
+ SPECVECTOR_MODEL Model to use
67
+ ```
68
+
69
+ ## GitHub Action
70
+
71
+ Add to your repository:
72
+
73
+ ```yaml
74
+ name: SpecVector Code Review
75
+
76
+ on:
77
+ pull_request:
78
+ types: [opened, synchronize, reopened]
79
+
80
+ permissions:
81
+ contents: read
82
+ pull-requests: write
12
83
 
13
- - What ticket the PR is for
14
- - Your architectural decisions
15
- - Your database schema
16
- - Your coding standards
84
+ jobs:
85
+ review:
86
+ runs-on: ubuntu-latest
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ with:
90
+ fetch-depth: 0
17
91
 
18
- ## Installation
92
+ - uses: oven-sh/setup-bun@v2
93
+
94
+ - name: Install SpecVector
95
+ run: |
96
+ git clone https://github.com/nedlink/specvector.git /tmp/specvector
97
+ cd /tmp/specvector && bun install
98
+
99
+ - name: Review PR
100
+ env:
101
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
102
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103
+ run: |
104
+ cd ${{ github.workspace }}
105
+ bun run /tmp/specvector/src/index.ts review ${{ github.event.pull_request.number }}
106
+ ```
107
+
108
+ ## Local Development
19
109
 
20
110
  ```bash
21
- npx specvector --help
111
+ # Run tests
112
+ bun test
113
+
114
+ # Type check
115
+ bun run check
116
+
117
+ # Test the agent
118
+ bun run scripts/test-agent.ts
22
119
  ```
23
120
 
24
- ## Documentation
121
+ ## LLM Providers
25
122
 
26
- Full documentation coming soon at [github.com/nedlink/specvector](https://github.com/nedlink/specvector)
123
+ | Provider | Use Case | Setup |
124
+ | ---------- | -------------------------------------- | -------------------- |
125
+ | OpenRouter | Cloud access to Claude, GPT-4, Llama | `OPENROUTER_API_KEY` |
126
+ | Ollama | Self-hosted, air-gapped, privacy-first | `ollama serve` |
127
+
128
+ ```bash
129
+ # Use Ollama instead of OpenRouter
130
+ SPECVECTOR_PROVIDER=ollama SPECVECTOR_MODEL=llama3.2 bun run src/index.ts review 123 --dry-run
131
+ ```
132
+
133
+ ## Architecture
134
+
135
+ ```
136
+ PR Diff ───→ Agent Loop ───→ LLM ───→ Review Comment
137
+ ↓ ↑
138
+ Tools
139
+ (read_file,
140
+ grep,
141
+ list_dir)
142
+ ```
143
+
144
+ The agent can:
145
+
146
+ - **read_file** — Read source code files
147
+ - **grep** — Search for patterns in the codebase
148
+ - **list_dir** — Explore project structure
27
149
 
28
150
  ## License
29
151
 
30
152
  MIT
153
+
154
+ ## Contributing
155
+
156
+ PRs welcome! Run `bun test` before submitting.
package/package.json CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "specvector",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Context-aware AI code review using Model Context Protocol (MCP)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "bin": {
8
8
  "specvector": "src/index.ts"
9
9
  },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
10
15
  "scripts": {
11
16
  "start": "bun src/index.ts",
12
- "test": "bun test"
17
+ "test": "bun test",
18
+ "check": "bun x tsc --noEmit"
13
19
  },
14
20
  "keywords": [
15
21
  "code-review",
@@ -19,17 +25,29 @@
19
25
  "model-context-protocol",
20
26
  "github-actions",
21
27
  "linear",
22
- "anthropic"
28
+ "anthropic",
29
+ "openrouter"
23
30
  ],
24
31
  "author": "Dragos",
25
32
  "license": "MIT",
26
33
  "repository": {
27
34
  "type": "git",
28
- "url": "https://github.com/nedlink/specvector"
35
+ "url": "git+https://github.com/Not-Diamond/specvector.git"
36
+ },
37
+ "homepage": "https://github.com/Not-Diamond/specvector#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/Not-Diamond/specvector/issues"
29
40
  },
30
41
  "engines": {
31
- "node": ">=20.0.0"
42
+ "bun": ">=1.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@larryhudson/linear-mcp-server": "0.1.4"
46
+ },
47
+ "devDependencies": {
48
+ "@types/bun": "latest"
32
49
  },
33
- "dependencies": {},
34
- "devDependencies": {}
50
+ "peerDependencies": {
51
+ "typescript": "^5"
52
+ }
35
53
  }
File without changes
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Agent module - Public API
3
+ */
4
+
5
+ // Re-export types
6
+ export type {
7
+ Tool,
8
+ ToolResult,
9
+ ToolError,
10
+ AgentConfig,
11
+ AgentError,
12
+ AgentErrorCode,
13
+ AgentResult,
14
+ AgentState,
15
+ } from "./types";
16
+
17
+ export {
18
+ DEFAULT_AGENT_CONFIG,
19
+ toolToLLMTool,
20
+ createAgentError,
21
+ AgentErrors,
22
+ } from "./types";
23
+
24
+ // Re-export loop
25
+ export { AgentLoop, createAgentLoop } from "./loop";
26
+
27
+ // Re-export tools
28
+ export * from "./tools";
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Agent Loop - Core agentic execution engine.
3
+ *
4
+ * Implements a simple ReAct-style loop:
5
+ * 1. Send messages to LLM
6
+ * 2. If LLM returns tool_calls, execute them in parallel
7
+ * 3. Add tool results to messages and repeat
8
+ * 4. Stop when LLM returns content without tool_calls
9
+ *
10
+ * Fixes applied:
11
+ * - M1: Parallel tool execution with Promise.all
12
+ * - M2: Tool errors sent back to LLM instead of aborting
13
+ * - L2: Per-tool timeout
14
+ */
15
+
16
+
17
+ import { ok, err } from "../types/result";
18
+ import type { Message, ToolCall } from "../types/llm";
19
+ import type { LLMProvider, LLMError } from "../llm/provider";
20
+ import type {
21
+ Tool,
22
+ AgentConfig,
23
+ AgentResult,
24
+ AgentError,
25
+ } from "./types";
26
+ import {
27
+ DEFAULT_AGENT_CONFIG,
28
+ toolToLLMTool,
29
+ AgentErrors,
30
+ } from "./types";
31
+
32
+ /** Default timeout for individual tool execution (30 seconds) */
33
+ const DEFAULT_TOOL_TIMEOUT_MS = 30_000;
34
+
35
+ /**
36
+ * Agent loop - manages LLM ↔ Tool interaction.
37
+ */
38
+ export class AgentLoop {
39
+ private readonly provider: LLMProvider;
40
+ private readonly tools: Map<string, Tool>;
41
+ private readonly config: Required<AgentConfig>;
42
+
43
+ constructor(
44
+ provider: LLMProvider,
45
+ tools: Tool[],
46
+ config?: AgentConfig
47
+ ) {
48
+ this.provider = provider;
49
+ this.tools = new Map(tools.map((t) => [t.name, t]));
50
+ this.config = { ...DEFAULT_AGENT_CONFIG, ...config };
51
+ }
52
+
53
+ /**
54
+ * Run the agent loop with a task.
55
+ */
56
+ async run(task: string): Promise<AgentResult> {
57
+ const messages: Message[] = [
58
+ { role: "system", content: this.config.systemPrompt },
59
+ { role: "user", content: task },
60
+ ];
61
+
62
+ const startTime = Date.now();
63
+ let iteration = 0;
64
+
65
+ while (iteration < this.config.maxIterations) {
66
+ iteration++;
67
+
68
+ // Check timeout
69
+ if (Date.now() - startTime > this.config.timeoutMs) {
70
+ return err(AgentErrors.timeout());
71
+ }
72
+
73
+ // Get LLM response
74
+ const llmResult = await this.provider.chat(
75
+ messages,
76
+ {
77
+ tools: Array.from(this.tools.values()).map(toolToLLMTool),
78
+ }
79
+ );
80
+
81
+ if (!llmResult.ok) {
82
+ return err(this.mapLLMError(llmResult.error));
83
+ }
84
+
85
+ const response = llmResult.value;
86
+
87
+ // Check for tool calls
88
+ if (response.tool_calls && response.tool_calls.length > 0) {
89
+ // Add assistant message with tool calls
90
+ messages.push({
91
+ role: "assistant",
92
+ content: response.content,
93
+ tool_calls: response.tool_calls,
94
+ });
95
+
96
+ // Execute tools in parallel (M1 fix)
97
+ const toolMessages = await this.executeToolsParallel(response.tool_calls);
98
+
99
+ // Add all tool results to messages (M2 fix: errors as messages, not aborts)
100
+ for (const msg of toolMessages) {
101
+ messages.push(msg);
102
+ }
103
+ } else {
104
+ // No tool calls - we have the final answer
105
+ return ok(response.content ?? "");
106
+ }
107
+ }
108
+
109
+ // Exceeded max iterations
110
+ return err(AgentErrors.maxIterationsExceeded(this.config.maxIterations));
111
+ }
112
+
113
+ /**
114
+ * Execute tool calls in parallel and return tool messages.
115
+ * Tool errors are returned as error messages to the LLM, not thrown.
116
+ */
117
+ private async executeToolsParallel(toolCalls: ToolCall[]): Promise<Message[]> {
118
+ const results = await Promise.all(
119
+ toolCalls.map(call => this.executeSingleTool(call))
120
+ );
121
+ return results;
122
+ }
123
+
124
+ /**
125
+ * Execute a single tool call with timeout.
126
+ * Returns a tool message (success or error content).
127
+ */
128
+ private async executeSingleTool(call: ToolCall): Promise<Message> {
129
+ const tool = this.tools.get(call.name);
130
+
131
+ if (!tool) {
132
+ // Tool not found - return error message to LLM
133
+ return {
134
+ role: "tool" as const,
135
+ content: `Error: Tool '${call.name}' not found. Available tools: ${Array.from(this.tools.keys()).join(", ")}`,
136
+ tool_call_id: call.id,
137
+ name: call.name,
138
+ };
139
+ }
140
+
141
+ // Parse arguments
142
+ let args: Record<string, unknown>;
143
+ try {
144
+ args = JSON.parse(call.arguments);
145
+ } catch {
146
+ return {
147
+ role: "tool" as const,
148
+ content: `Error: Invalid JSON arguments for tool '${call.name}'`,
149
+ tool_call_id: call.id,
150
+ name: call.name,
151
+ };
152
+ }
153
+
154
+ // Execute tool with timeout (L2 fix)
155
+ try {
156
+ const toolResult = await Promise.race([
157
+ tool.execute(args),
158
+ this.createToolTimeout(call.name),
159
+ ]);
160
+
161
+ if (!toolResult.ok) {
162
+ // Tool returned an error - send it back to LLM (M2 fix)
163
+ return {
164
+ role: "tool" as const,
165
+ content: `Error: ${toolResult.error.message}`,
166
+ tool_call_id: call.id,
167
+ name: call.name,
168
+ };
169
+ }
170
+
171
+ // Success
172
+ return {
173
+ role: "tool" as const,
174
+ content: toolResult.value,
175
+ tool_call_id: call.id,
176
+ name: call.name,
177
+ };
178
+ } catch (error) {
179
+ // Timeout or unexpected error
180
+ const message = error instanceof Error ? error.message : "Unknown error";
181
+ return {
182
+ role: "tool" as const,
183
+ content: `Error: ${message}`,
184
+ tool_call_id: call.id,
185
+ name: call.name,
186
+ };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Create a timeout promise for tool execution.
192
+ */
193
+ private createToolTimeout(toolName: string): Promise<never> {
194
+ return new Promise((_, reject) => {
195
+ setTimeout(() => {
196
+ reject(new Error(`Tool '${toolName}' timed out after ${DEFAULT_TOOL_TIMEOUT_MS}ms`));
197
+ }, DEFAULT_TOOL_TIMEOUT_MS);
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Map LLM error to agent error.
203
+ */
204
+ private mapLLMError(error: LLMError): AgentError {
205
+ return AgentErrors.llmError(
206
+ `LLM error: ${error.message}`,
207
+ error.cause
208
+ );
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Create an agent loop.
214
+ */
215
+ export function createAgentLoop(
216
+ provider: LLMProvider,
217
+ tools: Tool[],
218
+ config?: AgentConfig
219
+ ): AgentLoop {
220
+ return new AgentLoop(provider, tools, config);
221
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Find Symbol Tool - Find where a symbol is defined.
3
+ *
4
+ * Uses grep with smart patterns to find function, class, and variable definitions.
5
+ */
6
+
7
+ import { execFile } from "child_process";
8
+ import { promisify } from "util";
9
+ import { resolve } from "path";
10
+ import type { Tool, ToolResult } from "../types";
11
+ import { ok, err } from "../../types/result";
12
+
13
+ const execFileAsync = promisify(execFile);
14
+
15
+ /** Default timeout for symbol search (10 seconds) */
16
+ const DEFAULT_TIMEOUT_MS = 10_000;
17
+
18
+ /** Maximum results to return */
19
+ const MAX_RESULTS = 20;
20
+
21
+ /** Configuration for find_symbol tool */
22
+ export interface FindSymbolConfig {
23
+ /** Working directory for search */
24
+ workingDir: string;
25
+ /** Timeout in milliseconds */
26
+ timeoutMs?: number;
27
+ }
28
+
29
+ /**
30
+ * Create the find_symbol tool.
31
+ */
32
+ export function createFindSymbolTool(config: FindSymbolConfig): Tool {
33
+ const normalizedWorkingDir = resolve(config.workingDir);
34
+ const timeout = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
35
+
36
+ return {
37
+ name: "find_symbol",
38
+ description: "Find where a function, class, or variable is defined. Use this to locate definitions when you see a symbol being used.",
39
+ parameters: {
40
+ type: "object",
41
+ properties: {
42
+ symbol: {
43
+ type: "string",
44
+ description: "Name of the function, class, or variable to find",
45
+ },
46
+ type: {
47
+ type: "string",
48
+ enum: ["function", "class", "variable", "any"],
49
+ description: "Type of symbol to find (optional, defaults to 'any')",
50
+ },
51
+ },
52
+ required: ["symbol"],
53
+ },
54
+
55
+ execute: async (args): Promise<ToolResult> => {
56
+ const symbolArg = args.symbol;
57
+ const typeArg = (args.type as string) ?? "any";
58
+
59
+ if (typeof symbolArg !== "string" || !symbolArg.trim()) {
60
+ return err({
61
+ code: "INVALID_SYMBOL",
62
+ message: "Symbol must be a non-empty string",
63
+ });
64
+ }
65
+
66
+ // SECURITY: Validate symbol name (alphanumeric + underscore only)
67
+ // This prevents: ReDoS (no regex metacharacters like +*?), command injection (no shell chars)
68
+ // Safe to embed in regex patterns after this check passes
69
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(symbolArg)) {
70
+ return err({
71
+ code: "INVALID_SYMBOL",
72
+ message: "Symbol must be a valid identifier",
73
+ });
74
+ }
75
+
76
+ // Build search patterns based on type
77
+ // SECURITY: symbolArg is validated above - only [a-zA-Z0-9_] chars allowed
78
+ const patterns = buildPatterns(symbolArg, typeArg);
79
+
80
+ // Pre-compile regex patterns for efficient labeling (used in result processing)
81
+ const compiledPatterns = patterns.map(p => ({
82
+ regex: new RegExp(p.pattern),
83
+ label: p.label,
84
+ }));
85
+
86
+ const results: Array<{ file: string; line: number; content: string; type: string }> = [];
87
+
88
+ // Combine all patterns into single grep for performance
89
+ const combinedPattern = patterns.map(p => `(${p.pattern})`).join("|");
90
+
91
+ try {
92
+ const { stdout } = await execFileAsync(
93
+ "grep",
94
+ [
95
+ "-rn", // Recursive with line numbers
96
+ "-E", // Extended regex
97
+ "--include=*.ts",
98
+ "--include=*.tsx",
99
+ "--include=*.js",
100
+ "--include=*.jsx",
101
+ "--include=*.py",
102
+ "--include=*.go",
103
+ "--include=*.rs",
104
+ combinedPattern,
105
+ ".",
106
+ ],
107
+ {
108
+ cwd: normalizedWorkingDir,
109
+ timeout,
110
+ maxBuffer: 1024 * 1024, // 1MB
111
+ }
112
+ );
113
+
114
+ // Parse grep output (handles both Unix and Windows paths)
115
+ for (const line of stdout.split("\n")) {
116
+ if (!line.trim() || results.length >= MAX_RESULTS) continue;
117
+
118
+ // Match both ./path and .\path (Windows)
119
+ const match = line.match(/^\.[\\/](.+?):(\d+):(.*)$/);
120
+ if (match) {
121
+ const file = match[1];
122
+ const lineNum = match[2];
123
+ const content = match[3];
124
+
125
+ if (!file || !lineNum) continue;
126
+
127
+ // Skip node_modules and test files for cleaner results
128
+ // Handle both forward and back slashes
129
+ if (file.includes("node_modules") || file.includes(".test.")) {
130
+ continue;
131
+ }
132
+
133
+ // Determine which pattern matched for the label (using pre-compiled regex)
134
+ // Limit line length to prevent ReDoS on malformed grep output
135
+ const safeContent = (content ?? "").slice(0, 500);
136
+ const matchedPattern = compiledPatterns.find(p => p.regex.test(safeContent));
137
+ const label = matchedPattern?.label ?? "definition";
138
+
139
+ results.push({
140
+ file: file.replace(/\\/g, "/"), // Normalize to forward slashes
141
+ line: parseInt(lineNum, 10),
142
+ content: (content ?? "").trim().slice(0, 100),
143
+ type: label,
144
+ });
145
+ }
146
+ }
147
+ } catch (error) {
148
+ // grep returns exit code 1 when no matches - that's expected
149
+ const isExecError = (err: unknown): err is { code: number; stderr?: string } =>
150
+ err !== null && typeof err === "object" && "code" in err;
151
+
152
+ if (isExecError(error) && error.code === 1 && !error.stderr) {
153
+ // No matches - continue to return empty results message
154
+ } else if (error instanceof Error) {
155
+ // Real error - log it for debugging
156
+ console.error(`find_symbol grep error: ${error.message}`);
157
+ }
158
+ }
159
+
160
+ if (results.length === 0) {
161
+ return ok(`No definition found for symbol '${symbolArg}'`);
162
+ }
163
+
164
+ // Format output
165
+ const output = results
166
+ .map(r => `${r.file}:${r.line} (${r.type})\n ${r.content}`)
167
+ .join("\n\n");
168
+
169
+ return ok(`Found ${results.length} definition(s) for '${symbolArg}':\n\n${output}`);
170
+ },
171
+ };
172
+ }
173
+
174
+ interface SearchPattern {
175
+ pattern: string;
176
+ label: string;
177
+ }
178
+
179
+ function buildPatterns(symbol: string, type: string): SearchPattern[] {
180
+ const patterns: SearchPattern[] = [];
181
+
182
+ // Function patterns
183
+ if (type === "function" || type === "any") {
184
+ patterns.push(
185
+ // TypeScript/JavaScript
186
+ { pattern: `^(export\\s+)?(async\\s+)?function\\s+${symbol}\\s*\\(`, label: "function" },
187
+ { pattern: `^(export\\s+)?const\\s+${symbol}\\s*=\\s*(async\\s+)?\\([^)]*\\)\\s*=>`, label: "arrow fn" },
188
+ { pattern: `^(export\\s+)?const\\s+${symbol}\\s*=\\s*(async\\s+)?function`, label: "fn expr" },
189
+ // Python
190
+ { pattern: `^(async\\s+)?def\\s+${symbol}\\s*\\(`, label: "function" },
191
+ // Go
192
+ { pattern: `^func\\s+${symbol}\\s*\\(`, label: "function" },
193
+ // Rust
194
+ { pattern: `^(pub\\s+)?fn\\s+${symbol}\\s*[(<]`, label: "function" },
195
+ );
196
+ }
197
+
198
+ // Class patterns
199
+ if (type === "class" || type === "any") {
200
+ patterns.push(
201
+ // TypeScript/JavaScript
202
+ { pattern: `^(export\\s+)?(abstract\\s+)?class\\s+${symbol}\\b`, label: "class" },
203
+ // Python
204
+ { pattern: `^class\\s+${symbol}\\s*[:(]`, label: "class" },
205
+ // Go (struct)
206
+ { pattern: `^type\\s+${symbol}\\s+struct`, label: "struct" },
207
+ // Rust
208
+ { pattern: `^(pub\\s+)?struct\\s+${symbol}\\b`, label: "struct" },
209
+ );
210
+ }
211
+
212
+ // Variable/const patterns
213
+ if (type === "variable" || type === "any") {
214
+ patterns.push(
215
+ // TypeScript/JavaScript
216
+ { pattern: `^(export\\s+)?(const|let|var)\\s+${symbol}\\s*[=:]`, label: "variable" },
217
+ // Interface/Type
218
+ { pattern: `^(export\\s+)?interface\\s+${symbol}\\b`, label: "interface" },
219
+ { pattern: `^(export\\s+)?type\\s+${symbol}\\s*=`, label: "type" },
220
+ );
221
+ }
222
+
223
+ return patterns;
224
+ }