codetrap 0.1.6 → 0.1.7
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 +10 -1
- package/package.json +1 -1
- package/src/commands/workflow.ts +49 -7
- package/src/db/connection.ts +6 -6
- package/src/index.ts +4 -4
- package/src/lib/command-requests.ts +22 -0
- package/src/lib/doctor.ts +62 -2
- package/src/lib/embed-output.ts +26 -0
- package/src/lib/scope-context.ts +7 -7
- package/src/lib/scope.ts +4 -4
- package/src/lib/search-eval.ts +100 -7
- package/src/lib/session-operations.ts +37 -0
- package/src/lib/session-store.ts +108 -0
- package/src/lib/store.ts +7 -3
- package/src/web/client-script.ts +1168 -0
- package/src/web/client-text.ts +335 -0
- package/src/web/server.ts +70 -11
- package/src/web/static.ts +224 -472
package/README.md
CHANGED
|
@@ -160,7 +160,8 @@ codetrap/
|
|
|
160
160
|
| `repair-scope` | Move legacy mis-scoped project traps into the current project (dry-run by default, `--apply` to mutate, `--json`) |
|
|
161
161
|
| `migrate-project` | Move project traps between initialized projects (`--from-project-path`, `--to-project-path`, dry-run by default, `--apply`, `--json`) |
|
|
162
162
|
| `embed` | Generate embeddings (requires JINA_API_KEY) |
|
|
163
|
-
| `session` | Start a development session, append notes, promote explicit structured trap notes into candidates,
|
|
163
|
+
| `session` | Start a development session, append notes, promote explicit structured trap notes into candidates, accept/reject candidates, and clean up session files |
|
|
164
|
+
| `web` | Start the local review and trap library console |
|
|
164
165
|
| `serve` | Start MCP server |
|
|
165
166
|
|
|
166
167
|
### Session Mode
|
|
@@ -179,6 +180,14 @@ codetrap session accept cand-001
|
|
|
179
180
|
|
|
180
181
|
`session accept` writes the confirmed lesson through `TrapOperations`, attaches session evidence, and checks similar active traps before saving. `--edit-json` is applied before the conflict check, so edits to scope/module/title/tags/path globs affect both the saved trap and conflict detection. If a possible conflict is found, the candidate keeps its edited trap shape and conflict diagnostics; use `--accept-anyway` to keep both traps or `--supersedes <trap-id>` to preserve lifecycle history.
|
|
181
182
|
|
|
183
|
+
Session maintenance commands keep temporary files from becoming stale context:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
codetrap session cleanup <session-id> --deleted-trap-candidates
|
|
187
|
+
codetrap session delete <session-id>
|
|
188
|
+
codetrap session prune --older-than 90d --apply
|
|
189
|
+
```
|
|
190
|
+
|
|
182
191
|
## Agent Integration
|
|
183
192
|
|
|
184
193
|
For AI coding agents, use the CLI as the default integration path:
|
package/package.json
CHANGED
package/src/commands/workflow.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../lib/scope-migration";
|
|
11
11
|
import { TrapOperations } from "../lib/trap-operations";
|
|
12
12
|
import { buildDoctorReport, formatDoctorText } from "../lib/doctor";
|
|
13
|
+
import { formatEmbedText } from "../lib/embed-output";
|
|
13
14
|
import { searchDefaultsFromConfig } from "../lib/config";
|
|
14
15
|
import { SessionStore } from "../lib/session-store";
|
|
15
16
|
import { SessionOperations, type SessionConflictResult } from "../lib/session-operations";
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
sessionIdRequestFromArgs,
|
|
45
46
|
sessionListRequestFromArgs,
|
|
46
47
|
sessionNoteRequestFromArgs,
|
|
48
|
+
sessionPruneRequestFromArgs,
|
|
47
49
|
sessionRejectRequestFromArgs,
|
|
48
50
|
sessionShowRequestFromArgs,
|
|
49
51
|
sessionStartRequestFromArgs,
|
|
@@ -380,12 +382,7 @@ async function cmdEmbed(args: string[], store: TrapStore): Promise<CommandResult
|
|
|
380
382
|
const { opts } = parseArgs(args);
|
|
381
383
|
try {
|
|
382
384
|
const result = await store.ensureEmbeddings(embedRequestFromArgs(opts));
|
|
383
|
-
return textResult(
|
|
384
|
-
...result.scopes.map((scoped) =>
|
|
385
|
-
`[${scoped.scope}] embeddings generated: ${scoped.generated}, skipped: ${scoped.skipped}, batches: ${scoped.batches}`
|
|
386
|
-
),
|
|
387
|
-
`Total generated: ${result.generated}, skipped: ${result.skipped}, batches: ${result.batches}`,
|
|
388
|
-
].join("\n"));
|
|
385
|
+
return textResult(formatEmbedText(result));
|
|
389
386
|
} catch (error) {
|
|
390
387
|
return errorFrom(error);
|
|
391
388
|
}
|
|
@@ -424,8 +421,14 @@ async function cmdSession(args: string[], store: TrapStore, trapOperations: Trap
|
|
|
424
421
|
return cmdSessionAccept(rest, sessions);
|
|
425
422
|
case "reject":
|
|
426
423
|
return cmdSessionReject(rest, sessions);
|
|
424
|
+
case "delete":
|
|
425
|
+
return cmdSessionDelete(rest, sessions);
|
|
426
|
+
case "prune":
|
|
427
|
+
return cmdSessionPrune(rest, sessions);
|
|
428
|
+
case "cleanup":
|
|
429
|
+
return cmdSessionCleanup(rest, sessions);
|
|
427
430
|
default:
|
|
428
|
-
return errorResult("Usage: codetrap session <start|note|status|list|show|notes|close|candidates|candidate|accept|reject>");
|
|
431
|
+
return errorResult("Usage: codetrap session <start|note|status|list|show|notes|close|candidates|candidate|accept|reject|delete|prune|cleanup>");
|
|
429
432
|
}
|
|
430
433
|
} catch (error) {
|
|
431
434
|
return errorFrom(error);
|
|
@@ -589,6 +592,45 @@ function cmdSessionReject(args: string[], sessions: SessionOperations): CommandR
|
|
|
589
592
|
return textResult(`Rejected ${rejected.candidate.id}.`);
|
|
590
593
|
}
|
|
591
594
|
|
|
595
|
+
function cmdSessionDelete(args: string[], sessions: SessionOperations): CommandResult {
|
|
596
|
+
const { opts, positionals } = parseArgs(args);
|
|
597
|
+
const request = sessionShowRequestFromArgs(positionals);
|
|
598
|
+
const result = sessions.deleteSession(request.sessionId);
|
|
599
|
+
const payload = { success: result.deleted, ...result };
|
|
600
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
601
|
+
return textResult(`Deleted session ${result.session_id}.`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function cmdSessionPrune(args: string[], sessions: SessionOperations): CommandResult {
|
|
605
|
+
const { opts } = parseArgs(args);
|
|
606
|
+
const result = sessions.pruneSessions(sessionPruneRequestFromArgs(opts));
|
|
607
|
+
if (opts.json !== undefined) return jsonResult(result);
|
|
608
|
+
const verb = result.dry_run ? "Would delete" : "Deleted";
|
|
609
|
+
const lines = [`${verb} ${result.dry_run ? result.sessions.length : result.deleted_count} session(s) older than ${result.cutoff}.`];
|
|
610
|
+
if (result.dry_run && result.sessions.length > 0) {
|
|
611
|
+
lines.push("Run with --apply to delete them.");
|
|
612
|
+
}
|
|
613
|
+
lines.push(...result.sessions.map((session) => `- ${session.id} [${session.status}] ${session.goal}`));
|
|
614
|
+
return textResult(lines.join("\n"));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function cmdSessionCleanup(args: string[], sessions: SessionOperations): CommandResult {
|
|
618
|
+
const { opts, positionals } = parseArgs(args);
|
|
619
|
+
if (opts["deleted-trap-candidates"] === undefined && opts.deleted_trap_candidates === undefined) {
|
|
620
|
+
return errorResult("Usage: codetrap session cleanup [session-id] --deleted-trap-candidates [--json]");
|
|
621
|
+
}
|
|
622
|
+
const request = sessionIdRequestFromArgs(positionals);
|
|
623
|
+
const result = sessions.cleanupDeletedTrapCandidates(request.sessionId);
|
|
624
|
+
const payload = {
|
|
625
|
+
success: true,
|
|
626
|
+
session_id: result.session.id,
|
|
627
|
+
removed_count: result.removed_count,
|
|
628
|
+
removed_candidate_ids: result.removed_candidate_ids,
|
|
629
|
+
};
|
|
630
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
631
|
+
return textResult(`Removed ${result.removed_count} deleted-trap candidate(s) from session ${result.session.id}.`);
|
|
632
|
+
}
|
|
633
|
+
|
|
592
634
|
function formatStatsText(stats: ReturnType<TrapOperations["getStats"]>): string {
|
|
593
635
|
const sections: string[] = [];
|
|
594
636
|
if (stats.project) {
|
package/src/db/connection.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
|
|
|
2
2
|
import { getGlobalDB, getProjectDB } from "../lib/scope";
|
|
3
3
|
import { initSchema } from "./schema";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const globalDBs = new Map<string, Database>();
|
|
6
6
|
const projectDBs = new Map<string, Database>();
|
|
7
7
|
|
|
8
8
|
export function openDatabase(path = ":memory:"): Database {
|
|
@@ -11,12 +11,12 @@ export function openDatabase(path = ":memory:"): Database {
|
|
|
11
11
|
return db;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function openGlobal(): Database {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export function openGlobal(home?: string): Database {
|
|
15
|
+
const path = getGlobalDB(home);
|
|
16
|
+
if (!globalDBs.has(path)) {
|
|
17
|
+
globalDBs.set(path, openDatabase(path));
|
|
18
18
|
}
|
|
19
|
-
return
|
|
19
|
+
return globalDBs.get(path)!;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function openProject(root: string): Database {
|
package/src/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ function showHelp(): void {
|
|
|
47
47
|
console.log(" supersede_trap Mark one trap as superseded by another");
|
|
48
48
|
console.log(" embed Generate embeddings for semantic search");
|
|
49
49
|
console.log(" session Record implementation notes and capture candidate traps");
|
|
50
|
-
console.log(" web Start local
|
|
50
|
+
console.log(" web Start local review and trap library console");
|
|
51
51
|
console.log(" export Export traps as JSON");
|
|
52
52
|
console.log(" import <file.json> Import traps from JSON");
|
|
53
53
|
console.log(" stats Show statistics");
|
|
@@ -68,9 +68,9 @@ function showHelp(): void {
|
|
|
68
68
|
console.log(" --no-rerank Disable query-aware search reranking");
|
|
69
69
|
console.log(" --ranking-signals Include search ranking diagnostics in JSON cards");
|
|
70
70
|
console.log(" --batch-size <n> Embedding generation batch size");
|
|
71
|
-
console.log(" --project <path> Project path for web
|
|
72
|
-
console.log(" --host <host> Host for web
|
|
73
|
-
console.log(" --port <n> Port for web
|
|
71
|
+
console.log(" --project <path> Project path for web console");
|
|
72
|
+
console.log(" --host <host> Host for web console (default 127.0.0.1)");
|
|
73
|
+
console.log(" --port <n> Port for web console (default 4737)");
|
|
74
74
|
console.log(" --json JSON output for search/show/list/stats/doctor; JSON input for add/edit");
|
|
75
75
|
console.log(" --output-json JSON output for add/edit when --json is used as input");
|
|
76
76
|
console.log(" --from-project-path <path> Source project path for scope repair/migration");
|
|
@@ -63,6 +63,11 @@ export type SessionRejectRequest = SessionCandidateRequest & {
|
|
|
63
63
|
reason?: string;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
export type SessionPruneRequest = {
|
|
67
|
+
olderThanDays: number;
|
|
68
|
+
apply: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
66
71
|
export type SessionNoteStdin = {
|
|
67
72
|
isTTY: boolean;
|
|
68
73
|
read: () => string;
|
|
@@ -205,6 +210,15 @@ export function sessionRejectRequestFromArgs(positionals: string[], args: RawArg
|
|
|
205
210
|
};
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
export function sessionPruneRequestFromArgs(args: RawArgs): SessionPruneRequest {
|
|
214
|
+
const olderThan = stringOption(args, "older_than", "older-than");
|
|
215
|
+
if (!olderThan) throw new Error("--older-than is required.");
|
|
216
|
+
return {
|
|
217
|
+
olderThanDays: parseDurationDays(olderThan),
|
|
218
|
+
apply: flagPresent(args, "apply"),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
function stringOption(args: RawArgs, ...keys: string[]): string | undefined {
|
|
209
223
|
for (const key of keys) {
|
|
210
224
|
const value = args[key];
|
|
@@ -282,6 +296,14 @@ function jsonObjectOption(args: RawArgs, key: string): Record<string, unknown> |
|
|
|
282
296
|
}
|
|
283
297
|
}
|
|
284
298
|
|
|
299
|
+
function parseDurationDays(value: string): number {
|
|
300
|
+
const match = value.trim().match(/^(\d+)\s*(d|day|days)?$/i);
|
|
301
|
+
if (!match) throw new Error(`Invalid duration: ${value}. Use a value like 90d.`);
|
|
302
|
+
const days = Number.parseInt(match[1], 10);
|
|
303
|
+
if (!Number.isInteger(days) || days <= 0) throw new Error(`Invalid duration: ${value}. Use a positive day count.`);
|
|
304
|
+
return days;
|
|
305
|
+
}
|
|
306
|
+
|
|
285
307
|
function requiredPositional(positionals: string[], index: number, name: string): string {
|
|
286
308
|
const value = positionals[index];
|
|
287
309
|
if (!value) throw new Error(`${name} is required.`);
|
package/src/lib/doctor.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { TrapStore } from "./store";
|
|
2
2
|
import type { TrapOperations } from "./trap-operations";
|
|
3
|
-
import type { EmbeddingStatsResult, HybridFallbackReason } from "./embedding-health";
|
|
3
|
+
import type { EmbeddingStateSummary, EmbeddingStatsResult, HybridFallbackReason } from "./embedding-health";
|
|
4
4
|
import { createScopeContext } from "./scope-context";
|
|
5
5
|
import { hybridFallbackReason } from "./embedding-health";
|
|
6
6
|
|
|
7
|
+
export type DoctorNextAction = {
|
|
8
|
+
command: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
7
12
|
export type DoctorReport = {
|
|
8
13
|
cwd: string;
|
|
9
14
|
project_root: string | null;
|
|
@@ -23,6 +28,7 @@ export type DoctorReport = {
|
|
|
23
28
|
global_db_project_traps: ReturnType<TrapStore["diagnostics"]>["mis_scoped_traps"]["global_db_project_traps"];
|
|
24
29
|
};
|
|
25
30
|
};
|
|
31
|
+
next_actions: DoctorNextAction[];
|
|
26
32
|
mcp_hint: string;
|
|
27
33
|
};
|
|
28
34
|
|
|
@@ -35,6 +41,7 @@ export function buildDoctorReport(
|
|
|
35
41
|
const stats = operations.getStats();
|
|
36
42
|
const embeddings = operations.getEmbeddingStats();
|
|
37
43
|
const semanticAvailable = store.hasEmbeddingProvider();
|
|
44
|
+
const diagnostics = store.diagnostics();
|
|
38
45
|
|
|
39
46
|
return {
|
|
40
47
|
...scope,
|
|
@@ -48,8 +55,9 @@ export function buildDoctorReport(
|
|
|
48
55
|
fallback_reason: hybridFallbackReason(semanticAvailable, embeddings),
|
|
49
56
|
},
|
|
50
57
|
diagnostics: {
|
|
51
|
-
mis_scoped_traps:
|
|
58
|
+
mis_scoped_traps: diagnostics.mis_scoped_traps,
|
|
52
59
|
},
|
|
60
|
+
next_actions: buildDoctorNextActions(semanticAvailable, embeddings, diagnostics),
|
|
53
61
|
mcp_hint: "Pass cwd in MCP tool calls, or restart codetrap serve after changing projects.",
|
|
54
62
|
};
|
|
55
63
|
}
|
|
@@ -70,10 +78,62 @@ export function formatDoctorText(report: DoctorReport): string {
|
|
|
70
78
|
` fallback_reason: ${report.hybrid_search.fallback_reason ?? "(none)"}`,
|
|
71
79
|
"Diagnostics:",
|
|
72
80
|
` global_db_project_traps: ${report.diagnostics.mis_scoped_traps.global_db_project_traps.length}`,
|
|
81
|
+
"Next actions:",
|
|
82
|
+
...formatNextActions(report.next_actions),
|
|
73
83
|
`mcp_hint: ${report.mcp_hint}`,
|
|
74
84
|
].join("\n");
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
function buildDoctorNextActions(
|
|
88
|
+
semanticAvailable: boolean,
|
|
89
|
+
embeddings: EmbeddingStatsResult,
|
|
90
|
+
diagnostics: ReturnType<TrapStore["diagnostics"]>
|
|
91
|
+
): DoctorNextAction[] {
|
|
92
|
+
const actions: DoctorNextAction[] = [];
|
|
93
|
+
if (!semanticAvailable) {
|
|
94
|
+
actions.push({
|
|
95
|
+
command: "export JINA_API_KEY=<your-jina-api-key>",
|
|
96
|
+
reason: "Enable semantic and hybrid search; otherwise use --mode fts.",
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
const projectAction = embeddingRefreshAction("project", embeddings.project);
|
|
100
|
+
const globalAction = embeddingRefreshAction("global", embeddings.global);
|
|
101
|
+
if (projectAction) actions.push(projectAction);
|
|
102
|
+
if (globalAction) actions.push(globalAction);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stranded = diagnostics.mis_scoped_traps.global_db_project_traps.length;
|
|
106
|
+
if (stranded > 0) {
|
|
107
|
+
actions.push({
|
|
108
|
+
command: "codetrap repair-scope --dry-run --json",
|
|
109
|
+
reason: `${stranded} project-scoped trap(s) are stored in the global database.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return actions;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function embeddingRefreshAction(
|
|
116
|
+
scope: "project" | "global",
|
|
117
|
+
stats: EmbeddingStateSummary | null
|
|
118
|
+
): DoctorNextAction | null {
|
|
119
|
+
if (!stats || stats.total === 0) return null;
|
|
120
|
+
const needsRefresh = stats.missing + stats.stale;
|
|
121
|
+
if (needsRefresh === 0) return null;
|
|
122
|
+
const parts = [
|
|
123
|
+
stats.missing > 0 ? `${stats.missing} missing` : null,
|
|
124
|
+
stats.stale > 0 ? `${stats.stale} stale` : null,
|
|
125
|
+
].filter((item): item is string => item !== null);
|
|
126
|
+
return {
|
|
127
|
+
command: `codetrap embed --scope ${scope}`,
|
|
128
|
+
reason: `${scope} embeddings need refresh (${parts.join(", ")}).`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatNextActions(actions: DoctorNextAction[]): string[] {
|
|
133
|
+
if (actions.length === 0) return [" (none)"];
|
|
134
|
+
return actions.map((action) => ` - ${action.command} # ${action.reason}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
77
137
|
function formatEmbeddingStats(
|
|
78
138
|
label: string,
|
|
79
139
|
stats: EmbeddingStatsResult["global"] | null
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type EmbedCommandResult = {
|
|
2
|
+
generated: number;
|
|
3
|
+
skipped: number;
|
|
4
|
+
batches: number;
|
|
5
|
+
scopes: { scope: string; generated: number; skipped: number; batches: number }[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function formatEmbedText(result: EmbedCommandResult): string {
|
|
9
|
+
return [
|
|
10
|
+
...result.scopes.map((scoped) =>
|
|
11
|
+
`[${scoped.scope}] embeddings generated: ${scoped.generated}, skipped: ${scoped.skipped}, batches: ${scoped.batches}`
|
|
12
|
+
),
|
|
13
|
+
`Total generated: ${result.generated}, skipped: ${result.skipped}, batches: ${result.batches}`,
|
|
14
|
+
`Next: ${embedNextAction(result)}`,
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function embedNextAction(result: EmbedCommandResult): string {
|
|
19
|
+
if (result.generated > 0) {
|
|
20
|
+
return 'codetrap search "<query>" --mode hybrid';
|
|
21
|
+
}
|
|
22
|
+
if (result.skipped > 0) {
|
|
23
|
+
return "embeddings are already fresh; run codetrap doctor to verify hybrid search.";
|
|
24
|
+
}
|
|
25
|
+
return "add traps first, then rerun codetrap embed --scope project or --scope global.";
|
|
26
|
+
}
|
package/src/lib/scope-context.ts
CHANGED
|
@@ -15,14 +15,14 @@ export type ScopeContext = {
|
|
|
15
15
|
global_db: string;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export function createScopeContext(cwd = process.cwd()): ScopeContext {
|
|
19
|
-
const resolvedCwd = resolveScopePath(cwd);
|
|
20
|
-
const projectRoot = findProjectRoot(resolvedCwd);
|
|
18
|
+
export function createScopeContext(cwd = process.cwd(), home?: string): ScopeContext {
|
|
19
|
+
const resolvedCwd = resolveScopePath(cwd, home ?? cwd);
|
|
20
|
+
const projectRoot = findProjectRoot(resolvedCwd, home);
|
|
21
21
|
return {
|
|
22
22
|
cwd: resolvedCwd,
|
|
23
23
|
project_root: projectRoot,
|
|
24
24
|
project_db: projectRoot ? scopePath.join(projectRoot, CODETRAP_DIR, TRAPS_DB_FILE) : null,
|
|
25
|
-
global_db: getGlobalDB(),
|
|
25
|
+
global_db: getGlobalDB(home),
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -36,8 +36,8 @@ export class ScopedRepositoryContext {
|
|
|
36
36
|
private globalRepository?: TrapRepository;
|
|
37
37
|
private projectRepository?: TrapRepository;
|
|
38
38
|
|
|
39
|
-
constructor(cwd = process.cwd(), private readonly embedder?: EmbeddingProvider) {
|
|
40
|
-
this.context = createScopeContext(cwd);
|
|
39
|
+
constructor(cwd = process.cwd(), private readonly embedder?: EmbeddingProvider, private readonly home?: string) {
|
|
40
|
+
this.context = createScopeContext(cwd, home);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
hasProject(): boolean {
|
|
@@ -94,7 +94,7 @@ export class ScopedRepositoryContext {
|
|
|
94
94
|
|
|
95
95
|
private globalRepo(): TrapRepository {
|
|
96
96
|
if (!this.globalRepository) {
|
|
97
|
-
this.globalRepository = new TrapRepository(openGlobal(), this.embedder);
|
|
97
|
+
this.globalRepository = new TrapRepository(openGlobal(this.home), this.embedder);
|
|
98
98
|
}
|
|
99
99
|
return this.globalRepository;
|
|
100
100
|
}
|
package/src/lib/scope.ts
CHANGED
|
@@ -5,14 +5,14 @@ import { defaultScopePathResolver, resolveScopePath, ScopePathResolver } from ".
|
|
|
5
5
|
|
|
6
6
|
export { resolveScopePath, ScopePathResolver } from "./scope-path";
|
|
7
7
|
|
|
8
|
-
export function getGlobalDir(): string {
|
|
9
|
-
const dir = defaultScopePathResolver.join(
|
|
8
|
+
export function getGlobalDir(homeDir = homedir()): string {
|
|
9
|
+
const dir = defaultScopePathResolver.join(homeDir, CODETRAP_DIR);
|
|
10
10
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
11
11
|
return dir;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function getGlobalDB(): string {
|
|
15
|
-
return defaultScopePathResolver.join(getGlobalDir(), TRAPS_DB_FILE);
|
|
14
|
+
export function getGlobalDB(homeDir = homedir()): string {
|
|
15
|
+
return defaultScopePathResolver.join(getGlobalDir(homeDir), TRAPS_DB_FILE);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function findProjectRoot(
|
package/src/lib/search-eval.ts
CHANGED
|
@@ -56,6 +56,11 @@ export type SearchEvalMetrics = {
|
|
|
56
56
|
semantic_error_count: number;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
export type SearchEvalNextAction = {
|
|
60
|
+
command: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
59
64
|
export type SearchEvalReport = {
|
|
60
65
|
mode: "deterministic" | "live";
|
|
61
66
|
fixture: string;
|
|
@@ -71,6 +76,7 @@ export type SearchEvalReport = {
|
|
|
71
76
|
failures: EvalCaseReport[];
|
|
72
77
|
misses: EvalCaseReport[];
|
|
73
78
|
noisy_hits: EvalCaseReport[];
|
|
79
|
+
next_actions: SearchEvalNextAction[];
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
export type RecordDogfoodResult = {
|
|
@@ -111,17 +117,22 @@ export async function reportDogfood(fixturePath: string, live: boolean): Promise
|
|
|
111
117
|
const fixture = readEvalFixture(fixturePath);
|
|
112
118
|
const provider = live ? createDefaultEmbeddingProvider() : new EvalEmbedder();
|
|
113
119
|
const evaluated = await evaluateSearchFixture(fixture, provider);
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
const mode: SearchEvalReport["mode"] = live ? "live" : "deterministic";
|
|
121
|
+
const report: Omit<SearchEvalReport, "next_actions"> = {
|
|
122
|
+
mode,
|
|
116
123
|
fixture: fixturePath,
|
|
117
124
|
...evaluated,
|
|
118
125
|
};
|
|
126
|
+
return {
|
|
127
|
+
...report,
|
|
128
|
+
next_actions: buildSearchEvalNextActions(report),
|
|
129
|
+
};
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
export async function evaluateSearchFixture(
|
|
122
133
|
fixture: EvalFixture,
|
|
123
134
|
provider: EmbeddingProvider | undefined
|
|
124
|
-
): Promise<Omit<SearchEvalReport, "mode" | "fixture">> {
|
|
135
|
+
): Promise<Omit<SearchEvalReport, "mode" | "fixture" | "next_actions">> {
|
|
125
136
|
const repo = fixtureRepository(fixture, provider);
|
|
126
137
|
|
|
127
138
|
let providerError: string | null = null;
|
|
@@ -153,6 +164,9 @@ export async function evaluateSearchFixture(
|
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
const dogfoodCases = cases.filter((item) => item.phaseGate === "dogfood" || item.judgment !== undefined);
|
|
167
|
+
const failures = cases.filter((item) => !item.passed);
|
|
168
|
+
const misses = cases.filter((item) => item.judgment === "miss" || item.recallAt5 < (fixtureQuery(item, fixture)?.minRecallAt5 ?? 1));
|
|
169
|
+
const noisyHits = cases.filter((item) => item.judgment === "noisy_hit");
|
|
156
170
|
const metrics = aggregateMetrics(cases);
|
|
157
171
|
return {
|
|
158
172
|
provider: provider ? embeddingConfig(provider) : null,
|
|
@@ -173,9 +187,9 @@ export async function evaluateSearchFixture(
|
|
|
173
187
|
no_relevant_trap: dogfoodCases.filter((item) => item.judgment === "no_relevant_trap").length,
|
|
174
188
|
},
|
|
175
189
|
},
|
|
176
|
-
failures
|
|
177
|
-
misses
|
|
178
|
-
noisy_hits:
|
|
190
|
+
failures,
|
|
191
|
+
misses,
|
|
192
|
+
noisy_hits: noisyHits,
|
|
179
193
|
};
|
|
180
194
|
}
|
|
181
195
|
|
|
@@ -211,11 +225,90 @@ export function formatSearchEvalReport(report: SearchEvalReport): string {
|
|
|
211
225
|
`MRR: ${report.metrics.mrr}`,
|
|
212
226
|
`Hybrid fallback count: ${report.metrics.hybrid_fallback_count}`,
|
|
213
227
|
`Semantic error count: ${report.metrics.semantic_error_count}`,
|
|
214
|
-
`Dogfood cases: ${report.dogfood.total}
|
|
228
|
+
`Dogfood cases: ${report.dogfood.total}`,
|
|
229
|
+
`Judgments: ${formatJudgmentCounts(report.dogfood.judgment_counts)}`
|
|
215
230
|
);
|
|
231
|
+
appendCaseSection(lines, "Failures", report.failures);
|
|
232
|
+
appendCaseSection(lines, "Misses to inspect", report.misses);
|
|
233
|
+
appendCaseSection(lines, "Noisy hits to inspect", report.noisy_hits);
|
|
234
|
+
lines.push("Next actions:", ...formatNextActions(report.next_actions));
|
|
216
235
|
return lines.join("\n");
|
|
217
236
|
}
|
|
218
237
|
|
|
238
|
+
function buildSearchEvalNextActions(
|
|
239
|
+
report: Omit<SearchEvalReport, "next_actions">
|
|
240
|
+
): SearchEvalNextAction[] {
|
|
241
|
+
const actions: SearchEvalNextAction[] = [];
|
|
242
|
+
if (report.mode === "live" && !report.semantic_available) {
|
|
243
|
+
actions.push({
|
|
244
|
+
command: "export JINA_API_KEY=<your-jina-api-key>",
|
|
245
|
+
reason: "Enable live semantic checks, then rerun bun run eval:dogfood -- report --live.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (report.failures.length > 0) {
|
|
249
|
+
actions.push({
|
|
250
|
+
command: "bun run eval:dogfood -- report --json",
|
|
251
|
+
reason: "Inspect expected ids, top results, and errors before changing search behavior or fixture expectations.",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (report.misses.length > 0) {
|
|
255
|
+
actions.push({
|
|
256
|
+
command: 'codetrap search "<miss query>" --mode hybrid --ranking-signals --json',
|
|
257
|
+
reason: "Replay a miss with ranking signals before deciding whether to tune search or promote a fixture case.",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (report.noisy_hits.length > 0) {
|
|
261
|
+
actions.push({
|
|
262
|
+
command: 'codetrap search "<noisy query>" --mode hybrid --ranking-signals --json',
|
|
263
|
+
reason: "Inspect why noisy results ranked before deciding whether the case belongs in dogfood eval.",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (actions.length === 0) {
|
|
267
|
+
actions.push({
|
|
268
|
+
command: 'codetrap search "<task keywords>" --mode hybrid --json',
|
|
269
|
+
reason: "Keep logging real pre-edit searches in dogfood-log.md before automating promotion.",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return actions;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formatJudgmentCounts(counts: Record<DogfoodJudgment, number>): string {
|
|
276
|
+
return DOGFOOD_JUDGMENTS.map((judgment) => `${judgment}=${counts[judgment]}`).join(", ");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function appendCaseSection(lines: string[], title: string, cases: EvalCaseReport[]): void {
|
|
280
|
+
if (cases.length === 0) return;
|
|
281
|
+
lines.push(`${title}:`);
|
|
282
|
+
for (const item of cases.slice(0, 5)) {
|
|
283
|
+
lines.push(` - [${item.mode}] ${item.query}`);
|
|
284
|
+
if (item.goldTrapIds.length > 0) {
|
|
285
|
+
lines.push(` expected: ${formatExpected(item)}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push(` top: ${formatTopResults(item)}`);
|
|
288
|
+
if (item.error) lines.push(` error: ${item.error}`);
|
|
289
|
+
}
|
|
290
|
+
if (cases.length > 5) lines.push(` ... ${cases.length - 5} more`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatExpected(item: EvalCaseReport): string {
|
|
294
|
+
return item.goldTrapIds
|
|
295
|
+
.map((id, index) => `#${id} ${item.expectedTitles[index] ?? ""}`.trim())
|
|
296
|
+
.join(", ");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatTopResults(item: EvalCaseReport): string {
|
|
300
|
+
if (item.topResults.length === 0) return "(none)";
|
|
301
|
+
return item.topResults
|
|
302
|
+
.slice(0, 3)
|
|
303
|
+
.map((result) => `#${result.id} ${result.title}`)
|
|
304
|
+
.join("; ");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function formatNextActions(actions: SearchEvalNextAction[]): string[] {
|
|
308
|
+
if (actions.length === 0) return [" (none)"];
|
|
309
|
+
return actions.map((action) => ` - ${action.command} # ${action.reason}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
219
312
|
function fixtureRepository(fixture: EvalFixture, provider: EmbeddingProvider | undefined): TrapRepository {
|
|
220
313
|
const repo = new TrapRepository(openDatabase(":memory:"), provider);
|
|
221
314
|
for (const trap of fixture.traps) repo.add(trap);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { CandidateTrap } from "../domain/session";
|
|
2
2
|
import { buildTrapInput } from "../domain/trap";
|
|
3
|
+
import type { Scope } from "./constants";
|
|
3
4
|
import type { TrapOperations } from "./trap-operations";
|
|
4
5
|
import { findCandidateConflicts, type CandidateConflict } from "./session-conflicts";
|
|
5
6
|
import type {
|
|
6
7
|
AcceptCandidateResult,
|
|
7
8
|
AddSessionNoteArgs,
|
|
8
9
|
CloseSessionResult,
|
|
10
|
+
DeleteSessionResult,
|
|
11
|
+
PruneSessionsResult,
|
|
12
|
+
RemoveSessionCandidatesResult,
|
|
9
13
|
SessionStore,
|
|
10
14
|
StartSessionArgs,
|
|
11
15
|
} from "./session-store";
|
|
@@ -30,6 +34,12 @@ export type SessionRejectRequest = {
|
|
|
30
34
|
reason?: string | null;
|
|
31
35
|
};
|
|
32
36
|
|
|
37
|
+
export type SessionPruneRequest = {
|
|
38
|
+
olderThanDays: number;
|
|
39
|
+
apply: boolean;
|
|
40
|
+
now?: Date;
|
|
41
|
+
};
|
|
42
|
+
|
|
33
43
|
export type SessionConflictResult = {
|
|
34
44
|
success: false;
|
|
35
45
|
session_id: string;
|
|
@@ -155,6 +165,29 @@ export class SessionOperations {
|
|
|
155
165
|
reason: request.reason,
|
|
156
166
|
});
|
|
157
167
|
}
|
|
168
|
+
|
|
169
|
+
deleteSession(sessionId: string): DeleteSessionResult {
|
|
170
|
+
return this.sessions.deleteSession(sessionId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pruneSessions(request: SessionPruneRequest): PruneSessionsResult {
|
|
174
|
+
const now = request.now ?? new Date();
|
|
175
|
+
const cutoff = new Date(now.getTime() - request.olderThanDays * 24 * 60 * 60 * 1000);
|
|
176
|
+
return this.sessions.pruneSessions({ cutoff, dryRun: !request.apply });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
cleanupDeletedTrapCandidates(sessionId?: string): RemoveSessionCandidatesResult {
|
|
180
|
+
const document = this.sessions.candidateDocument(sessionId);
|
|
181
|
+
const missingCandidateIds = document.candidates
|
|
182
|
+
.filter((candidate) => candidate.status === "accepted")
|
|
183
|
+
.filter((candidate) => {
|
|
184
|
+
const trapId = candidate.accepted_trap_id;
|
|
185
|
+
if (trapId === undefined) return true;
|
|
186
|
+
return !this.traps.getTrapDetails(trapId, acceptedScope(candidate));
|
|
187
|
+
})
|
|
188
|
+
.map((candidate) => candidate.id);
|
|
189
|
+
return this.sessions.removeCandidates(sessionId, missingCandidateIds);
|
|
190
|
+
}
|
|
158
191
|
}
|
|
159
192
|
|
|
160
193
|
function candidateWithTrapEdits(candidate: CandidateTrap, edit: Record<string, unknown> | undefined): CandidateTrap {
|
|
@@ -209,6 +242,10 @@ function candidateRelatedFiles(candidate: CandidateTrap): string[] {
|
|
|
209
242
|
return uniqueStrings(candidate.evidence.flatMap((evidence) => evidence.related_files ?? []));
|
|
210
243
|
}
|
|
211
244
|
|
|
245
|
+
function acceptedScope(candidate: CandidateTrap): Scope {
|
|
246
|
+
return candidate.accepted_scope ?? (candidate.trap.scope === "global" ? "global" : "project");
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
function uniqueStrings(values: string[]): string[] {
|
|
213
250
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
214
251
|
}
|