codetrap 0.1.5 → 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 +52 -1
- package/docs/installation.md +21 -3
- package/package.json +3 -1
- package/scripts/dogfood-eval.ts +53 -0
- package/src/commands/workflow.ts +308 -7
- package/src/db/connection.ts +7 -7
- package/src/domain/session.ts +119 -0
- package/src/index.ts +8 -0
- package/src/lib/command-requests.ts +178 -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 +505 -0
- package/src/lib/session-capture.ts +96 -0
- package/src/lib/session-codec.ts +261 -0
- package/src/lib/session-conflicts.ts +104 -0
- package/src/lib/session-operations.ts +251 -0
- package/src/lib/session-store.ts +611 -0
- package/src/lib/store.ts +7 -3
- package/src/lib/trap-quality.ts +111 -0
- package/src/lib/trap-scope-match.ts +1 -1
- package/src/web/client-script.ts +1168 -0
- package/src/web/client-text.ts +335 -0
- package/src/web/project-registry.ts +106 -0
- package/src/web/server.ts +500 -0
- package/src/web/static.ts +528 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { TrapEvidenceInput, TrapInput } from "./trap";
|
|
2
|
+
import type { Scope } from "../lib/constants";
|
|
3
|
+
|
|
4
|
+
export const SESSION_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export const SESSION_STATUSES = ["active", "closed"] as const;
|
|
7
|
+
export type SessionStatus = (typeof SESSION_STATUSES)[number];
|
|
8
|
+
|
|
9
|
+
export const SESSION_NOTE_KINDS = [
|
|
10
|
+
"decision",
|
|
11
|
+
"deviation",
|
|
12
|
+
"tradeoff",
|
|
13
|
+
"open_question",
|
|
14
|
+
"failure",
|
|
15
|
+
"test_failure",
|
|
16
|
+
"correction",
|
|
17
|
+
"review",
|
|
18
|
+
"observation",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type SessionNoteKind = (typeof SESSION_NOTE_KINDS)[number];
|
|
22
|
+
|
|
23
|
+
export const CANDIDATE_STATUSES = ["proposed", "accepted", "rejected"] as const;
|
|
24
|
+
export type CandidateStatus = (typeof CANDIDATE_STATUSES)[number];
|
|
25
|
+
|
|
26
|
+
export interface SessionMetadata {
|
|
27
|
+
version: typeof SESSION_VERSION;
|
|
28
|
+
id: string;
|
|
29
|
+
goal: string;
|
|
30
|
+
status: SessionStatus;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
closed_at: string | null;
|
|
34
|
+
scope: Scope;
|
|
35
|
+
project_path: string;
|
|
36
|
+
module: string | null;
|
|
37
|
+
owner: string | null;
|
|
38
|
+
spec_ref: string | null;
|
|
39
|
+
notes_path: string;
|
|
40
|
+
recap_path: string;
|
|
41
|
+
candidate_traps_path: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SessionNote {
|
|
45
|
+
created_at: string;
|
|
46
|
+
kind: SessionNoteKind;
|
|
47
|
+
text: string;
|
|
48
|
+
related_files: string[];
|
|
49
|
+
source_ref: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type SessionNoteCounts = Partial<Record<SessionNoteKind, number>>;
|
|
53
|
+
|
|
54
|
+
export interface SessionIndexEntry {
|
|
55
|
+
id: string;
|
|
56
|
+
goal: string;
|
|
57
|
+
status: SessionStatus;
|
|
58
|
+
created_at: string;
|
|
59
|
+
closed_at: string | null;
|
|
60
|
+
module: string | null;
|
|
61
|
+
owner: string | null;
|
|
62
|
+
note_counts: SessionNoteCounts;
|
|
63
|
+
candidate_count: number;
|
|
64
|
+
accepted_count: number;
|
|
65
|
+
summary: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SessionIndexDocument {
|
|
69
|
+
version: typeof SESSION_VERSION;
|
|
70
|
+
sessions: SessionIndexEntry[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ActiveSessionDocument {
|
|
74
|
+
active_session_id: string | null;
|
|
75
|
+
updated_at: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CandidateQuality {
|
|
79
|
+
has_clear_trigger: boolean;
|
|
80
|
+
has_clear_mistake: boolean;
|
|
81
|
+
has_actionable_fix: boolean;
|
|
82
|
+
not_too_broad: boolean;
|
|
83
|
+
future_reuse_likely: boolean;
|
|
84
|
+
proper_scope: boolean;
|
|
85
|
+
evidence_count: number;
|
|
86
|
+
conflict_checked: boolean;
|
|
87
|
+
conflict_status: "none" | "possible" | "confirmed";
|
|
88
|
+
staleness_risk: "low" | "medium" | "high";
|
|
89
|
+
suggested_action: "accept" | "edit" | "supersede" | "archive_old" | "reject";
|
|
90
|
+
warnings: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CandidateTrap {
|
|
94
|
+
id: string;
|
|
95
|
+
status: CandidateStatus;
|
|
96
|
+
quality_score: number;
|
|
97
|
+
quality: CandidateQuality;
|
|
98
|
+
trap: TrapInput;
|
|
99
|
+
evidence: TrapEvidenceInput[];
|
|
100
|
+
accepted_trap_id?: number;
|
|
101
|
+
accepted_scope?: Scope;
|
|
102
|
+
accepted_at?: string;
|
|
103
|
+
rejected_at?: string;
|
|
104
|
+
rejection_reason?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface CandidateTrapDocument {
|
|
108
|
+
version: typeof SESSION_VERSION;
|
|
109
|
+
session_id: string;
|
|
110
|
+
candidates: CandidateTrap[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseSessionNoteKind(value: string | undefined): SessionNoteKind {
|
|
114
|
+
const normalized = value ?? "observation";
|
|
115
|
+
if ((SESSION_NOTE_KINDS as readonly string[]).includes(normalized)) {
|
|
116
|
+
return normalized as SessionNoteKind;
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Invalid session note kind: ${normalized}. Expected one of: ${SESSION_NOTE_KINDS.join(", ")}`);
|
|
119
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ if (args.length === 0) {
|
|
|
14
14
|
showHelp();
|
|
15
15
|
} else if (args[0] === "serve") {
|
|
16
16
|
import("./mcp/server").then((m) => m.start());
|
|
17
|
+
} else if (args[0] === "web") {
|
|
18
|
+
const { startWebServerFromArgs } = await import("./web/server");
|
|
19
|
+
await startWebServerFromArgs(args.slice(1));
|
|
17
20
|
} else if (args[0] === "init") {
|
|
18
21
|
const cwd = process.cwd();
|
|
19
22
|
if (findProjectRoot(cwd)) {
|
|
@@ -43,6 +46,8 @@ function showHelp(): void {
|
|
|
43
46
|
console.log(" archive_trap Archive a trap");
|
|
44
47
|
console.log(" supersede_trap Mark one trap as superseded by another");
|
|
45
48
|
console.log(" embed Generate embeddings for semantic search");
|
|
49
|
+
console.log(" session Record implementation notes and capture candidate traps");
|
|
50
|
+
console.log(" web Start local review and trap library console");
|
|
46
51
|
console.log(" export Export traps as JSON");
|
|
47
52
|
console.log(" import <file.json> Import traps from JSON");
|
|
48
53
|
console.log(" stats Show statistics");
|
|
@@ -63,6 +68,9 @@ function showHelp(): void {
|
|
|
63
68
|
console.log(" --no-rerank Disable query-aware search reranking");
|
|
64
69
|
console.log(" --ranking-signals Include search ranking diagnostics in JSON cards");
|
|
65
70
|
console.log(" --batch-size <n> Embedding generation batch size");
|
|
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)");
|
|
66
74
|
console.log(" --json JSON output for search/show/list/stats/doctor; JSON input for add/edit");
|
|
67
75
|
console.log(" --output-json JSON output for add/edit when --json is used as input");
|
|
68
76
|
console.log(" --from-project-path <path> Source project path for scope repair/migration");
|
|
@@ -16,6 +16,63 @@ export type StatsRequest = {
|
|
|
16
16
|
scope?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type SessionStartRequest = {
|
|
20
|
+
goal: string;
|
|
21
|
+
specRef?: string;
|
|
22
|
+
module?: string;
|
|
23
|
+
owner?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SessionNoteRequest = {
|
|
27
|
+
kind?: string;
|
|
28
|
+
text: string;
|
|
29
|
+
relatedFiles?: string[];
|
|
30
|
+
sourceRef?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type SessionListRequest = {
|
|
34
|
+
status?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SessionCloseRequest = {
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
proposeTraps: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SessionIdRequest = {
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type SessionShowRequest = {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SessionCandidateRequest = {
|
|
52
|
+
candidateId: string;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type SessionAcceptRequest = SessionCandidateRequest & {
|
|
57
|
+
edit?: Record<string, unknown>;
|
|
58
|
+
supersedesId?: number;
|
|
59
|
+
acceptAnyway: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type SessionRejectRequest = SessionCandidateRequest & {
|
|
63
|
+
reason?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type SessionPruneRequest = {
|
|
67
|
+
olderThanDays: number;
|
|
68
|
+
apply: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type SessionNoteStdin = {
|
|
72
|
+
isTTY: boolean;
|
|
73
|
+
read: () => string;
|
|
74
|
+
};
|
|
75
|
+
|
|
19
76
|
export function searchRequestFromArgs(query: string, args: RawArgs, defaults: SearchDefaults): SearchTrapsArgs {
|
|
20
77
|
return {
|
|
21
78
|
query,
|
|
@@ -70,6 +127,98 @@ export function evidenceRequestFromArgs(args: RawArgs): RawArgs {
|
|
|
70
127
|
};
|
|
71
128
|
}
|
|
72
129
|
|
|
130
|
+
export function sessionStartRequestFromArgs(positionals: string[], args: RawArgs): SessionStartRequest {
|
|
131
|
+
const goal = positionals.join(" ").trim();
|
|
132
|
+
if (!goal) throw new Error("Session goal is required.");
|
|
133
|
+
return {
|
|
134
|
+
goal,
|
|
135
|
+
specRef: stringOption(args, "spec"),
|
|
136
|
+
module: stringOption(args, "module"),
|
|
137
|
+
owner: stringOption(args, "owner"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sessionNoteRequestFromArgs(
|
|
142
|
+
positionals: string[],
|
|
143
|
+
args: RawArgs,
|
|
144
|
+
stdin: SessionNoteStdin
|
|
145
|
+
): SessionNoteRequest {
|
|
146
|
+
const hasText = args.text !== undefined;
|
|
147
|
+
const hasStdin = args.stdin !== undefined;
|
|
148
|
+
if (hasText && hasStdin) throw new Error("Choose either --text or --stdin, not both.");
|
|
149
|
+
if (hasStdin && stdin.isTTY) throw new Error("--stdin requires piped input.");
|
|
150
|
+
|
|
151
|
+
const text = hasStdin
|
|
152
|
+
? stdin.read().trim()
|
|
153
|
+
: stringOption(args, "text") ?? positionals.join(" ").trim();
|
|
154
|
+
if (!text) throw new Error("Session note text is required.");
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
kind: stringOption(args, "kind"),
|
|
158
|
+
text,
|
|
159
|
+
relatedFiles: csvOrArrayOption(args, "related_files", "related-files"),
|
|
160
|
+
sourceRef: stringOption(args, "source_ref", "source-ref"),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function sessionListRequestFromArgs(args: RawArgs): SessionListRequest {
|
|
165
|
+
return {
|
|
166
|
+
status: stringOption(args, "status") ?? "all",
|
|
167
|
+
limit: optionalIntOption(args, "limit"),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function sessionCloseRequestFromArgs(positionals: string[], args: RawArgs): SessionCloseRequest {
|
|
172
|
+
return {
|
|
173
|
+
sessionId: positionals[0],
|
|
174
|
+
proposeTraps: flagPresent(args, "propose-traps"),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function sessionIdRequestFromArgs(positionals: string[]): SessionIdRequest {
|
|
179
|
+
return {
|
|
180
|
+
sessionId: positionals[0],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function sessionShowRequestFromArgs(positionals: string[]): SessionShowRequest {
|
|
185
|
+
return {
|
|
186
|
+
sessionId: requiredPositional(positionals, 0, "session-id"),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function sessionCandidateRequestFromArgs(positionals: string[], args: RawArgs): SessionCandidateRequest {
|
|
191
|
+
return {
|
|
192
|
+
candidateId: requiredPositional(positionals, 0, "candidate-id"),
|
|
193
|
+
sessionId: stringOption(args, "session"),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function sessionAcceptRequestFromArgs(positionals: string[], args: RawArgs): SessionAcceptRequest {
|
|
198
|
+
return {
|
|
199
|
+
...sessionCandidateRequestFromArgs(positionals, args),
|
|
200
|
+
edit: jsonObjectOption(args, "edit-json"),
|
|
201
|
+
supersedesId: optionalIntOption(args, "supersedes"),
|
|
202
|
+
acceptAnyway: flagPresent(args, "accept-anyway"),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function sessionRejectRequestFromArgs(positionals: string[], args: RawArgs): SessionRejectRequest {
|
|
207
|
+
return {
|
|
208
|
+
...sessionCandidateRequestFromArgs(positionals, args),
|
|
209
|
+
reason: stringOption(args, "reason"),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
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
|
+
|
|
73
222
|
function stringOption(args: RawArgs, ...keys: string[]): string | undefined {
|
|
74
223
|
for (const key of keys) {
|
|
75
224
|
const value = args[key];
|
|
@@ -131,3 +280,32 @@ function csvOrArrayOption(args: RawArgs, ...keys: string[]): string[] | undefine
|
|
|
131
280
|
}
|
|
132
281
|
return undefined;
|
|
133
282
|
}
|
|
283
|
+
|
|
284
|
+
function jsonObjectOption(args: RawArgs, key: string): Record<string, unknown> | undefined {
|
|
285
|
+
const value = stringOption(args, key);
|
|
286
|
+
if (value === undefined) return undefined;
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(value) as unknown;
|
|
289
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
290
|
+
throw new Error(`${key} must be a JSON object.`);
|
|
291
|
+
}
|
|
292
|
+
return parsed as Record<string, unknown>;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
295
|
+
throw new Error(`Invalid --${key}: ${message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
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
|
+
|
|
307
|
+
function requiredPositional(positionals: string[], index: number, name: string): string {
|
|
308
|
+
const value = positionals[index];
|
|
309
|
+
if (!value) throw new Error(`${name} is required.`);
|
|
310
|
+
return value;
|
|
311
|
+
}
|
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(
|