specvector 0.0.1 → 0.1.2

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,149 @@
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
+ - **Fetches Linear tickets** for requirements context
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Install
19
+
20
+ ```bash
21
+ # Via npm/bun (recommended)
22
+ bunx specvector review 123 --dry-run
23
+
24
+ # Or clone for development
25
+ git clone https://github.com/Not-Diamond/specvector.git
26
+ cd specvector && bun install
27
+ ```
28
+
29
+ ### 2. Set up API Key
30
+
31
+ ```bash
32
+ # Add your OpenRouter API key
33
+ export OPENROUTER_API_KEY=your-key-here
34
+ ```
35
+
36
+ Get a key at [openrouter.ai](https://openrouter.ai)
37
+
38
+ ### 3. Run a Review
39
+
40
+ ```bash
41
+ # Review a PR (dry run - no posting)
42
+ bunx specvector review 123 --dry-run
43
+
44
+ # Or with mock review (no LLM calls)
45
+ bunx specvector review 123 --mock --dry-run
46
+ ```
47
+
48
+ ## CLI Usage
49
+
50
+ ```
51
+ SpecVector CLI v0.1.0
52
+ Context-aware AI code review
53
+
54
+ USAGE:
55
+ bunx specvector review <pr-number> Review a pull request
56
+ bunx specvector review <pr-number> --dry-run Preview review without posting
57
+ bunx specvector review <pr-number> --mock Use mock review (no LLM)
58
+ bunx specvector --help Show this help
59
+ bunx specvector --version Show version
10
60
 
11
- Unlike generic AI review tools that only see the diff, SpecVector understands:
61
+ ENVIRONMENT:
62
+ OPENROUTER_API_KEY API key for OpenRouter
63
+ LINEAR_API_TOKEN API key for Linear (optional)
64
+ SPECVECTOR_PROVIDER LLM provider (openrouter or ollama)
65
+ SPECVECTOR_MODEL Model to use
66
+ ```
67
+
68
+ ## GitHub Action
69
+
70
+ Add to your repository:
71
+
72
+ ```yaml
73
+ name: SpecVector Code Review
74
+
75
+ on:
76
+ pull_request:
77
+ types: [opened, synchronize, reopened]
78
+
79
+ permissions:
80
+ contents: read
81
+ pull-requests: write
12
82
 
13
- - What ticket the PR is for
14
- - Your architectural decisions
15
- - Your database schema
16
- - Your coding standards
83
+ jobs:
84
+ review:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ with:
89
+ fetch-depth: 0
17
90
 
18
- ## Installation
91
+ - uses: oven-sh/setup-bun@v2
92
+
93
+ - name: Review PR
94
+ env:
95
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
96
+ LINEAR_API_TOKEN: ${{ secrets.LINEAR_API_TOKEN }} # Optional
97
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
98
+ run: bunx specvector review ${{ github.event.pull_request.number }}
99
+ ```
100
+
101
+ ## Configuration
102
+
103
+ Create `.specvector/config.yaml` in your repo:
104
+
105
+ ```yaml
106
+ provider: openrouter
107
+ model: anthropic/claude-sonnet-4.5
108
+ strictness: normal # strict | normal | lenient
109
+ ```
110
+
111
+ ## LLM Providers
112
+
113
+ | Provider | Use Case | Setup |
114
+ | ---------- | -------------------------------------- | -------------------- |
115
+ | OpenRouter | Cloud access to Claude, GPT-4, Llama | `OPENROUTER_API_KEY` |
116
+ | Ollama | Self-hosted, air-gapped, privacy-first | `ollama serve` |
19
117
 
20
118
  ```bash
21
- npx specvector --help
119
+ # Use Ollama instead of OpenRouter
120
+ SPECVECTOR_PROVIDER=ollama SPECVECTOR_MODEL=llama3.2 bunx specvector review 123 --dry-run
22
121
  ```
23
122
 
24
- ## Documentation
123
+ ## Architecture
124
+
125
+ ```
126
+ PR Diff ───→ Agent Loop ───→ LLM ───→ Review Comment
127
+ ↓ ↑
128
+ Tools Linear MCP
129
+ (read_file, → (ticket context)
130
+ grep,
131
+ list_dir)
132
+ ```
25
133
 
26
- Full documentation coming soon at [github.com/nedlink/specvector](https://github.com/nedlink/specvector)
134
+ ## Local Development
135
+
136
+ ```bash
137
+ # Run tests (158 passing)
138
+ bun test
139
+
140
+ # Type check
141
+ bun run check
142
+ ```
27
143
 
28
144
  ## License
29
145
 
30
146
  MIT
147
+
148
+ ## Contributing
149
+
150
+ PRs welcome! Run `bun test` before submitting.
package/package.json CHANGED
@@ -1,15 +1,24 @@
1
1
  {
2
2
  "name": "specvector",
3
- "version": "0.0.1",
3
+ "version": "0.1.2",
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",
19
+ "release:patch": "npm version patch && npm publish && git push --follow-tags",
20
+ "release:minor": "npm version minor && npm publish && git push --follow-tags",
21
+ "release:major": "npm version major && npm publish && git push --follow-tags"
13
22
  },
14
23
  "keywords": [
15
24
  "code-review",
@@ -19,17 +28,29 @@
19
28
  "model-context-protocol",
20
29
  "github-actions",
21
30
  "linear",
22
- "anthropic"
31
+ "anthropic",
32
+ "openrouter"
23
33
  ],
24
34
  "author": "Dragos",
25
35
  "license": "MIT",
26
36
  "repository": {
27
37
  "type": "git",
28
- "url": "https://github.com/nedlink/specvector"
38
+ "url": "git+https://github.com/Not-Diamond/specvector.git"
39
+ },
40
+ "homepage": "https://github.com/Not-Diamond/specvector#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/Not-Diamond/specvector/issues"
29
43
  },
30
44
  "engines": {
31
- "node": ">=20.0.0"
45
+ "bun": ">=1.0.0"
46
+ },
47
+ "dependencies": {
48
+ "@larryhudson/linear-mcp-server": "0.1.4"
49
+ },
50
+ "devDependencies": {
51
+ "@types/bun": "latest"
32
52
  },
33
- "dependencies": {},
34
- "devDependencies": {}
53
+ "peerDependencies": {
54
+ "typescript": "^5"
55
+ }
35
56
  }
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
+ }