codetrap 0.1.6 → 0.1.8
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 +159 -51
- package/docs/installation.md +113 -29
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +1 -2
- 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.md +31 -5
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +186 -68
- package/src/db/connection.ts +6 -6
- 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 +32 -7
- package/src/lib/command-requests.ts +134 -1
- package/src/lib/config.ts +57 -7
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +96 -6
- package/src/lib/embed-output.ts +26 -0
- 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 +17 -11
- package/src/lib/scope-migration.ts +2 -1
- package/src/lib/scope.ts +4 -6
- package/src/lib/search-eval.ts +136 -23
- 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 +111 -51
- package/src/lib/session-review.ts +327 -0
- package/src/lib/session-store.ts +177 -55
- package/src/lib/store.ts +79 -11
- 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 +1543 -0
- package/src/web/client-shell.ts +414 -0
- package/src/web/client-text.ts +447 -0
- package/src/web/project-registry.ts +3 -5
- package/src/web/server.ts +184 -111
- package/src/web/static.ts +581 -484
- 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/session-store.ts
CHANGED
|
@@ -3,12 +3,22 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
|
+
rmSync,
|
|
6
7
|
writeFileSync,
|
|
7
8
|
} from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import type { CandidateTrap, CandidateTrapDocument, SessionIndexDocument, SessionMetadata, SessionNote } from "../domain/session";
|
|
10
11
|
import { parseSessionNoteKind, SESSION_VERSION } from "../domain/session";
|
|
11
12
|
import { CODETRAP_DIR } from "./constants";
|
|
13
|
+
import {
|
|
14
|
+
acceptCandidateInDocument,
|
|
15
|
+
addCandidateToDocument,
|
|
16
|
+
recordCandidateConflictCheckInDocument,
|
|
17
|
+
rejectCandidateInDocument,
|
|
18
|
+
removeCandidatesFromDocument,
|
|
19
|
+
saveCandidateTrapInDocument,
|
|
20
|
+
type CandidateDocumentUpdateResult,
|
|
21
|
+
} from "./session-candidate-document";
|
|
12
22
|
import {
|
|
13
23
|
ACTIVE_SESSION_FILE,
|
|
14
24
|
CANDIDATES_FILE,
|
|
@@ -22,13 +32,14 @@ import {
|
|
|
22
32
|
recapSummary,
|
|
23
33
|
RECAP_FILE,
|
|
24
34
|
sessionIndexEntry,
|
|
35
|
+
sessionRelativeDir,
|
|
25
36
|
SESSION_FILE,
|
|
26
37
|
SESSION_INDEX_FILE,
|
|
27
38
|
sessionRelativeFile,
|
|
28
39
|
SESSIONS_DIR,
|
|
29
40
|
} from "./session-codec";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
41
|
+
import { uniqueStrings } from "./string-list";
|
|
42
|
+
import { mergeCandidateTraps, proposeCandidateTraps, type CandidateDraft } from "./session-capture";
|
|
32
43
|
|
|
33
44
|
export interface StartSessionArgs {
|
|
34
45
|
goal: string;
|
|
@@ -48,7 +59,6 @@ export interface CloseSessionResult {
|
|
|
48
59
|
session: SessionMetadata;
|
|
49
60
|
recap_path: string;
|
|
50
61
|
candidate_count: number;
|
|
51
|
-
traps_written: number;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export interface AcceptCandidateResult {
|
|
@@ -60,6 +70,38 @@ export interface AcceptCandidateResult {
|
|
|
60
70
|
superseded_id: number | null;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
export interface DeleteSessionResult {
|
|
74
|
+
session_id: string;
|
|
75
|
+
deleted: boolean;
|
|
76
|
+
active_cleared: boolean;
|
|
77
|
+
session_dir: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface PruneSessionsResult {
|
|
81
|
+
cutoff: string;
|
|
82
|
+
dry_run: boolean;
|
|
83
|
+
deleted_count: number;
|
|
84
|
+
sessions: {
|
|
85
|
+
id: string;
|
|
86
|
+
goal: string;
|
|
87
|
+
status: string;
|
|
88
|
+
created_at: string;
|
|
89
|
+
closed_at: string | null;
|
|
90
|
+
}[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface RemoveSessionCandidatesResult {
|
|
94
|
+
session: SessionMetadata;
|
|
95
|
+
removed_count: number;
|
|
96
|
+
removed_candidate_ids: string[];
|
|
97
|
+
candidates: CandidateTrap[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AddSessionCandidateArgs {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
draft: CandidateDraft;
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
export class SessionStore {
|
|
64
106
|
constructor(private readonly projectRoot: string) {}
|
|
65
107
|
|
|
@@ -190,7 +232,10 @@ export class SessionStore {
|
|
|
190
232
|
|
|
191
233
|
const closedAt = now.toISOString();
|
|
192
234
|
const notes = this.readNotes(session.id);
|
|
193
|
-
const
|
|
235
|
+
const existingCandidates = this.readCandidateDocument(session.id).candidates;
|
|
236
|
+
const candidates = proposeTraps
|
|
237
|
+
? mergeCandidateTraps(existingCandidates, proposeCandidateTraps(session, notes))
|
|
238
|
+
: existingCandidates;
|
|
194
239
|
if (proposeTraps) this.writeCandidateDocument(session.id, candidates);
|
|
195
240
|
|
|
196
241
|
const updated = {
|
|
@@ -208,7 +253,6 @@ export class SessionStore {
|
|
|
208
253
|
session: updated,
|
|
209
254
|
recap_path: sessionRelativeFile(updated.id, RECAP_FILE),
|
|
210
255
|
candidate_count: candidates.length,
|
|
211
|
-
traps_written: 0,
|
|
212
256
|
};
|
|
213
257
|
}
|
|
214
258
|
|
|
@@ -224,6 +268,22 @@ export class SessionStore {
|
|
|
224
268
|
return { session, candidate };
|
|
225
269
|
}
|
|
226
270
|
|
|
271
|
+
addCandidate(args: AddSessionCandidateArgs): { session: SessionMetadata; candidate: CandidateTrap; candidates_path: string; duplicate: boolean } {
|
|
272
|
+
const session = this.requireSession(args.sessionId);
|
|
273
|
+
const document = this.readCandidateDocument(session.id);
|
|
274
|
+
const added = addCandidateToDocument(document.candidates, args.draft);
|
|
275
|
+
if (!added.duplicate) {
|
|
276
|
+
this.writeCandidateDocument(session.id, added.candidates);
|
|
277
|
+
this.refreshSessionSummaries(session.id);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
session: this.requireSession(session.id),
|
|
281
|
+
candidate: added.candidate,
|
|
282
|
+
candidates_path: sessionRelativeFile(session.id, CANDIDATES_FILE),
|
|
283
|
+
duplicate: added.duplicate,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
227
287
|
recordCandidateConflictCheck(
|
|
228
288
|
candidateId: string,
|
|
229
289
|
args: {
|
|
@@ -236,17 +296,16 @@ export class SessionStore {
|
|
|
236
296
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
237
297
|
const session = this.requireSession(sessionId);
|
|
238
298
|
const document = this.readCandidateDocument(sessionId);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return { session, candidate };
|
|
299
|
+
return this.saveCandidateDocumentMutation(session, recordCandidateConflictCheckInDocument(
|
|
300
|
+
document.candidates,
|
|
301
|
+
candidateId,
|
|
302
|
+
{
|
|
303
|
+
sessionId,
|
|
304
|
+
trap: args.trap,
|
|
305
|
+
conflictStatus: args.conflictStatus,
|
|
306
|
+
suggestedAction: args.suggestedAction,
|
|
307
|
+
}
|
|
308
|
+
));
|
|
250
309
|
}
|
|
251
310
|
|
|
252
311
|
saveCandidateTrap(
|
|
@@ -259,17 +318,11 @@ export class SessionStore {
|
|
|
259
318
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
260
319
|
const session = this.requireSession(sessionId);
|
|
261
320
|
const document = this.readCandidateDocument(sessionId);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
candidate.trap = args.trap;
|
|
268
|
-
candidate.quality_score = scored.score;
|
|
269
|
-
candidate.quality = scored.quality;
|
|
270
|
-
this.writeCandidateDocument(session.id, document.candidates);
|
|
271
|
-
this.refreshSessionSummaries(session.id);
|
|
272
|
-
return { session, candidate };
|
|
321
|
+
return this.saveCandidateDocumentMutation(session, saveCandidateTrapInDocument(
|
|
322
|
+
document.candidates,
|
|
323
|
+
candidateId,
|
|
324
|
+
{ sessionId, trap: args.trap }
|
|
325
|
+
));
|
|
273
326
|
}
|
|
274
327
|
|
|
275
328
|
acceptCandidate(
|
|
@@ -290,24 +343,24 @@ export class SessionStore {
|
|
|
290
343
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
291
344
|
const session = this.requireSession(sessionId);
|
|
292
345
|
const document = this.readCandidateDocument(sessionId);
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
346
|
+
const accepted = this.saveCandidateDocumentMutation(session, acceptCandidateInDocument(
|
|
347
|
+
document.candidates,
|
|
348
|
+
candidateId,
|
|
349
|
+
{
|
|
350
|
+
sessionId,
|
|
351
|
+
trap: args.trap,
|
|
352
|
+
trapId: args.trapId,
|
|
353
|
+
scope: args.scope,
|
|
354
|
+
conflictChecked: args.conflictChecked,
|
|
355
|
+
conflictStatus: args.conflictStatus,
|
|
356
|
+
suggestedAction: args.suggestedAction,
|
|
357
|
+
},
|
|
358
|
+
now
|
|
359
|
+
));
|
|
307
360
|
|
|
308
361
|
return {
|
|
309
362
|
session,
|
|
310
|
-
candidate,
|
|
363
|
+
candidate: accepted.candidate,
|
|
311
364
|
trap_id: args.trapId,
|
|
312
365
|
scope: args.scope,
|
|
313
366
|
evidence_id: args.evidenceId,
|
|
@@ -323,16 +376,73 @@ export class SessionStore {
|
|
|
323
376
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
324
377
|
const session = this.requireSession(sessionId);
|
|
325
378
|
const document = this.readCandidateDocument(sessionId);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
379
|
+
return this.saveCandidateDocumentMutation(session, rejectCandidateInDocument(
|
|
380
|
+
document.candidates,
|
|
381
|
+
candidateId,
|
|
382
|
+
{ sessionId, reason: args.reason },
|
|
383
|
+
now
|
|
384
|
+
));
|
|
385
|
+
}
|
|
329
386
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
this.
|
|
334
|
-
|
|
335
|
-
|
|
387
|
+
removeCandidates(sessionId: string | undefined, candidateIds: string[]): RemoveSessionCandidatesResult {
|
|
388
|
+
const resolvedSessionId = this.resolveSessionId(sessionId);
|
|
389
|
+
const session = this.requireSession(resolvedSessionId);
|
|
390
|
+
const document = this.readCandidateDocument(session.id);
|
|
391
|
+
const removed = removeCandidatesFromDocument(document.candidates, candidateIds);
|
|
392
|
+
if (removed.removed.length > 0) {
|
|
393
|
+
this.writeCandidateDocument(session.id, removed.candidates);
|
|
394
|
+
this.refreshSessionSummaries(session.id);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
session,
|
|
399
|
+
removed_count: removed.removed.length,
|
|
400
|
+
removed_candidate_ids: removed.removed.map((candidate) => candidate.id),
|
|
401
|
+
candidates: removed.candidates,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
deleteSession(id: string): DeleteSessionResult {
|
|
406
|
+
this.requireSession(id);
|
|
407
|
+
const active_cleared = this.readActive()?.active_session_id === id;
|
|
408
|
+
rmSync(this.sessionDir(id), { recursive: true, force: true });
|
|
409
|
+
this.removeIndexEntry(id);
|
|
410
|
+
if (active_cleared) this.clearActive();
|
|
411
|
+
return {
|
|
412
|
+
session_id: id,
|
|
413
|
+
deleted: true,
|
|
414
|
+
active_cleared,
|
|
415
|
+
session_dir: sessionRelativeDir(id),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
pruneSessions(args: { cutoff: Date; dryRun?: boolean }): PruneSessionsResult {
|
|
420
|
+
const cutoffTime = args.cutoff.getTime();
|
|
421
|
+
const candidates = this.readIndex().sessions
|
|
422
|
+
.filter((entry) => entry.status === "closed")
|
|
423
|
+
.filter((entry) => {
|
|
424
|
+
const closedAt = entry.closed_at ? Date.parse(entry.closed_at) : Date.parse(entry.created_at);
|
|
425
|
+
return Number.isFinite(closedAt) && closedAt < cutoffTime;
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (!args.dryRun) {
|
|
429
|
+
for (const session of candidates) {
|
|
430
|
+
this.deleteSession(session.id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
cutoff: args.cutoff.toISOString(),
|
|
436
|
+
dry_run: args.dryRun ?? false,
|
|
437
|
+
deleted_count: args.dryRun ? 0 : candidates.length,
|
|
438
|
+
sessions: candidates.map((session) => ({
|
|
439
|
+
id: session.id,
|
|
440
|
+
goal: session.goal,
|
|
441
|
+
status: session.status,
|
|
442
|
+
created_at: session.created_at,
|
|
443
|
+
closed_at: session.closed_at,
|
|
444
|
+
})),
|
|
445
|
+
};
|
|
336
446
|
}
|
|
337
447
|
|
|
338
448
|
readNotes(id: string): SessionNote[] {
|
|
@@ -387,6 +497,15 @@ export class SessionStore {
|
|
|
387
497
|
return candidate;
|
|
388
498
|
}
|
|
389
499
|
|
|
500
|
+
private saveCandidateDocumentMutation(
|
|
501
|
+
session: SessionMetadata,
|
|
502
|
+
mutation: CandidateDocumentUpdateResult
|
|
503
|
+
): { session: SessionMetadata; candidate: CandidateTrap } {
|
|
504
|
+
this.writeCandidateDocument(session.id, mutation.candidates);
|
|
505
|
+
this.refreshSessionSummaries(session.id);
|
|
506
|
+
return { session, candidate: mutation.candidate };
|
|
507
|
+
}
|
|
508
|
+
|
|
390
509
|
private readCandidateDocument(id: string): CandidateTrapDocument {
|
|
391
510
|
const path = this.candidatesPath(id);
|
|
392
511
|
if (!existsSync(path)) {
|
|
@@ -436,6 +555,13 @@ export class SessionStore {
|
|
|
436
555
|
writeFileSync(this.indexPath(), `${JSON.stringify({ version: SESSION_VERSION, sessions } satisfies SessionIndexDocument, null, 2)}\n`);
|
|
437
556
|
}
|
|
438
557
|
|
|
558
|
+
private removeIndexEntry(id: string): void {
|
|
559
|
+
this.ensureSessionsDir();
|
|
560
|
+
const index = this.readIndex();
|
|
561
|
+
const sessions = index.sessions.filter((entry) => entry.id !== id);
|
|
562
|
+
writeFileSync(this.indexPath(), `${JSON.stringify({ version: SESSION_VERSION, sessions } satisfies SessionIndexDocument, null, 2)}\n`);
|
|
563
|
+
}
|
|
564
|
+
|
|
439
565
|
private readActive(): { active_session_id: string | null; updated_at: string } | null {
|
|
440
566
|
const path = this.activePath();
|
|
441
567
|
if (!existsSync(path)) return null;
|
|
@@ -497,7 +623,3 @@ export class SessionStore {
|
|
|
497
623
|
return join(this.sessionDir(id), CANDIDATES_FILE);
|
|
498
624
|
}
|
|
499
625
|
}
|
|
500
|
-
|
|
501
|
-
function uniqueStrings(values: string[]): string[] {
|
|
502
|
-
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
503
|
-
}
|
package/src/lib/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EmbeddingProfileSummary } from "../db/embedding-queries";
|
|
1
2
|
import type { TrapStats } from "../db/repository";
|
|
2
3
|
import {
|
|
3
4
|
type Trap,
|
|
@@ -10,7 +11,20 @@ import {
|
|
|
10
11
|
type TrapUpdate,
|
|
11
12
|
} from "../domain/trap";
|
|
12
13
|
import type { SearchMode, TrapStatus } from "./constants";
|
|
13
|
-
import {
|
|
14
|
+
import type { EmbeddingConfig } from "./embedder";
|
|
15
|
+
import {
|
|
16
|
+
defaultEmbeddingRuntime,
|
|
17
|
+
embeddingRuntimeFrom,
|
|
18
|
+
type EmbeddingRuntime,
|
|
19
|
+
type EmbeddingRuntimeInput,
|
|
20
|
+
type EmbeddingRuntimeStatus,
|
|
21
|
+
} from "./embedding-runtime";
|
|
22
|
+
import {
|
|
23
|
+
type ConfigWriteResult,
|
|
24
|
+
type EmbeddingSettings,
|
|
25
|
+
loadCodetrapConfig,
|
|
26
|
+
setCodetrapEmbeddingSettings,
|
|
27
|
+
} from "./config";
|
|
14
28
|
import { summarizeEmbeddingState, type EmbeddingStateSummary, type EmbeddingStatsResult } from "./embedding-health";
|
|
15
29
|
import { normalizeScope, ScopedRepositoryContext, type ScopedRepository } from "./scope-context";
|
|
16
30
|
import { importTrapArchive } from "./trap-archive";
|
|
@@ -33,14 +47,32 @@ export {
|
|
|
33
47
|
|
|
34
48
|
export type { EmbeddingStateSummary };
|
|
35
49
|
export type TrapEmbeddingStats = EmbeddingStatsResult;
|
|
50
|
+
export type TrapEmbeddingProfiles = {
|
|
51
|
+
project: EmbeddingProfileSummary[] | null;
|
|
52
|
+
global: EmbeddingProfileSummary[] | null;
|
|
53
|
+
};
|
|
54
|
+
export type EmbeddingScopeStatus = EmbeddingStateSummary & {
|
|
55
|
+
profiles: EmbeddingProfileSummary[];
|
|
56
|
+
};
|
|
57
|
+
export type TrapEmbeddingStatus = {
|
|
58
|
+
runtime: EmbeddingRuntimeStatus;
|
|
59
|
+
project: EmbeddingScopeStatus | null;
|
|
60
|
+
global: EmbeddingScopeStatus | null;
|
|
61
|
+
};
|
|
36
62
|
|
|
37
63
|
export class TrapStore {
|
|
38
64
|
private readonly scopes: ScopedRepositoryContext;
|
|
39
|
-
private readonly
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
private readonly embeddings: EmbeddingRuntime;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
cwd: string,
|
|
69
|
+
embeddings?: EmbeddingRuntimeInput,
|
|
70
|
+
private readonly home?: string
|
|
71
|
+
) {
|
|
72
|
+
this.embeddings = embeddings === undefined
|
|
73
|
+
? defaultEmbeddingRuntime(process.env, loadCodetrapConfig(home))
|
|
74
|
+
: embeddingRuntimeFrom(embeddings);
|
|
75
|
+
this.scopes = new ScopedRepositoryContext(cwd, this.embeddings, home);
|
|
44
76
|
}
|
|
45
77
|
|
|
46
78
|
add(input: TrapInput): { id: number; scope: string } {
|
|
@@ -195,6 +227,34 @@ export class TrapStore {
|
|
|
195
227
|
};
|
|
196
228
|
}
|
|
197
229
|
|
|
230
|
+
embeddingProfiles(opts: { scope?: string } = {}): TrapEmbeddingProfiles {
|
|
231
|
+
const scope = opts.scope ? normalizeScope(opts.scope) : null;
|
|
232
|
+
const project = scope === "global"
|
|
233
|
+
? null
|
|
234
|
+
: this.scopes.repositoryEntry("project")
|
|
235
|
+
? this.scopes.repositoryFor("project").embeddingProfiles({ scope: "project" })
|
|
236
|
+
: null;
|
|
237
|
+
const global = scope === "project"
|
|
238
|
+
? null
|
|
239
|
+
: this.scopes.repositoryFor("global").embeddingProfiles({ scope: "global" });
|
|
240
|
+
return { project, global };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async embeddingStatus(opts: { scope?: string } = {}): Promise<TrapEmbeddingStatus> {
|
|
244
|
+
const runtime = await this.embeddingRuntimeHealth();
|
|
245
|
+
const stats = this.embeddingStats(opts);
|
|
246
|
+
const profiles = this.embeddingProfiles(opts);
|
|
247
|
+
return {
|
|
248
|
+
runtime,
|
|
249
|
+
project: stats.project
|
|
250
|
+
? { ...stats.project, profiles: profiles.project ?? [] }
|
|
251
|
+
: null,
|
|
252
|
+
global: stats.global
|
|
253
|
+
? { ...stats.global, profiles: profiles.global ?? [] }
|
|
254
|
+
: null,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
198
258
|
diagnostics(): {
|
|
199
259
|
mis_scoped_traps: {
|
|
200
260
|
global_db_project_traps: Pick<Trap, "id" | "title" | "scope" | "project_path" | "status">[];
|
|
@@ -238,16 +298,24 @@ export class TrapStore {
|
|
|
238
298
|
return this.scopes.projectRoot();
|
|
239
299
|
}
|
|
240
300
|
|
|
241
|
-
|
|
242
|
-
return this.
|
|
301
|
+
embeddingConfig(): EmbeddingConfig | null {
|
|
302
|
+
return this.embeddings.config();
|
|
243
303
|
}
|
|
244
304
|
|
|
245
|
-
|
|
246
|
-
return this.
|
|
305
|
+
embeddingRuntimeStatus(): EmbeddingRuntimeStatus {
|
|
306
|
+
return this.embeddings.status();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
embeddingRuntimeHealth(): Promise<EmbeddingRuntimeStatus> {
|
|
310
|
+
return this.embeddings.health();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
configureEmbeddings(settings: EmbeddingSettings): ConfigWriteResult {
|
|
314
|
+
return setCodetrapEmbeddingSettings(settings, this.home);
|
|
247
315
|
}
|
|
248
316
|
|
|
249
317
|
forCwd(cwd: string): TrapStore {
|
|
250
|
-
return new TrapStore(cwd, this.
|
|
318
|
+
return new TrapStore(cwd, this.embeddings, this.home);
|
|
251
319
|
}
|
|
252
320
|
|
|
253
321
|
async ensureEmbeddings(opts: { scope?: string; category?: string; limit?: number; force?: boolean; batchSize?: number } = {}): Promise<{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Trap, TrapInput, TrapUpdate } from "../domain/trap";
|
|
3
|
-
import type
|
|
3
|
+
import { embeddingProfileId, type EmbeddingConfig, type StoredEmbedding } from "./embedder";
|
|
4
4
|
import { buildSearchText, SEARCH_TEXT_FIELD_NAMES, type SearchTextFields } from "./search-normalizer";
|
|
5
5
|
import { parseTrapPathGlobs, parseTrapTags } from "./trap-json-fields";
|
|
6
6
|
|
|
@@ -74,6 +74,7 @@ export function embeddingIsFresh(trap: Trap, embedding: StoredEmbedding | null,
|
|
|
74
74
|
embedding.provider === config.provider &&
|
|
75
75
|
embedding.model === config.model &&
|
|
76
76
|
embedding.dimensions === config.dimensions &&
|
|
77
|
+
embedding.profile_id === embeddingProfileId(config) &&
|
|
77
78
|
embedding.passage_version === config.passageVersion &&
|
|
78
79
|
embedding.passage_hash === passageHashForTrap(trap)
|
|
79
80
|
);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export type ReviewSessionLike = {
|
|
2
|
+
id: string;
|
|
3
|
+
pending_count?: number | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ReviewCandidateView = "inbox" | "reviewed";
|
|
7
|
+
|
|
8
|
+
export type ReviewCandidateLike = {
|
|
9
|
+
id: string;
|
|
10
|
+
status: string;
|
|
11
|
+
quality_score: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ReviewSummaryLike = {
|
|
15
|
+
pending_count?: number | null;
|
|
16
|
+
pending_session_count?: number | null;
|
|
17
|
+
high_quality_pending_count?: number | null;
|
|
18
|
+
needs_edit_count?: number | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ReviewCandidateDraftFields = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
export type ReviewCandidateTrapDraft = {
|
|
24
|
+
title: string;
|
|
25
|
+
category: string;
|
|
26
|
+
scope: string;
|
|
27
|
+
severity: string;
|
|
28
|
+
tags: string[];
|
|
29
|
+
path_globs: string[];
|
|
30
|
+
module: string | null;
|
|
31
|
+
owner: string | null;
|
|
32
|
+
context: string;
|
|
33
|
+
mistake: string;
|
|
34
|
+
fix: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ReviewCandidateMutationPayloadArgs = {
|
|
38
|
+
projectRoot: string | null;
|
|
39
|
+
sessionId: string | null;
|
|
40
|
+
candidateId: string | null;
|
|
41
|
+
trap: ReviewCandidateTrapDraft;
|
|
42
|
+
extra?: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ReviewCandidateMutationPayload = {
|
|
46
|
+
projectRoot: string | null;
|
|
47
|
+
sessionId: string | null;
|
|
48
|
+
candidateId: string | null;
|
|
49
|
+
trap: ReviewCandidateTrapDraft;
|
|
50
|
+
} & Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
export type ReviewQueueModelArgs<TCandidate extends ReviewCandidateLike> = {
|
|
53
|
+
candidates: TCandidate[];
|
|
54
|
+
candidateView: ReviewCandidateView;
|
|
55
|
+
candidateId?: string | null;
|
|
56
|
+
candidateReview?: ReviewSummaryLike | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function selectedReviewSessionId(
|
|
60
|
+
sessions: ReviewSessionLike[],
|
|
61
|
+
currentSessionId: string | null | undefined
|
|
62
|
+
): string | null {
|
|
63
|
+
if (currentSessionId && sessions.some((session) => session.id === currentSessionId)) {
|
|
64
|
+
return currentSessionId;
|
|
65
|
+
}
|
|
66
|
+
return sessions.find((session) => (session.pending_count || 0) > 0)?.id || sessions[0]?.id || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function reviewQueueModel<TCandidate extends ReviewCandidateLike>(
|
|
70
|
+
args: ReviewQueueModelArgs<TCandidate>
|
|
71
|
+
) {
|
|
72
|
+
const pendingCount = args.candidates.filter((candidate) => candidate.status === "proposed").length;
|
|
73
|
+
const reviewedCount = args.candidates.length - pendingCount;
|
|
74
|
+
const visibleCandidates = sortedReviewCandidates(args.candidates, args.candidateView);
|
|
75
|
+
return {
|
|
76
|
+
pendingCount,
|
|
77
|
+
reviewedCount,
|
|
78
|
+
visibleCandidates,
|
|
79
|
+
selectedCandidateId: selectedReviewCandidateId(visibleCandidates, args.candidateId),
|
|
80
|
+
summary: visibleReviewSummary(args.candidateReview),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function sortedReviewCandidates<TCandidate extends ReviewCandidateLike>(
|
|
85
|
+
candidates: TCandidate[],
|
|
86
|
+
candidateView: ReviewCandidateView
|
|
87
|
+
): TCandidate[] {
|
|
88
|
+
return [...candidates]
|
|
89
|
+
.filter((candidate) => candidateVisibleInReviewView(candidate, candidateView))
|
|
90
|
+
.sort((a, b) => reviewStatusRank(a.status) - reviewStatusRank(b.status) || b.quality_score - a.quality_score);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function selectedReviewCandidateId(
|
|
94
|
+
candidates: ReviewCandidateLike[],
|
|
95
|
+
currentCandidateId: string | null | undefined
|
|
96
|
+
): string | null {
|
|
97
|
+
if (currentCandidateId && candidates.some((candidate) => candidate.id === currentCandidateId)) {
|
|
98
|
+
return currentCandidateId;
|
|
99
|
+
}
|
|
100
|
+
return candidates[0]?.id || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function visibleReviewSummary(summary: ReviewSummaryLike | null | undefined): ReviewSummaryLike | null {
|
|
104
|
+
if (!summary || (summary.pending_count || 0) === 0) return null;
|
|
105
|
+
return summary;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function candidateVisibleInReviewView(
|
|
109
|
+
candidate: ReviewCandidateLike,
|
|
110
|
+
candidateView: ReviewCandidateView
|
|
111
|
+
): boolean {
|
|
112
|
+
return candidateView === "inbox" ? candidate.status === "proposed" : candidate.status !== "proposed";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function reviewStatusRank(status: string): number {
|
|
116
|
+
return status === "proposed" ? 0 : status === "accepted" ? 1 : 2;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function reviewCandidateTrapDraft(fields: ReviewCandidateDraftFields): ReviewCandidateTrapDraft {
|
|
120
|
+
return {
|
|
121
|
+
title: String(fields.title || ""),
|
|
122
|
+
category: String(fields.category || ""),
|
|
123
|
+
scope: String(fields.scope || ""),
|
|
124
|
+
severity: String(fields.severity || ""),
|
|
125
|
+
tags: reviewSplitList(fields.tags),
|
|
126
|
+
path_globs: reviewSplitList(fields.path_globs),
|
|
127
|
+
module: reviewBlankToNull(fields.module),
|
|
128
|
+
owner: reviewBlankToNull(fields.owner),
|
|
129
|
+
context: String(fields.context || ""),
|
|
130
|
+
mistake: String(fields.mistake || ""),
|
|
131
|
+
fix: String(fields.fix || ""),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function reviewCandidateMutationPayload(args: ReviewCandidateMutationPayloadArgs): ReviewCandidateMutationPayload {
|
|
136
|
+
return {
|
|
137
|
+
projectRoot: args.projectRoot,
|
|
138
|
+
sessionId: args.sessionId,
|
|
139
|
+
candidateId: args.candidateId,
|
|
140
|
+
trap: args.trap,
|
|
141
|
+
...(args.extra || {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function reviewSplitList(value: unknown): string[] {
|
|
146
|
+
const values = Array.isArray(value) ? value : String(value || "").split(",");
|
|
147
|
+
return [...new Set(values.map((item) => String(item).trim()).filter(Boolean))];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function reviewBlankToNull(value: unknown): string | null {
|
|
151
|
+
const text = String(value || "").trim();
|
|
152
|
+
return text ? text : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const CLIENT_REVIEW_FUNCTIONS = [
|
|
156
|
+
selectedReviewSessionId,
|
|
157
|
+
reviewQueueModel,
|
|
158
|
+
sortedReviewCandidates,
|
|
159
|
+
selectedReviewCandidateId,
|
|
160
|
+
visibleReviewSummary,
|
|
161
|
+
candidateVisibleInReviewView,
|
|
162
|
+
reviewStatusRank,
|
|
163
|
+
reviewCandidateTrapDraft,
|
|
164
|
+
reviewCandidateMutationPayload,
|
|
165
|
+
reviewSplitList,
|
|
166
|
+
reviewBlankToNull,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
export const WEB_REVIEW_CLIENT_SCRIPT = CLIENT_REVIEW_FUNCTIONS
|
|
170
|
+
.map((fn) => ` ${fn.toString().replace(/\n/g, "\n ")}`)
|
|
171
|
+
.join("\n\n");
|