opencode-rag-plugin 1.19.8 → 1.20.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/dist/chunker/image.d.ts +1 -1
- package/dist/chunker/image.js +30 -22
- package/dist/cli/commands/describe-image.js +2 -1
- package/dist/cli/commands/init-helpers.js +4 -3
- package/dist/cli/types.d.ts +2 -0
- package/dist/mcp/handlers.d.ts +2 -0
- package/dist/mcp/handlers.js +1 -1
- package/dist/mcp/server.js +2 -1
- package/dist/opencode/system-guidance.js +4 -4
- package/dist/opencode/tools.js +4 -1
- package/dist/web/api.js +24 -15
- package/dist/web/pca.d.ts +5 -2
- package/dist/web/pca.js +75 -20
- package/dist/web/ui/assets/ScatterPlot3D-BFWO5sAH.js +4116 -0
- package/dist/web/ui/assets/index-BLzCza1W.css +1 -0
- package/dist/web/ui/assets/index-BdPHzjQh.js +4 -0
- package/dist/web/ui/index.html +2 -2
- package/package.json +4 -1
- package/dist/web/ui/assets/index-BDPYdtA1.js +0 -3
- package/dist/web/ui/assets/index-CKdp79Tw.css +0 -1
package/dist/chunker/image.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function getMimeType(ext: string): string;
|
|
|
18
18
|
* OpenAI, Anthropic, or Google Gemini).
|
|
19
19
|
*/
|
|
20
20
|
export interface ImageVisionProvider {
|
|
21
|
-
describeImage(imageBase64: string, mimeType: string, prompt: string, abort?: AbortSignal): Promise<string>;
|
|
21
|
+
describeImage(imageBase64: string, mimeType: string, prompt: string, systemPrompt?: string, abort?: AbortSignal): Promise<string>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Factory function that creates the appropriate {@link ImageVisionProvider}
|
package/dist/chunker/image.js
CHANGED
|
@@ -53,16 +53,15 @@ class OllamaImageVisionProvider {
|
|
|
53
53
|
this.keepAlive = config.keepAlive;
|
|
54
54
|
this.proxy = config.proxy;
|
|
55
55
|
}
|
|
56
|
-
async describeImage(imageBase64, _mimeType, prompt, abort) {
|
|
56
|
+
async describeImage(imageBase64, _mimeType, prompt, systemPrompt, abort) {
|
|
57
|
+
const messages = [];
|
|
58
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
59
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
60
|
+
}
|
|
61
|
+
messages.push({ role: "user", content: prompt, images: [imageBase64] });
|
|
57
62
|
const body = {
|
|
58
63
|
model: this.model,
|
|
59
|
-
messages
|
|
60
|
-
{
|
|
61
|
-
role: "user",
|
|
62
|
-
content: prompt,
|
|
63
|
-
images: [imageBase64],
|
|
64
|
-
},
|
|
65
|
-
],
|
|
64
|
+
messages,
|
|
66
65
|
stream: false,
|
|
67
66
|
think: this.think,
|
|
68
67
|
options: { num_ctx: this.numCtx },
|
|
@@ -114,22 +113,25 @@ class OpenAIImageVisionProvider {
|
|
|
114
113
|
this.timeoutMs = config.timeoutMs;
|
|
115
114
|
this.proxy = config.proxy;
|
|
116
115
|
}
|
|
117
|
-
async describeImage(imageBase64, mimeType, prompt, abort) {
|
|
116
|
+
async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
|
|
118
117
|
const url = `${this.baseUrl}${this.baseUrl.endsWith("/v1") ? "" : "/v1"}/chat/completions`;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
messages:
|
|
118
|
+
const messages = [];
|
|
119
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
120
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
121
|
+
}
|
|
122
|
+
messages.push({
|
|
123
|
+
role: "user",
|
|
124
|
+
content: [
|
|
125
|
+
{ type: "text", text: prompt },
|
|
122
126
|
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{ type: "text", text: prompt },
|
|
126
|
-
{
|
|
127
|
-
type: "image_url",
|
|
128
|
-
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
|
129
|
-
},
|
|
130
|
-
],
|
|
127
|
+
type: "image_url",
|
|
128
|
+
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
|
131
129
|
},
|
|
132
130
|
],
|
|
131
|
+
});
|
|
132
|
+
const body = {
|
|
133
|
+
model: this.model,
|
|
134
|
+
messages,
|
|
133
135
|
max_tokens: 2048,
|
|
134
136
|
};
|
|
135
137
|
const headers = {
|
|
@@ -182,7 +184,7 @@ class AnthropicImageVisionProvider {
|
|
|
182
184
|
this.timeoutMs = config.timeoutMs;
|
|
183
185
|
this.proxy = config.proxy;
|
|
184
186
|
}
|
|
185
|
-
async describeImage(imageBase64, mimeType, prompt, abort) {
|
|
187
|
+
async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
|
|
186
188
|
const body = {
|
|
187
189
|
model: this.model,
|
|
188
190
|
max_tokens: 2048,
|
|
@@ -199,6 +201,9 @@ class AnthropicImageVisionProvider {
|
|
|
199
201
|
},
|
|
200
202
|
],
|
|
201
203
|
};
|
|
204
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
205
|
+
body.system = systemPrompt;
|
|
206
|
+
}
|
|
202
207
|
const headers = {
|
|
203
208
|
"x-api-key": this.apiKey,
|
|
204
209
|
"anthropic-version": "2023-06-01",
|
|
@@ -248,7 +253,7 @@ class GeminiImageVisionProvider {
|
|
|
248
253
|
this.timeoutMs = config.timeoutMs;
|
|
249
254
|
this.proxy = config.proxy;
|
|
250
255
|
}
|
|
251
|
-
async describeImage(imageBase64, mimeType, prompt, abort) {
|
|
256
|
+
async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
|
|
252
257
|
const body = {
|
|
253
258
|
contents: [
|
|
254
259
|
{
|
|
@@ -265,6 +270,9 @@ class GeminiImageVisionProvider {
|
|
|
265
270
|
},
|
|
266
271
|
],
|
|
267
272
|
};
|
|
273
|
+
if (systemPrompt && systemPrompt.trim().length > 0) {
|
|
274
|
+
body.system_instruction = { parts: [{ text: systemPrompt }] };
|
|
275
|
+
}
|
|
268
276
|
const headers = {
|
|
269
277
|
"Content-Type": "application/json",
|
|
270
278
|
};
|
|
@@ -22,6 +22,7 @@ export function registerDescribeImageCommand(program) {
|
|
|
22
22
|
.description("Describe an image file using a vision model")
|
|
23
23
|
.argument("<filePath>", "path to image file")
|
|
24
24
|
.option("-c, --config <path>", "path to config file")
|
|
25
|
+
.option("-s, --system-prompt <text>", "optional system prompt to steer the description toward specific features")
|
|
25
26
|
.action(async (filePath, options) => {
|
|
26
27
|
try {
|
|
27
28
|
const cwd = process.cwd();
|
|
@@ -56,7 +57,7 @@ export function registerDescribeImageCommand(program) {
|
|
|
56
57
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
57
58
|
const b64 = sized.toString("base64");
|
|
58
59
|
const provider = createImageVisionProvider(imageDescriptionConfig);
|
|
59
|
-
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt);
|
|
60
|
+
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, options.systemPrompt);
|
|
60
61
|
logCliInfo(logFilePath, "describe-image", `\n${c.desc(description)}\n`);
|
|
61
62
|
await cleanupContext(ctx);
|
|
62
63
|
}
|
|
@@ -165,7 +165,7 @@ export function generateSkillFile() {
|
|
|
165
165
|
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
166
166
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
167
167
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
168
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
168
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` (optionally pass `systemPrompt` to focus on specific features) to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
169
169
|
"6. You encounter an error or need a known pitfall → `recall_quirks(query)`",
|
|
170
170
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it",
|
|
171
171
|
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
@@ -188,7 +188,7 @@ export function generateSkillFile() {
|
|
|
188
188
|
"1. **Skeleton first** — call `get_file_skeleton(filePath)` to see structure",
|
|
189
189
|
"2. **Find usages** — call `find_usages(symbolName)` before modifying any symbol",
|
|
190
190
|
"3. **Search** — call `search_semantic(query)` to find relevant code",
|
|
191
|
-
"4. **Describe images** — call `describe_image(filePath)` when context involves an image file",
|
|
191
|
+
"4. **Describe images** — call `describe_image(filePath)` when context involves an image file (pass `systemPrompt` to focus on specific features)",
|
|
192
192
|
"5. **Read** — use the `read` tool on specific line ranges identified above",
|
|
193
193
|
"6. **Edit** — now you have full context to make safe changes",
|
|
194
194
|
"",
|
|
@@ -205,7 +205,7 @@ export function generateSkillFile() {
|
|
|
205
205
|
"- `search_semantic`: `query` (req), `pathHints?`, `languageHints?`, `topK?`",
|
|
206
206
|
"- `get_file_skeleton`: `filePath` (req)",
|
|
207
207
|
"- `find_usages`: `symbolName` (req), `pathHint?`, `topK?`",
|
|
208
|
-
"- `describe_image`: `filePath` (req)",
|
|
208
|
+
"- `describe_image`: `filePath` (req), `systemPrompt?`",
|
|
209
209
|
"- `recall_quirks`: `query` (req), `topK?`, `quirkType?`, `tags?`",
|
|
210
210
|
"- `add_quirk`: `content` (req), `quirkType?`, `tags?`, `sourceRef?`",
|
|
211
211
|
"- `update_quirk`: `id` (req) + at least one of `content?`, `quirkType?`, `tags?`, `confidence?`, `sourceRef?`",
|
|
@@ -216,6 +216,7 @@ export function generateSkillFile() {
|
|
|
216
216
|
"- Use `pathHints` to narrow searches to specific directories",
|
|
217
217
|
"- Use `languageHints` to filter by file type",
|
|
218
218
|
"- `find_usages` is essential before refactoring — it shows every reference",
|
|
219
|
+
"- Pass `systemPrompt` to `describe_image` when you need specific details (e.g. `\"focus on the chart's axes and values\"`)",
|
|
219
220
|
"- If no results appear, the workspace may not be indexed yet — run `opencode-rag index`",
|
|
220
221
|
"- Image descriptions are generated at index time using the configured vision provider; ensure `imageDescription` is configured in `opencode-rag.json` if your project includes images",
|
|
221
222
|
"",
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface CliOptions {
|
|
|
20
20
|
explain?: boolean;
|
|
21
21
|
/** Skip confirmation prompts for destructive operations. */
|
|
22
22
|
yes?: boolean;
|
|
23
|
+
/** Optional system prompt to steer an image description toward specific features. */
|
|
24
|
+
systemPrompt?: string;
|
|
23
25
|
}
|
|
24
26
|
/** Options for the `init` command. */
|
|
25
27
|
export interface InitOptions {
|
package/dist/mcp/handlers.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface FindUsagesResult {
|
|
|
85
85
|
export interface DescribeImageParams {
|
|
86
86
|
/** Path to the image file. */
|
|
87
87
|
filePath: string;
|
|
88
|
+
/** Optional system prompt to steer the description toward specific features. */
|
|
89
|
+
systemPrompt?: string;
|
|
88
90
|
}
|
|
89
91
|
/** Result of an image description operation. */
|
|
90
92
|
export interface DescribeImageResult {
|
package/dist/mcp/handlers.js
CHANGED
|
@@ -247,7 +247,7 @@ export async function handleDescribeImage(params, cfg, worktree, visionProvider)
|
|
|
247
247
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
248
248
|
const b64 = sized.toString("base64");
|
|
249
249
|
const provider = visionProvider ?? (await import("../chunker/image.js")).createImageVisionProvider(imageDescriptionConfig);
|
|
250
|
-
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt);
|
|
250
|
+
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, params.systemPrompt);
|
|
251
251
|
const formatted = [
|
|
252
252
|
`**Image description** — ${params.filePath}`,
|
|
253
253
|
"",
|
package/dist/mcp/server.js
CHANGED
|
@@ -73,8 +73,9 @@ export async function createMcpServer(options) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
|
-
server.tool("describe_image", "Describe an image file using a vision model. Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), and returns a text description of the image contents.", {
|
|
76
|
+
server.tool("describe_image", "Describe an image file using a vision model. Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), and returns a text description of the image contents. Optionally accepts a systemPrompt to steer the description toward specific features.", {
|
|
77
77
|
filePath: z.string().min(1, "An image file path is required."),
|
|
78
|
+
systemPrompt: z.string().optional(),
|
|
78
79
|
}, async (args) => {
|
|
79
80
|
try {
|
|
80
81
|
const result = await handleDescribeImage(args, ctx.config, cwd);
|
|
@@ -17,7 +17,7 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
17
17
|
"- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
|
|
18
18
|
"- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
|
|
19
19
|
"- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
|
|
20
|
-
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
20
|
+
"- `describe_image(filePath, systemPrompt?)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image. Optional `systemPrompt` steers the description toward specific features.",
|
|
21
21
|
"- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
|
|
22
22
|
"- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
|
|
23
23
|
"- `update_quirk(id, ...)`: fix an outdated or wrong quirk (content, type, tags, confidence, source ref). The ID is shown in `recall_quirks` output.",
|
|
@@ -28,7 +28,7 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
28
28
|
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
29
29
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
30
30
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
31
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
31
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` (optionally pass `systemPrompt` to focus on specific features) to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
32
32
|
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
33
33
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
34
34
|
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
@@ -92,7 +92,7 @@ export function buildAgentsMdDirective(opts) {
|
|
|
92
92
|
"- **Search first** — `search_semantic(query)` instead of grep/glob",
|
|
93
93
|
"- **Skeleton before read** — `get_file_skeleton(filePath)` then read specific lines",
|
|
94
94
|
"- **Usages before edit** — `find_usages(symbolName)` before modifying any symbol",
|
|
95
|
-
"- **Images via describe** — `describe_image(filePath)` — never read raw bytes",
|
|
95
|
+
"- **Images via describe** — `describe_image(filePath, systemPrompt?)` — never read raw bytes",
|
|
96
96
|
"- **Recall quirks** — `recall_quirks(query)` when you hit a known pitfall",
|
|
97
97
|
"- **Add quirks** — `add_quirk(content)` when you discover a non-obvious fact",
|
|
98
98
|
"- **Fix quirks** — `update_quirk(id, ...)` / `delete_quirk(id)` when a stored quirk is outdated or wrong",
|
|
@@ -104,7 +104,7 @@ export function buildAgentsMdDirective(opts) {
|
|
|
104
104
|
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
105
105
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
106
106
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
107
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
107
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` (optionally pass `systemPrompt` to focus on specific features) to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
108
108
|
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
109
109
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
110
110
|
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
package/dist/opencode/tools.js
CHANGED
|
@@ -256,9 +256,12 @@ export function createDescribeImageTool(options) {
|
|
|
256
256
|
description: "Describe an image file using a vision model. " +
|
|
257
257
|
"Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), " +
|
|
258
258
|
"and returns a natural language description of what the image shows. " +
|
|
259
|
+
"Optionally accepts a `systemPrompt` to steer the description toward specific features or details you care about " +
|
|
260
|
+
"(e.g. colors, layout, accessibility, text content, specific UI elements). " +
|
|
259
261
|
"Use when the user refers to a screenshot, diagram, mockup, or any image in the workspace.",
|
|
260
262
|
args: {
|
|
261
263
|
filePath: tool.schema.string().min(1, "An image file path is required."),
|
|
264
|
+
systemPrompt: tool.schema.string().optional(),
|
|
262
265
|
},
|
|
263
266
|
async execute(args) {
|
|
264
267
|
try {
|
|
@@ -295,7 +298,7 @@ export function createDescribeImageTool(options) {
|
|
|
295
298
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
296
299
|
const b64 = sized.toString("base64");
|
|
297
300
|
const provider = visionProvider ?? createImageVisionProvider(imageDescriptionConfig);
|
|
298
|
-
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt);
|
|
301
|
+
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, args.systemPrompt);
|
|
299
302
|
return {
|
|
300
303
|
title: `Image description — ${args.filePath}`,
|
|
301
304
|
output: `**${args.filePath}**\n\n${description}\n\n_Generated with ${imageDescriptionConfig.provider}/${imageDescriptionConfig.model}_`,
|
package/dist/web/api.js
CHANGED
|
@@ -602,17 +602,18 @@ function redactKeys(obj) {
|
|
|
602
602
|
}
|
|
603
603
|
}
|
|
604
604
|
/**
|
|
605
|
-
* Project chunk embeddings to 2D via PCA for the Embedding Space Explorer.
|
|
606
|
-
* Capped at 5000 chunks and memoized per (
|
|
605
|
+
* Project chunk embeddings to 2D/3D via PCA for the Embedding Space Explorer.
|
|
606
|
+
* Capped at 5000 chunks and memoized per (maxChunks, dims) so the
|
|
607
607
|
* O(n·dim²) computation does not run on every visit.
|
|
608
608
|
*/
|
|
609
609
|
let projectionCache = null;
|
|
610
610
|
async function handleEmbeddingProjection(store, params) {
|
|
611
611
|
const rawMaxChunks = parseInt(params.get("maxChunks") ?? "5000", 10);
|
|
612
612
|
const maxChunks = Number.isFinite(rawMaxChunks) ? Math.min(5000, Math.max(1, rawMaxChunks)) : 5000;
|
|
613
|
+
const dims = parseInt(params.get("dims") ?? "2", 10) === 3 ? 3 : 2;
|
|
613
614
|
try {
|
|
614
615
|
// Invalidated after a reindex pass completes (see handleReindex)
|
|
615
|
-
const cacheKey = `${maxChunks}`;
|
|
616
|
+
const cacheKey = `${maxChunks}:${dims}`;
|
|
616
617
|
if (projectionCache && projectionCache.key === cacheKey) {
|
|
617
618
|
return { status: 200, body: projectionCache.body };
|
|
618
619
|
}
|
|
@@ -622,23 +623,31 @@ async function handleEmbeddingProjection(store, params) {
|
|
|
622
623
|
return { status: 200, body: projectionCache.body };
|
|
623
624
|
}
|
|
624
625
|
if (chunks.length === 1) {
|
|
625
|
-
const
|
|
626
|
+
const point = { id: chunks[0].id, x: 0.5, y: 0.5, filePath: chunks[0].filePath, startLine: chunks[0].startLine, endLine: chunks[0].endLine, language: chunks[0].language, description: chunks[0].description };
|
|
627
|
+
if (dims === 3)
|
|
628
|
+
point.z = 0.5;
|
|
629
|
+
const body = { points: [point], totalChunks: 1, displayedChunks: 1 };
|
|
626
630
|
projectionCache = { key: cacheKey, body };
|
|
627
631
|
return { status: 200, body };
|
|
628
632
|
}
|
|
629
633
|
const { computePCA } = await import("./pca.js");
|
|
630
634
|
const vectors = chunks.map(c => c.embedding);
|
|
631
|
-
const projected = computePCA(vectors);
|
|
632
|
-
const points = chunks.map((c, i) =>
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
635
|
+
const projected = computePCA(vectors, dims);
|
|
636
|
+
const points = chunks.map((c, i) => {
|
|
637
|
+
const point = {
|
|
638
|
+
id: c.id,
|
|
639
|
+
x: projected[i].x,
|
|
640
|
+
y: projected[i].y,
|
|
641
|
+
filePath: c.filePath,
|
|
642
|
+
startLine: c.startLine,
|
|
643
|
+
endLine: c.endLine,
|
|
644
|
+
language: c.language,
|
|
645
|
+
description: c.description,
|
|
646
|
+
};
|
|
647
|
+
if (dims === 3)
|
|
648
|
+
point.z = projected[i].z;
|
|
649
|
+
return point;
|
|
650
|
+
});
|
|
642
651
|
const body = { points, totalChunks: chunks.length, displayedChunks: points.length };
|
|
643
652
|
projectionCache = { key: cacheKey, body };
|
|
644
653
|
return { status: 200, body };
|
package/dist/web/pca.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-contained, zero-dependency PCA implementation for
|
|
2
|
+
* Self-contained, zero-dependency PCA implementation for embedding projection.
|
|
3
|
+
* Supports projecting to 2 or 3 dimensions (top-K eigenvectors via power
|
|
4
|
+
* iteration + deflation).
|
|
3
5
|
*/
|
|
4
|
-
export declare function computePCA(vectors: number[][]): {
|
|
6
|
+
export declare function computePCA(vectors: number[][], dims?: 2 | 3): {
|
|
5
7
|
x: number;
|
|
6
8
|
y: number;
|
|
9
|
+
z?: number;
|
|
7
10
|
}[];
|
package/dist/web/pca.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-contained, zero-dependency PCA implementation for
|
|
2
|
+
* Self-contained, zero-dependency PCA implementation for embedding projection.
|
|
3
|
+
* Supports projecting to 2 or 3 dimensions (top-K eigenvectors via power
|
|
4
|
+
* iteration + deflation).
|
|
3
5
|
*/
|
|
4
|
-
export function computePCA(vectors) {
|
|
6
|
+
export function computePCA(vectors, dims = 2) {
|
|
5
7
|
const n = vectors.length;
|
|
6
8
|
if (n === 0)
|
|
7
9
|
return [];
|
|
8
10
|
const dim = vectors[0].length;
|
|
9
11
|
if (n === 1)
|
|
10
|
-
return [{ x: 0.5, y: 0.5 }];
|
|
12
|
+
return dims === 3 ? [{ x: 0.5, y: 0.5, z: 0.5 }] : [{ x: 0.5, y: 0.5 }];
|
|
11
13
|
// 1. Compute column means
|
|
12
14
|
const means = new Array(dim).fill(0);
|
|
13
15
|
for (let i = 0; i < n; i++) {
|
|
@@ -19,7 +21,9 @@ export function computePCA(vectors) {
|
|
|
19
21
|
means[j] /= n;
|
|
20
22
|
// 2. Center data
|
|
21
23
|
const centered = vectors.map(v => v.map((val, j) => val - means[j]));
|
|
22
|
-
// 3. Compute covariance matrix (dim x dim)
|
|
24
|
+
// 3. Compute covariance matrix (dim x dim); fill the upper triangle then
|
|
25
|
+
// mirror it so the matrix is symmetric (power iteration needs a symmetric
|
|
26
|
+
// operator to find the true principal axes).
|
|
23
27
|
const cov = Array.from({ length: dim }, () => new Array(dim).fill(0));
|
|
24
28
|
for (let i = 0; i < n; i++) {
|
|
25
29
|
for (let j = 0; j < dim; j++) {
|
|
@@ -31,23 +35,42 @@ export function computePCA(vectors) {
|
|
|
31
35
|
for (let j = 0; j < dim; j++) {
|
|
32
36
|
for (let k = j; k < dim; k++) {
|
|
33
37
|
cov[j][k] /= n - 1;
|
|
38
|
+
cov[k][j] = cov[j][k];
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
|
-
// 4.
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// 4. Find the top-K eigenvectors: power iteration, then deflate the
|
|
42
|
+
// covariance by each discovered eigenvector before finding the next.
|
|
43
|
+
// Once the remaining matrix is numerically ~zero (degenerate / low-rank
|
|
44
|
+
// input), the rest of the PCs are zero vectors — this keeps PC2/PC3 from
|
|
45
|
+
// picking up deflation noise and avoids NaN from a 0/0 deflation.
|
|
46
|
+
const pcs = [];
|
|
47
|
+
let deflated = cov;
|
|
48
|
+
const threshold = maxAbs(cov) * 1e-12;
|
|
49
|
+
for (let pc = 0; pc < dims; pc++) {
|
|
50
|
+
if (maxAbs(deflated) <= threshold) {
|
|
51
|
+
pcs.push(new Array(dim).fill(0));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const eigen = powerIteration(deflated, dim, 50);
|
|
55
|
+
pcs.push(eigen);
|
|
56
|
+
deflated = deflate(deflated, eigen);
|
|
57
|
+
}
|
|
58
|
+
// 5. Project centered data onto the PCs
|
|
59
|
+
const projected = centered.map(v => {
|
|
60
|
+
const point = { x: 0, y: 0, z: 0 };
|
|
61
|
+
for (let pc = 0; pc < dims; pc++) {
|
|
62
|
+
const s = v.reduce((sum, val, j) => sum + val * pcs[pc][j], 0);
|
|
63
|
+
if (pc === 0)
|
|
64
|
+
point.x = s;
|
|
65
|
+
else if (pc === 1)
|
|
66
|
+
point.y = s;
|
|
67
|
+
else
|
|
68
|
+
point.z = s;
|
|
69
|
+
}
|
|
70
|
+
return point;
|
|
43
71
|
});
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
const projected = centered.map(v => ({
|
|
47
|
-
x: v.reduce((sum, val, j) => sum + val * pc1[j], 0),
|
|
48
|
-
y: v.reduce((sum, val, j) => sum + val * pc2[j], 0),
|
|
49
|
-
}));
|
|
50
|
-
// 6. Normalize to [0, 1]
|
|
72
|
+
// 6. Normalize to [0, 1]. 2D keeps per-axis normalization (unchanged);
|
|
73
|
+
// 3D uses the max extent across all axes so the cube stays proportional.
|
|
51
74
|
const xs = projected.map(p => p.x);
|
|
52
75
|
const ys = projected.map(p => p.y);
|
|
53
76
|
const minX = Math.min(...xs);
|
|
@@ -56,11 +79,43 @@ export function computePCA(vectors) {
|
|
|
56
79
|
const maxY = Math.max(...ys);
|
|
57
80
|
const rangeX = maxX - minX || 1;
|
|
58
81
|
const rangeY = maxY - minY || 1;
|
|
82
|
+
if (dims === 2) {
|
|
83
|
+
return projected.map(p => ({
|
|
84
|
+
x: (p.x - minX) / rangeX,
|
|
85
|
+
y: (p.y - minY) / rangeY,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
const zs = projected.map(p => p.z);
|
|
89
|
+
const minZ = Math.min(...zs);
|
|
90
|
+
const maxZ = Math.max(...zs);
|
|
91
|
+
const maxRange = Math.max(rangeX, rangeY, maxZ - minZ || 1);
|
|
59
92
|
return projected.map(p => ({
|
|
60
|
-
x: (p.x - minX) /
|
|
61
|
-
y: (p.y - minY) /
|
|
93
|
+
x: (p.x - minX) / maxRange,
|
|
94
|
+
y: (p.y - minY) / maxRange,
|
|
95
|
+
z: (p.z - minZ) / maxRange,
|
|
62
96
|
}));
|
|
63
97
|
}
|
|
98
|
+
/** Subtract the outer-product contribution of a principal component from a symmetric matrix. */
|
|
99
|
+
function deflate(matrix, pc) {
|
|
100
|
+
const pcNormSq = pc.reduce((sum, v) => sum + v * v, 0);
|
|
101
|
+
return matrix.map((row, i) => {
|
|
102
|
+
const pcDotRow = pc.reduce((sum, v, idx) => sum + v * matrix[i][idx], 0);
|
|
103
|
+
const scale = pcNormSq > 1e-12 ? pcDotRow / pcNormSq : 0;
|
|
104
|
+
return row.map((val, j) => val - scale * pc[j]);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Largest absolute entry of a matrix. */
|
|
108
|
+
function maxAbs(matrix) {
|
|
109
|
+
let m = 0;
|
|
110
|
+
for (const row of matrix) {
|
|
111
|
+
for (const val of row) {
|
|
112
|
+
const abs = Math.abs(val);
|
|
113
|
+
if (abs > m)
|
|
114
|
+
m = abs;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return m;
|
|
118
|
+
}
|
|
64
119
|
/** Power iteration to find the dominant eigenvector of a symmetric matrix. */
|
|
65
120
|
function powerIteration(matrix, dim, maxIter) {
|
|
66
121
|
let v = new Array(dim).fill(0).map(() => Math.random() * 2 - 1);
|