codetrap 0.1.7 → 0.1.9
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 +132 -98
- package/docs/installation.md +61 -63
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +2 -3
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +19 -17
- package/plugins/codetrap-agent/hooks.json +2 -2
- package/{skills → plugins/codetrap-agent/skills}/codetrap-add/SKILL.md +10 -4
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +14 -3
- package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +52 -9
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +74 -6
- package/{skills → plugins/codetrap-agent/skills}/codetrap-search/SKILL.md +6 -5
- package/plugins/codetrap-agent/templates/AGENTS.codetrap-maintainer.md +15 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +16 -5
- package/scripts/release-preflight.ts +15 -0
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +172 -68
- package/src/db/embedding-queries.ts +230 -48
- package/src/db/queries.ts +0 -25
- package/src/db/repository.ts +32 -21
- package/src/db/schema.ts +80 -0
- package/src/index.ts +34 -4
- package/src/lib/codex-setup.ts +247 -0
- package/src/lib/command-requests.ts +112 -1
- package/src/lib/config.ts +57 -7
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +42 -12
- package/src/lib/embedder.ts +118 -3
- package/src/lib/embedding-health.ts +3 -1
- package/src/lib/embedding-job.ts +3 -0
- package/src/lib/embedding-management.ts +65 -0
- package/src/lib/embedding-runtime.ts +177 -0
- package/src/lib/output-json.ts +0 -2
- package/src/lib/scope-context.ts +12 -6
- package/src/lib/scope-migration.ts +2 -1
- package/src/lib/scope.ts +0 -2
- package/src/lib/search-eval.ts +38 -18
- package/src/lib/search-policy-sweep.ts +563 -0
- package/src/lib/search-policy.ts +0 -4
- package/src/lib/search-service.ts +14 -15
- package/src/lib/session-candidate-document.ts +175 -0
- package/src/lib/session-candidate-scope.ts +6 -0
- package/src/lib/session-capture.ts +298 -32
- package/src/lib/session-codec.ts +1 -8
- package/src/lib/session-operations.ts +83 -60
- package/src/lib/session-review.ts +327 -0
- package/src/lib/session-store.ts +87 -73
- package/src/lib/store.ts +74 -10
- package/src/lib/string-list.ts +3 -0
- package/src/lib/text-lines.ts +7 -0
- package/src/lib/trap-search-document.ts +2 -1
- package/src/lib/value-types.ts +3 -0
- package/src/web/client-review.ts +171 -0
- package/src/web/client-script.ts +426 -51
- package/src/web/client-shell.ts +414 -0
- package/src/web/client-text.ts +112 -0
- package/src/web/project-registry.ts +3 -5
- package/src/web/server.ts +117 -103
- package/src/web/static.ts +364 -19
- package/skills/codetrap-capture-external/SKILL.md +0 -62
- package/skills/codetrap-check/SKILL.md +0 -69
- package/src/lib/embedding-index.ts +0 -53
package/src/lib/embedder.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface EmbeddingConfig {
|
|
|
19
19
|
|
|
20
20
|
export interface StoredEmbedding {
|
|
21
21
|
trap_id: number;
|
|
22
|
+
profile_id: string;
|
|
22
23
|
provider: string;
|
|
23
24
|
model: string;
|
|
24
25
|
dimensions: number;
|
|
@@ -91,9 +92,119 @@ export class JinaEmbedder implements EmbeddingProvider {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
export
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
export type OllamaEmbedderOptions = {
|
|
96
|
+
endpoint?: string;
|
|
97
|
+
model?: string;
|
|
98
|
+
dimensions?: number;
|
|
99
|
+
fetch?: FetchLike;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type OllamaProviderHealth = {
|
|
103
|
+
ok: boolean;
|
|
104
|
+
message?: string;
|
|
105
|
+
command?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
109
|
+
|
|
110
|
+
export const DEFAULT_OLLAMA_ENDPOINT = "http://127.0.0.1:11434";
|
|
111
|
+
export const DEFAULT_OLLAMA_MODEL = "qwen3-embedding:0.6b";
|
|
112
|
+
export const DEFAULT_OLLAMA_DIMENSIONS = 1024;
|
|
113
|
+
|
|
114
|
+
export class OllamaEmbedder implements EmbeddingProvider {
|
|
115
|
+
readonly provider = "ollama";
|
|
116
|
+
readonly model: string;
|
|
117
|
+
readonly dimensions: number;
|
|
118
|
+
private readonly endpoint: string;
|
|
119
|
+
private readonly fetchImpl: FetchLike;
|
|
120
|
+
|
|
121
|
+
constructor(options: OllamaEmbedderOptions = {}) {
|
|
122
|
+
this.endpoint = normalizeOllamaEndpoint(options.endpoint ?? DEFAULT_OLLAMA_ENDPOINT);
|
|
123
|
+
this.model = options.model ?? DEFAULT_OLLAMA_MODEL;
|
|
124
|
+
this.dimensions = options.dimensions ?? DEFAULT_OLLAMA_DIMENSIONS;
|
|
125
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async embed(texts: string[], _task: EmbeddingTask): Promise<Float32Array[]> {
|
|
129
|
+
if (texts.length === 0) return [];
|
|
130
|
+
|
|
131
|
+
const response = await this.fetchImpl(`${this.endpoint}/api/embed`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
model: this.model,
|
|
138
|
+
input: texts,
|
|
139
|
+
dimensions: this.dimensions,
|
|
140
|
+
truncate: true,
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const body = await response.text();
|
|
146
|
+
throw new Error(`Ollama embeddings request failed (${response.status}): ${body}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const payload = (await response.json()) as {
|
|
150
|
+
embeddings?: number[][];
|
|
151
|
+
};
|
|
152
|
+
const rows = payload.embeddings ?? [];
|
|
153
|
+
if (rows.length !== texts.length) {
|
|
154
|
+
throw new Error(`Ollama embeddings returned ${rows.length} vectors for ${texts.length} inputs.`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return rows.map((embedding) => {
|
|
158
|
+
if (!Array.isArray(embedding)) {
|
|
159
|
+
throw new Error("Ollama embeddings response is missing an embedding vector.");
|
|
160
|
+
}
|
|
161
|
+
if (embedding.length !== this.dimensions) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Ollama embeddings returned ${embedding.length} dimensions, expected ${this.dimensions}.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return Float32Array.from(embedding);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async health(): Promise<OllamaProviderHealth> {
|
|
171
|
+
try {
|
|
172
|
+
const response = await this.fetchImpl(`${this.endpoint}/api/tags`);
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
message: `Ollama is not reachable at ${this.endpoint} (${response.status}).`,
|
|
177
|
+
command: "ollama list",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const payload = (await response.json()) as {
|
|
182
|
+
models?: { name?: string; model?: string }[];
|
|
183
|
+
};
|
|
184
|
+
const models = payload.models ?? [];
|
|
185
|
+
const installed = models.some((model) => model.name === this.model || model.model === this.model);
|
|
186
|
+
if (!installed) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
message: `Ollama model ${this.model} is not installed.`,
|
|
190
|
+
command: `ollama pull ${this.model}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { ok: true };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
message: `Ollama is not reachable at ${this.endpoint}: ${message}`,
|
|
200
|
+
command: "ollama list",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeOllamaEndpoint(endpoint: string): string {
|
|
207
|
+
return endpoint.replace(/\/+$/, "");
|
|
97
208
|
}
|
|
98
209
|
|
|
99
210
|
export function embeddingConfig(provider: EmbeddingProvider): EmbeddingConfig {
|
|
@@ -105,6 +216,10 @@ export function embeddingConfig(provider: EmbeddingProvider): EmbeddingConfig {
|
|
|
105
216
|
};
|
|
106
217
|
}
|
|
107
218
|
|
|
219
|
+
export function embeddingProfileId(config: EmbeddingConfig): string {
|
|
220
|
+
return `${config.provider}:${config.model}:${config.dimensions}:p${config.passageVersion}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
108
223
|
export function encodeEmbedding(embedding: Float32Array): Buffer {
|
|
109
224
|
return Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
110
225
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { embeddingProfileId, type EmbeddingConfig } from "./embedder";
|
|
2
2
|
|
|
3
3
|
export type EmbeddingStateCounts = {
|
|
4
4
|
total: number;
|
|
@@ -13,6 +13,7 @@ export type EmbeddingStateSummary = EmbeddingStateCounts & {
|
|
|
13
13
|
model: string | null;
|
|
14
14
|
dimensions: number | null;
|
|
15
15
|
passage_version: number | null;
|
|
16
|
+
profile_id: string | null;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export type EmbeddingStatsResult = {
|
|
@@ -33,6 +34,7 @@ export function summarizeEmbeddingState(
|
|
|
33
34
|
model: config?.model ?? null,
|
|
34
35
|
dimensions: config?.dimensions ?? null,
|
|
35
36
|
passage_version: config?.passageVersion ?? null,
|
|
37
|
+
profile_id: config ? embeddingProfileId(config) : null,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
|
package/src/lib/embedding-job.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Trap } from "../domain/trap";
|
|
2
2
|
import {
|
|
3
3
|
embeddingConfig,
|
|
4
|
+
embeddingProfileId,
|
|
4
5
|
type EmbeddingConfig,
|
|
5
6
|
type EmbeddingProvider,
|
|
6
7
|
type StoredEmbedding,
|
|
@@ -35,6 +36,7 @@ export async function runEmbeddingJob(
|
|
|
35
36
|
opts: EmbeddingJobOptions = {}
|
|
36
37
|
): Promise<EmbeddingJobResult> {
|
|
37
38
|
const config = embeddingConfig(provider);
|
|
39
|
+
const profileId = embeddingProfileId(config);
|
|
38
40
|
const total = adapter.countEmbeddable({ scope: opts.scope, category: opts.category });
|
|
39
41
|
const traps = adapter.trapsNeedingEmbeddings(config, opts);
|
|
40
42
|
if (traps.length === 0) return { generated: 0, skipped: total, batches: 0 };
|
|
@@ -53,6 +55,7 @@ export async function runEmbeddingJob(
|
|
|
53
55
|
for (let i = 0; i < batch.length; i++) {
|
|
54
56
|
adapter.saveEmbedding({
|
|
55
57
|
trap_id: batch[i].id,
|
|
58
|
+
profile_id: profileId,
|
|
56
59
|
provider: config.provider,
|
|
57
60
|
model: config.model,
|
|
58
61
|
dimensions: config.dimensions,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ConfigWriteResult, EmbeddingSettings } from "./config";
|
|
2
|
+
import type { EmbeddingScopeStatus, TrapEmbeddingProfiles, TrapEmbeddingStatus } from "./store";
|
|
3
|
+
import type { EmbeddingProfileSummary } from "../db/embedding-queries";
|
|
4
|
+
|
|
5
|
+
export type EmbeddingsUseResult = ConfigWriteResult & {
|
|
6
|
+
embeddings: EmbeddingSettings;
|
|
7
|
+
next_action: {
|
|
8
|
+
command: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function formatEmbeddingStatusText(status: TrapEmbeddingStatus): string {
|
|
14
|
+
const runtime = status.runtime.provider
|
|
15
|
+
? `${status.runtime.provider}/${status.runtime.model} (${status.runtime.dimensions}d)`
|
|
16
|
+
: "unconfigured";
|
|
17
|
+
const lines = [
|
|
18
|
+
`Runtime: ${runtime}`,
|
|
19
|
+
`Available: ${status.runtime.available ? "yes" : "no"}`,
|
|
20
|
+
`Active profile: ${status.runtime.profile_id ?? "(none)"}`,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if (status.runtime.setup_action) {
|
|
24
|
+
lines.push(`Next setup: ${status.runtime.setup_action.command}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lines.push(...scopeStatusLines("Project", status.project));
|
|
28
|
+
lines.push(...scopeStatusLines("Global", status.global));
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatEmbeddingProfilesText(profiles: TrapEmbeddingProfiles): string {
|
|
33
|
+
const lines = [
|
|
34
|
+
...profileSectionLines("Project", profiles.project),
|
|
35
|
+
...profileSectionLines("Global", profiles.global),
|
|
36
|
+
];
|
|
37
|
+
return lines.length > 0 ? lines.join("\n") : "No stored embedding profiles.";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatEmbeddingsUseText(result: EmbeddingsUseResult): string {
|
|
41
|
+
return [
|
|
42
|
+
`Configured embeddings provider: ${result.embeddings.provider}`,
|
|
43
|
+
`Config: ${result.path}`,
|
|
44
|
+
`Next: ${result.next_action.command}`,
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scopeStatusLines(label: string, status: EmbeddingScopeStatus | null): string[] {
|
|
49
|
+
if (!status) return [];
|
|
50
|
+
return [
|
|
51
|
+
"",
|
|
52
|
+
`${label}: total ${status.total}, fresh ${status.fresh}, stale ${status.stale}, missing ${status.missing}`,
|
|
53
|
+
...profileSectionLines("Stored profiles", status.profiles),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function profileSectionLines(label: string, profiles: EmbeddingProfileSummary[] | null): string[] {
|
|
58
|
+
if (!profiles || profiles.length === 0) return [];
|
|
59
|
+
return [
|
|
60
|
+
`${label}:`,
|
|
61
|
+
...profiles.map((profile) =>
|
|
62
|
+
` - ${profile.id} (${profile.embedding_count} embeddings, updated ${profile.updated_at ?? "unknown"})`
|
|
63
|
+
),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_OLLAMA_DIMENSIONS,
|
|
3
|
+
DEFAULT_OLLAMA_ENDPOINT,
|
|
4
|
+
DEFAULT_OLLAMA_MODEL,
|
|
5
|
+
EmbeddingProviderUnavailableError,
|
|
6
|
+
JinaEmbedder,
|
|
7
|
+
OllamaEmbedder,
|
|
8
|
+
embeddingConfig,
|
|
9
|
+
embeddingProfileId,
|
|
10
|
+
type EmbeddingConfig,
|
|
11
|
+
type EmbeddingProvider,
|
|
12
|
+
} from "./embedder";
|
|
13
|
+
import { loadCodetrapConfig, type CodetrapConfig, type EmbeddingProviderSetting } from "./config";
|
|
14
|
+
|
|
15
|
+
export type EmbeddingRuntimeInput = EmbeddingRuntime | EmbeddingProvider | undefined;
|
|
16
|
+
|
|
17
|
+
export type EmbeddingRuntimeAction = {
|
|
18
|
+
command: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type EmbeddingRuntimeStatus = {
|
|
23
|
+
available: boolean;
|
|
24
|
+
provider: string | null;
|
|
25
|
+
model: string | null;
|
|
26
|
+
dimensions: number | null;
|
|
27
|
+
passage_version: number | null;
|
|
28
|
+
profile_id: string | null;
|
|
29
|
+
setup_action: EmbeddingRuntimeAction | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class EmbeddingRuntime {
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly adapter?: EmbeddingProvider,
|
|
35
|
+
private readonly setupProvider: EmbeddingProviderSetting = "ollama",
|
|
36
|
+
private readonly configuredConfig: EmbeddingConfig | null = null
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
static fromEnvironment(
|
|
40
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
41
|
+
config: CodetrapConfig = loadCodetrapConfig()
|
|
42
|
+
): EmbeddingRuntime {
|
|
43
|
+
const provider = configuredProvider(env, config);
|
|
44
|
+
if (provider === "ollama") {
|
|
45
|
+
return new EmbeddingRuntime(new OllamaEmbedder(ollamaOptions(env, config)), "ollama");
|
|
46
|
+
}
|
|
47
|
+
if (provider === "jina") {
|
|
48
|
+
const apiKey = env.JINA_API_KEY;
|
|
49
|
+
const configuredConfig = embeddingConfig(new JinaEmbedder(apiKey ?? ""));
|
|
50
|
+
return new EmbeddingRuntime(apiKey ? new JinaEmbedder(apiKey) : undefined, "jina", configuredConfig);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const apiKey = env.JINA_API_KEY;
|
|
54
|
+
if (apiKey) return new EmbeddingRuntime(new JinaEmbedder(apiKey), "jina");
|
|
55
|
+
return new EmbeddingRuntime(undefined, "ollama");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
provider(): EmbeddingProvider | undefined {
|
|
59
|
+
return this.adapter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
available(): boolean {
|
|
63
|
+
return this.adapter !== undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
config(): EmbeddingConfig | null {
|
|
67
|
+
return this.adapter ? embeddingConfig(this.adapter) : this.configuredConfig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
requireProvider(message?: string): EmbeddingProvider {
|
|
71
|
+
if (!this.adapter) throw this.unavailableError(message);
|
|
72
|
+
return this.adapter;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
unavailableError(message?: string): EmbeddingProviderUnavailableError {
|
|
76
|
+
return new EmbeddingProviderUnavailableError(
|
|
77
|
+
message ?? "Embedding provider is unavailable. Configure an embedding provider or use --mode fts."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setupAction(): EmbeddingRuntimeAction | null {
|
|
82
|
+
if (this.adapter) return null;
|
|
83
|
+
if (this.setupProvider === "jina") {
|
|
84
|
+
return {
|
|
85
|
+
command: "export JINA_API_KEY=<your-jina-api-key>",
|
|
86
|
+
reason: "Enable Jina semantic and hybrid search; otherwise use --mode fts.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
command: "export CODETRAP_EMBEDDING_PROVIDER=ollama",
|
|
91
|
+
reason: "Enable local semantic and hybrid search after installing Ollama and pulling qwen3-embedding:0.6b; otherwise use --mode fts.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
status(): EmbeddingRuntimeStatus {
|
|
96
|
+
const config = this.config();
|
|
97
|
+
return {
|
|
98
|
+
available: this.available(),
|
|
99
|
+
provider: config?.provider ?? null,
|
|
100
|
+
model: config?.model ?? null,
|
|
101
|
+
dimensions: config?.dimensions ?? null,
|
|
102
|
+
passage_version: config?.passageVersion ?? null,
|
|
103
|
+
profile_id: config ? embeddingProfileId(config) : null,
|
|
104
|
+
setup_action: this.setupAction(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async health(): Promise<EmbeddingRuntimeStatus> {
|
|
109
|
+
const status = this.status();
|
|
110
|
+
if (!this.adapter || !(this.adapter instanceof OllamaEmbedder)) return status;
|
|
111
|
+
|
|
112
|
+
const health = await this.adapter.health();
|
|
113
|
+
if (health.ok) return status;
|
|
114
|
+
return {
|
|
115
|
+
...status,
|
|
116
|
+
available: false,
|
|
117
|
+
setup_action: {
|
|
118
|
+
command: health.command ?? `ollama pull ${this.adapter.model}`,
|
|
119
|
+
reason: health.message ?? "Ollama embedding provider is unavailable.",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function defaultEmbeddingRuntime(
|
|
126
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
127
|
+
config?: CodetrapConfig
|
|
128
|
+
): EmbeddingRuntime {
|
|
129
|
+
return EmbeddingRuntime.fromEnvironment(env, config);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function embeddingRuntimeFrom(input: EmbeddingRuntimeInput): EmbeddingRuntime {
|
|
133
|
+
return input instanceof EmbeddingRuntime ? input : new EmbeddingRuntime(input);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function configuredProvider(
|
|
137
|
+
env: NodeJS.ProcessEnv,
|
|
138
|
+
config: CodetrapConfig
|
|
139
|
+
): EmbeddingProviderSetting | undefined {
|
|
140
|
+
if (config.embeddings?.provider) return config.embeddings.provider;
|
|
141
|
+
if (env.CODETRAP_EMBEDDING_PROVIDER) return parseProviderEnv(env.CODETRAP_EMBEDDING_PROVIDER);
|
|
142
|
+
if (config.embeddings?.endpoint || config.embeddings?.model || config.embeddings?.dimensions) return "ollama";
|
|
143
|
+
if (env.CODETRAP_OLLAMA_ENDPOINT || env.CODETRAP_OLLAMA_MODEL || env.CODETRAP_OLLAMA_DIMENSIONS) {
|
|
144
|
+
return "ollama";
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ollamaOptions(env: NodeJS.ProcessEnv, config: CodetrapConfig) {
|
|
150
|
+
return {
|
|
151
|
+
endpoint: normalizeEndpointSetting(
|
|
152
|
+
config.embeddings?.endpoint ?? env.CODETRAP_OLLAMA_ENDPOINT ?? env.OLLAMA_HOST ?? DEFAULT_OLLAMA_ENDPOINT
|
|
153
|
+
),
|
|
154
|
+
model: config.embeddings?.model ?? env.CODETRAP_OLLAMA_MODEL ?? DEFAULT_OLLAMA_MODEL,
|
|
155
|
+
dimensions:
|
|
156
|
+
config.embeddings?.dimensions ??
|
|
157
|
+
parseOptionalPositiveInt(env.CODETRAP_OLLAMA_DIMENSIONS, "CODETRAP_OLLAMA_DIMENSIONS") ??
|
|
158
|
+
DEFAULT_OLLAMA_DIMENSIONS,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseProviderEnv(value: string): EmbeddingProviderSetting {
|
|
163
|
+
if (value === "ollama" || value === "jina") return value;
|
|
164
|
+
throw new Error(`Invalid CODETRAP_EMBEDDING_PROVIDER: ${value}. Expected ollama or jina.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseOptionalPositiveInt(value: string | undefined, label: string): number | undefined {
|
|
168
|
+
if (!value) return undefined;
|
|
169
|
+
const parsed = Number.parseInt(value, 10);
|
|
170
|
+
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
171
|
+
throw new Error(`Invalid ${label}: expected a positive integer.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeEndpointSetting(endpoint: string): string {
|
|
175
|
+
if (/^https?:\/\//.test(endpoint)) return endpoint;
|
|
176
|
+
return `http://${endpoint}`;
|
|
177
|
+
}
|
package/src/lib/output-json.ts
CHANGED
package/src/lib/scope-context.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { openGlobal, openProject } from "../db/connection";
|
|
|
2
2
|
import { TrapRepository } from "../db/repository";
|
|
3
3
|
import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
|
|
4
4
|
import type { Scope } from "./constants";
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import type { EmbeddingRuntime } from "./embedding-runtime";
|
|
6
|
+
import type { RankingConfig } from "./search-policy";
|
|
7
|
+
import { findProjectRoot, getGlobalDB } from "./scope";
|
|
8
|
+
import { resolveScopePath, ScopePathResolver } from "./scope-path";
|
|
8
9
|
|
|
9
10
|
const scopePath = new ScopePathResolver();
|
|
10
11
|
|
|
@@ -36,7 +37,12 @@ export class ScopedRepositoryContext {
|
|
|
36
37
|
private globalRepository?: TrapRepository;
|
|
37
38
|
private projectRepository?: TrapRepository;
|
|
38
39
|
|
|
39
|
-
constructor(
|
|
40
|
+
constructor(
|
|
41
|
+
cwd = process.cwd(),
|
|
42
|
+
private readonly embeddings?: EmbeddingRuntime,
|
|
43
|
+
private readonly home?: string,
|
|
44
|
+
private readonly ranking?: RankingConfig
|
|
45
|
+
) {
|
|
40
46
|
this.context = createScopeContext(cwd, home);
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -87,14 +93,14 @@ export class ScopedRepositoryContext {
|
|
|
87
93
|
throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
|
|
88
94
|
}
|
|
89
95
|
if (!this.projectRepository) {
|
|
90
|
-
this.projectRepository = new TrapRepository(openProject(projectRoot), this.
|
|
96
|
+
this.projectRepository = new TrapRepository(openProject(projectRoot), this.embeddings, this.ranking);
|
|
91
97
|
}
|
|
92
98
|
return this.projectRepository;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
private globalRepo(): TrapRepository {
|
|
96
102
|
if (!this.globalRepository) {
|
|
97
|
-
this.globalRepository = new TrapRepository(openGlobal(this.home), this.
|
|
103
|
+
this.globalRepository = new TrapRepository(openGlobal(this.home), this.embeddings, this.ranking);
|
|
98
104
|
}
|
|
99
105
|
return this.globalRepository;
|
|
100
106
|
}
|
|
@@ -3,7 +3,8 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { openDatabase } from "../db/connection";
|
|
4
4
|
import { TrapRepository } from "../db/repository";
|
|
5
5
|
import type { TrapExportRecord } from "../domain/trap";
|
|
6
|
-
import { findProjectRoot
|
|
6
|
+
import { findProjectRoot } from "./scope";
|
|
7
|
+
import { resolveScopePath } from "./scope-path";
|
|
7
8
|
import {
|
|
8
9
|
backupScopeDatabase,
|
|
9
10
|
buildScopeMaintenancePaths,
|
package/src/lib/scope.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
|
|
4
4
|
import { defaultScopePathResolver, resolveScopePath, ScopePathResolver } from "./scope-path";
|
|
5
5
|
|
|
6
|
-
export { resolveScopePath, ScopePathResolver } from "./scope-path";
|
|
7
|
-
|
|
8
6
|
export function getGlobalDir(homeDir = homedir()): string {
|
|
9
7
|
const dir = defaultScopePathResolver.join(homeDir, CODETRAP_DIR);
|
|
10
8
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
package/src/lib/search-eval.ts
CHANGED
|
@@ -3,13 +3,19 @@ import { openDatabase } from "../db/connection";
|
|
|
3
3
|
import { TrapRepository } from "../db/repository";
|
|
4
4
|
import type { TrapInput, TrapSearchResult } from "../domain/trap";
|
|
5
5
|
import { SEARCH_MODES, type SearchMode } from "./constants";
|
|
6
|
+
import { isRecord } from "./value-types";
|
|
6
7
|
import {
|
|
7
|
-
createDefaultEmbeddingProvider,
|
|
8
|
-
embeddingConfig,
|
|
9
8
|
type EmbeddingConfig,
|
|
10
9
|
type EmbeddingProvider,
|
|
11
10
|
type EmbeddingTask,
|
|
12
11
|
} from "./embedder";
|
|
12
|
+
import {
|
|
13
|
+
EmbeddingRuntime,
|
|
14
|
+
defaultEmbeddingRuntime,
|
|
15
|
+
embeddingRuntimeFrom,
|
|
16
|
+
type EmbeddingRuntimeInput,
|
|
17
|
+
} from "./embedding-runtime";
|
|
18
|
+
import type { RankingConfig } from "./search-policy";
|
|
13
19
|
|
|
14
20
|
export type PhaseGate = "phase0" | "phase1" | "phase4" | "dogfood";
|
|
15
21
|
export const DOGFOOD_JUDGMENTS = ["useful_hit", "miss", "noisy_hit", "no_relevant_trap"] as const;
|
|
@@ -79,6 +85,10 @@ export type SearchEvalReport = {
|
|
|
79
85
|
next_actions: SearchEvalNextAction[];
|
|
80
86
|
};
|
|
81
87
|
|
|
88
|
+
export type SearchEvalDetailedReport = Omit<SearchEvalReport, "mode" | "fixture" | "next_actions"> & {
|
|
89
|
+
cases: EvalCaseReport[];
|
|
90
|
+
};
|
|
91
|
+
|
|
82
92
|
export type RecordDogfoodResult = {
|
|
83
93
|
success: true;
|
|
84
94
|
fixture: string;
|
|
@@ -115,8 +125,8 @@ export function recordDogfoodCase(fixturePath: string, jsonInput: string | undef
|
|
|
115
125
|
|
|
116
126
|
export async function reportDogfood(fixturePath: string, live: boolean): Promise<SearchEvalReport> {
|
|
117
127
|
const fixture = readEvalFixture(fixturePath);
|
|
118
|
-
const
|
|
119
|
-
const evaluated = await evaluateSearchFixture(fixture,
|
|
128
|
+
const runtime = live ? defaultEmbeddingRuntime() : new EmbeddingRuntime(new EvalEmbedder());
|
|
129
|
+
const evaluated = await evaluateSearchFixture(fixture, runtime);
|
|
120
130
|
const mode: SearchEvalReport["mode"] = live ? "live" : "deterministic";
|
|
121
131
|
const report: Omit<SearchEvalReport, "next_actions"> = {
|
|
122
132
|
mode,
|
|
@@ -131,19 +141,30 @@ export async function reportDogfood(fixturePath: string, live: boolean): Promise
|
|
|
131
141
|
|
|
132
142
|
export async function evaluateSearchFixture(
|
|
133
143
|
fixture: EvalFixture,
|
|
134
|
-
|
|
144
|
+
embeddings: EmbeddingRuntimeInput,
|
|
145
|
+
ranking?: RankingConfig
|
|
135
146
|
): Promise<Omit<SearchEvalReport, "mode" | "fixture" | "next_actions">> {
|
|
136
|
-
const
|
|
147
|
+
const { cases: _cases, ...report } = await evaluateSearchFixtureCases(fixture, embeddings, ranking);
|
|
148
|
+
return report;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function evaluateSearchFixtureCases(
|
|
152
|
+
fixture: EvalFixture,
|
|
153
|
+
embeddings: EmbeddingRuntimeInput,
|
|
154
|
+
ranking?: RankingConfig
|
|
155
|
+
): Promise<SearchEvalDetailedReport> {
|
|
156
|
+
const runtime = embeddingRuntimeFrom(embeddings);
|
|
157
|
+
const repo = fixtureRepository(fixture, runtime, ranking);
|
|
137
158
|
|
|
138
159
|
let providerError: string | null = null;
|
|
139
|
-
if (
|
|
160
|
+
if (runtime.available()) {
|
|
140
161
|
try {
|
|
141
162
|
await repo.ensureEmbeddings();
|
|
142
163
|
} catch (error) {
|
|
143
164
|
providerError = errorMessage(error);
|
|
144
165
|
}
|
|
145
166
|
}
|
|
146
|
-
const searchRepo = providerError ? fixtureRepository(fixture, undefined) : repo;
|
|
167
|
+
const searchRepo = providerError ? fixtureRepository(fixture, undefined, ranking) : repo;
|
|
147
168
|
|
|
148
169
|
const cases: EvalCaseReport[] = [];
|
|
149
170
|
let hybridFallbackCount = 0;
|
|
@@ -154,7 +175,7 @@ export async function evaluateSearchFixture(
|
|
|
154
175
|
const results = await searchRepo.search(item.query, { mode: item.mode, limit: 5 });
|
|
155
176
|
const report = caseReport(item, fixture, results);
|
|
156
177
|
cases.push(report);
|
|
157
|
-
if (item.mode === "hybrid" && (!
|
|
178
|
+
if (item.mode === "hybrid" && (!runtime.available() || hasSemanticFallback(results))) {
|
|
158
179
|
hybridFallbackCount++;
|
|
159
180
|
}
|
|
160
181
|
} catch (error) {
|
|
@@ -169,8 +190,8 @@ export async function evaluateSearchFixture(
|
|
|
169
190
|
const noisyHits = cases.filter((item) => item.judgment === "noisy_hit");
|
|
170
191
|
const metrics = aggregateMetrics(cases);
|
|
171
192
|
return {
|
|
172
|
-
provider:
|
|
173
|
-
semantic_available:
|
|
193
|
+
provider: runtime.config(),
|
|
194
|
+
semantic_available: runtime.available() && providerError === null,
|
|
174
195
|
provider_error: providerError,
|
|
175
196
|
total_cases: cases.length,
|
|
176
197
|
metrics: {
|
|
@@ -190,6 +211,7 @@ export async function evaluateSearchFixture(
|
|
|
190
211
|
failures,
|
|
191
212
|
misses,
|
|
192
213
|
noisy_hits: noisyHits,
|
|
214
|
+
cases,
|
|
193
215
|
};
|
|
194
216
|
}
|
|
195
217
|
|
|
@@ -240,8 +262,9 @@ function buildSearchEvalNextActions(
|
|
|
240
262
|
): SearchEvalNextAction[] {
|
|
241
263
|
const actions: SearchEvalNextAction[] = [];
|
|
242
264
|
if (report.mode === "live" && !report.semantic_available) {
|
|
243
|
-
|
|
244
|
-
|
|
265
|
+
const action = defaultEmbeddingRuntime().setupAction();
|
|
266
|
+
if (action) actions.push({
|
|
267
|
+
command: action.command,
|
|
245
268
|
reason: "Enable live semantic checks, then rerun bun run eval:dogfood -- report --live.",
|
|
246
269
|
});
|
|
247
270
|
}
|
|
@@ -309,8 +332,8 @@ function formatNextActions(actions: SearchEvalNextAction[]): string[] {
|
|
|
309
332
|
return actions.map((action) => ` - ${action.command} # ${action.reason}`);
|
|
310
333
|
}
|
|
311
334
|
|
|
312
|
-
function fixtureRepository(fixture: EvalFixture,
|
|
313
|
-
const repo = new TrapRepository(openDatabase(":memory:"),
|
|
335
|
+
function fixtureRepository(fixture: EvalFixture, embeddings: EmbeddingRuntimeInput, ranking?: RankingConfig): TrapRepository {
|
|
336
|
+
const repo = new TrapRepository(openDatabase(":memory:"), embeddings, ranking);
|
|
314
337
|
for (const trap of fixture.traps) repo.add(trap);
|
|
315
338
|
return repo;
|
|
316
339
|
}
|
|
@@ -471,9 +494,6 @@ function providerLabel(provider: EmbeddingConfig | null): string {
|
|
|
471
494
|
return `${provider.provider}/${provider.model}`;
|
|
472
495
|
}
|
|
473
496
|
|
|
474
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
475
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
476
|
-
}
|
|
477
497
|
|
|
478
498
|
function round(value: number): number {
|
|
479
499
|
return Math.round(value * 10000) / 10000;
|