codetrap 0.1.7 → 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 +151 -52
- 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 +144 -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 +28 -3
- 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/session-store.ts
CHANGED
|
@@ -10,6 +10,15 @@ import { join } from "node:path";
|
|
|
10
10
|
import type { CandidateTrap, CandidateTrapDocument, SessionIndexDocument, SessionMetadata, SessionNote } from "../domain/session";
|
|
11
11
|
import { parseSessionNoteKind, SESSION_VERSION } from "../domain/session";
|
|
12
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";
|
|
13
22
|
import {
|
|
14
23
|
ACTIVE_SESSION_FILE,
|
|
15
24
|
CANDIDATES_FILE,
|
|
@@ -29,8 +38,8 @@ import {
|
|
|
29
38
|
sessionRelativeFile,
|
|
30
39
|
SESSIONS_DIR,
|
|
31
40
|
} from "./session-codec";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
41
|
+
import { uniqueStrings } from "./string-list";
|
|
42
|
+
import { mergeCandidateTraps, proposeCandidateTraps, type CandidateDraft } from "./session-capture";
|
|
34
43
|
|
|
35
44
|
export interface StartSessionArgs {
|
|
36
45
|
goal: string;
|
|
@@ -50,7 +59,6 @@ export interface CloseSessionResult {
|
|
|
50
59
|
session: SessionMetadata;
|
|
51
60
|
recap_path: string;
|
|
52
61
|
candidate_count: number;
|
|
53
|
-
traps_written: number;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
export interface AcceptCandidateResult {
|
|
@@ -89,6 +97,11 @@ export interface RemoveSessionCandidatesResult {
|
|
|
89
97
|
candidates: CandidateTrap[];
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
export interface AddSessionCandidateArgs {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
draft: CandidateDraft;
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
export class SessionStore {
|
|
93
106
|
constructor(private readonly projectRoot: string) {}
|
|
94
107
|
|
|
@@ -219,7 +232,10 @@ export class SessionStore {
|
|
|
219
232
|
|
|
220
233
|
const closedAt = now.toISOString();
|
|
221
234
|
const notes = this.readNotes(session.id);
|
|
222
|
-
const
|
|
235
|
+
const existingCandidates = this.readCandidateDocument(session.id).candidates;
|
|
236
|
+
const candidates = proposeTraps
|
|
237
|
+
? mergeCandidateTraps(existingCandidates, proposeCandidateTraps(session, notes))
|
|
238
|
+
: existingCandidates;
|
|
223
239
|
if (proposeTraps) this.writeCandidateDocument(session.id, candidates);
|
|
224
240
|
|
|
225
241
|
const updated = {
|
|
@@ -237,7 +253,6 @@ export class SessionStore {
|
|
|
237
253
|
session: updated,
|
|
238
254
|
recap_path: sessionRelativeFile(updated.id, RECAP_FILE),
|
|
239
255
|
candidate_count: candidates.length,
|
|
240
|
-
traps_written: 0,
|
|
241
256
|
};
|
|
242
257
|
}
|
|
243
258
|
|
|
@@ -253,6 +268,22 @@ export class SessionStore {
|
|
|
253
268
|
return { session, candidate };
|
|
254
269
|
}
|
|
255
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
|
+
|
|
256
287
|
recordCandidateConflictCheck(
|
|
257
288
|
candidateId: string,
|
|
258
289
|
args: {
|
|
@@ -265,17 +296,16 @@ export class SessionStore {
|
|
|
265
296
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
266
297
|
const session = this.requireSession(sessionId);
|
|
267
298
|
const document = this.readCandidateDocument(sessionId);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
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
|
+
));
|
|
279
309
|
}
|
|
280
310
|
|
|
281
311
|
saveCandidateTrap(
|
|
@@ -288,17 +318,11 @@ export class SessionStore {
|
|
|
288
318
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
289
319
|
const session = this.requireSession(sessionId);
|
|
290
320
|
const document = this.readCandidateDocument(sessionId);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
candidate.trap = args.trap;
|
|
297
|
-
candidate.quality_score = scored.score;
|
|
298
|
-
candidate.quality = scored.quality;
|
|
299
|
-
this.writeCandidateDocument(session.id, document.candidates);
|
|
300
|
-
this.refreshSessionSummaries(session.id);
|
|
301
|
-
return { session, candidate };
|
|
321
|
+
return this.saveCandidateDocumentMutation(session, saveCandidateTrapInDocument(
|
|
322
|
+
document.candidates,
|
|
323
|
+
candidateId,
|
|
324
|
+
{ sessionId, trap: args.trap }
|
|
325
|
+
));
|
|
302
326
|
}
|
|
303
327
|
|
|
304
328
|
acceptCandidate(
|
|
@@ -319,24 +343,24 @@ export class SessionStore {
|
|
|
319
343
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
320
344
|
const session = this.requireSession(sessionId);
|
|
321
345
|
const document = this.readCandidateDocument(sessionId);
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
));
|
|
336
360
|
|
|
337
361
|
return {
|
|
338
362
|
session,
|
|
339
|
-
candidate,
|
|
363
|
+
candidate: accepted.candidate,
|
|
340
364
|
trap_id: args.trapId,
|
|
341
365
|
scope: args.scope,
|
|
342
366
|
evidence_id: args.evidenceId,
|
|
@@ -352,44 +376,29 @@ export class SessionStore {
|
|
|
352
376
|
const sessionId = this.resolveSessionId(args.sessionId);
|
|
353
377
|
const session = this.requireSession(sessionId);
|
|
354
378
|
const document = this.readCandidateDocument(sessionId);
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (args.reason) candidate.rejection_reason = args.reason;
|
|
362
|
-
this.writeCandidateDocument(session.id, document.candidates);
|
|
363
|
-
this.refreshSessionSummaries(session.id);
|
|
364
|
-
return { session, candidate };
|
|
379
|
+
return this.saveCandidateDocumentMutation(session, rejectCandidateInDocument(
|
|
380
|
+
document.candidates,
|
|
381
|
+
candidateId,
|
|
382
|
+
{ sessionId, reason: args.reason },
|
|
383
|
+
now
|
|
384
|
+
));
|
|
365
385
|
}
|
|
366
386
|
|
|
367
387
|
removeCandidates(sessionId: string | undefined, candidateIds: string[]): RemoveSessionCandidatesResult {
|
|
368
388
|
const resolvedSessionId = this.resolveSessionId(sessionId);
|
|
369
389
|
const session = this.requireSession(resolvedSessionId);
|
|
370
|
-
const removeIds = new Set(uniqueStrings(candidateIds));
|
|
371
|
-
if (removeIds.size === 0) {
|
|
372
|
-
return {
|
|
373
|
-
session,
|
|
374
|
-
removed_count: 0,
|
|
375
|
-
removed_candidate_ids: [],
|
|
376
|
-
candidates: this.readCandidateDocument(session.id).candidates,
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
390
|
const document = this.readCandidateDocument(session.id);
|
|
381
|
-
const removed = document.candidates
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.writeCandidateDocument(session.id, candidates);
|
|
391
|
+
const removed = removeCandidatesFromDocument(document.candidates, candidateIds);
|
|
392
|
+
if (removed.removed.length > 0) {
|
|
393
|
+
this.writeCandidateDocument(session.id, removed.candidates);
|
|
385
394
|
this.refreshSessionSummaries(session.id);
|
|
386
395
|
}
|
|
387
396
|
|
|
388
397
|
return {
|
|
389
398
|
session,
|
|
390
|
-
removed_count: removed.length,
|
|
391
|
-
removed_candidate_ids: removed.map((candidate) => candidate.id),
|
|
392
|
-
candidates,
|
|
399
|
+
removed_count: removed.removed.length,
|
|
400
|
+
removed_candidate_ids: removed.removed.map((candidate) => candidate.id),
|
|
401
|
+
candidates: removed.candidates,
|
|
393
402
|
};
|
|
394
403
|
}
|
|
395
404
|
|
|
@@ -488,6 +497,15 @@ export class SessionStore {
|
|
|
488
497
|
return candidate;
|
|
489
498
|
}
|
|
490
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
|
+
|
|
491
509
|
private readCandidateDocument(id: string): CandidateTrapDocument {
|
|
492
510
|
const path = this.candidatesPath(id);
|
|
493
511
|
if (!existsSync(path)) {
|
|
@@ -605,7 +623,3 @@ export class SessionStore {
|
|
|
605
623
|
return join(this.sessionDir(id), CANDIDATES_FILE);
|
|
606
624
|
}
|
|
607
625
|
}
|
|
608
|
-
|
|
609
|
-
function uniqueStrings(values: string[]): string[] {
|
|
610
|
-
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
611
|
-
}
|
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,18 +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
|
|
65
|
+
private readonly embeddings: EmbeddingRuntime;
|
|
40
66
|
|
|
41
67
|
constructor(
|
|
42
68
|
cwd: string,
|
|
43
|
-
|
|
69
|
+
embeddings?: EmbeddingRuntimeInput,
|
|
44
70
|
private readonly home?: string
|
|
45
71
|
) {
|
|
46
|
-
this.
|
|
47
|
-
|
|
72
|
+
this.embeddings = embeddings === undefined
|
|
73
|
+
? defaultEmbeddingRuntime(process.env, loadCodetrapConfig(home))
|
|
74
|
+
: embeddingRuntimeFrom(embeddings);
|
|
75
|
+
this.scopes = new ScopedRepositoryContext(cwd, this.embeddings, home);
|
|
48
76
|
}
|
|
49
77
|
|
|
50
78
|
add(input: TrapInput): { id: number; scope: string } {
|
|
@@ -199,6 +227,34 @@ export class TrapStore {
|
|
|
199
227
|
};
|
|
200
228
|
}
|
|
201
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
|
+
|
|
202
258
|
diagnostics(): {
|
|
203
259
|
mis_scoped_traps: {
|
|
204
260
|
global_db_project_traps: Pick<Trap, "id" | "title" | "scope" | "project_path" | "status">[];
|
|
@@ -242,16 +298,24 @@ export class TrapStore {
|
|
|
242
298
|
return this.scopes.projectRoot();
|
|
243
299
|
}
|
|
244
300
|
|
|
245
|
-
|
|
246
|
-
return this.
|
|
301
|
+
embeddingConfig(): EmbeddingConfig | null {
|
|
302
|
+
return this.embeddings.config();
|
|
247
303
|
}
|
|
248
304
|
|
|
249
|
-
|
|
250
|
-
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);
|
|
251
315
|
}
|
|
252
316
|
|
|
253
317
|
forCwd(cwd: string): TrapStore {
|
|
254
|
-
return new TrapStore(cwd, this.
|
|
318
|
+
return new TrapStore(cwd, this.embeddings, this.home);
|
|
255
319
|
}
|
|
256
320
|
|
|
257
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");
|