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
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import type { CandidateTrap } from "../domain/session";
|
|
2
|
-
import {
|
|
2
|
+
import type { Scope } from "./constants";
|
|
3
3
|
import type { TrapOperations } from "./trap-operations";
|
|
4
|
+
import {
|
|
5
|
+
capturedCandidateDraft,
|
|
6
|
+
capturedTrapInput,
|
|
7
|
+
candidateWithTrapEdits,
|
|
8
|
+
captureGoal,
|
|
9
|
+
} from "./session-capture";
|
|
10
|
+
import { candidateAcceptedScope } from "./session-candidate-scope";
|
|
4
11
|
import { findCandidateConflicts, type CandidateConflict } from "./session-conflicts";
|
|
12
|
+
import {
|
|
13
|
+
projectCandidateReviewSummary,
|
|
14
|
+
sessionCandidateReviewSummary,
|
|
15
|
+
sessionIndexEntryWithReview,
|
|
16
|
+
type ProjectCandidateReviewSummary,
|
|
17
|
+
} from "./session-review";
|
|
5
18
|
import type {
|
|
6
19
|
AcceptCandidateResult,
|
|
7
20
|
AddSessionNoteArgs,
|
|
8
21
|
CloseSessionResult,
|
|
22
|
+
DeleteSessionResult,
|
|
23
|
+
PruneSessionsResult,
|
|
24
|
+
RemoveSessionCandidatesResult,
|
|
9
25
|
SessionStore,
|
|
10
26
|
StartSessionArgs,
|
|
11
27
|
} from "./session-store";
|
|
28
|
+
import { uniqueStrings } from "./string-list";
|
|
12
29
|
|
|
13
30
|
export type SessionAcceptRequest = {
|
|
14
31
|
candidateId: string;
|
|
@@ -30,6 +47,21 @@ export type SessionRejectRequest = {
|
|
|
30
47
|
reason?: string | null;
|
|
31
48
|
};
|
|
32
49
|
|
|
50
|
+
export type SessionPruneRequest = {
|
|
51
|
+
olderThanDays: number;
|
|
52
|
+
apply: boolean;
|
|
53
|
+
now?: Date;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type SessionCaptureRequest = {
|
|
57
|
+
trap: Record<string, unknown>;
|
|
58
|
+
goal?: string;
|
|
59
|
+
kind?: string;
|
|
60
|
+
relatedFiles?: string[];
|
|
61
|
+
sourceRef?: string;
|
|
62
|
+
evidenceNote?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
33
65
|
export type SessionConflictResult = {
|
|
34
66
|
success: false;
|
|
35
67
|
session_id: string;
|
|
@@ -43,6 +75,18 @@ export type SessionAcceptSuccess = AcceptCandidateResult & {
|
|
|
43
75
|
|
|
44
76
|
export type SessionAcceptResult = SessionAcceptSuccess | SessionConflictResult;
|
|
45
77
|
|
|
78
|
+
export type SessionCaptureResult = {
|
|
79
|
+
success: true;
|
|
80
|
+
session: ReturnType<SessionStore["getSession"]>;
|
|
81
|
+
candidate: CandidateTrap;
|
|
82
|
+
candidate_count: number;
|
|
83
|
+
candidates_path: string;
|
|
84
|
+
created_session: boolean;
|
|
85
|
+
closed_session: boolean;
|
|
86
|
+
duplicate: boolean;
|
|
87
|
+
recap_path: string | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
46
90
|
export class SessionOperations {
|
|
47
91
|
constructor(
|
|
48
92
|
private readonly sessions: SessionStore,
|
|
@@ -58,11 +102,23 @@ export class SessionOperations {
|
|
|
58
102
|
}
|
|
59
103
|
|
|
60
104
|
status() {
|
|
61
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
...this.sessions.status(),
|
|
107
|
+
candidate_review: this.candidateReviewSummary(),
|
|
108
|
+
};
|
|
62
109
|
}
|
|
63
110
|
|
|
64
111
|
listSessions(args: { status?: string; limit?: number } = {}) {
|
|
65
|
-
return this.sessions.listSessions(args)
|
|
112
|
+
return this.sessions.listSessions(args).map((session) =>
|
|
113
|
+
sessionIndexEntryWithReview(session, this.sessions.candidateDocument(session.id).candidates)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
candidateReviewSummary(): ProjectCandidateReviewSummary {
|
|
118
|
+
const sessions = this.sessions.listSessions({ status: "all" }).map((session) =>
|
|
119
|
+
sessionCandidateReviewSummary(session, this.sessions.candidateDocument(session.id).candidates)
|
|
120
|
+
);
|
|
121
|
+
return projectCandidateReviewSummary(sessions);
|
|
66
122
|
}
|
|
67
123
|
|
|
68
124
|
showSession(id: string) {
|
|
@@ -77,6 +133,39 @@ export class SessionOperations {
|
|
|
77
133
|
return this.sessions.closeSession(id, proposeTraps);
|
|
78
134
|
}
|
|
79
135
|
|
|
136
|
+
captureCandidate(request: SessionCaptureRequest): SessionCaptureResult {
|
|
137
|
+
const trap = capturedTrapInput(request.trap);
|
|
138
|
+
const active = this.sessions.status().session;
|
|
139
|
+
const createdSession = active === null;
|
|
140
|
+
const session = active ?? this.sessions.startSession({
|
|
141
|
+
goal: captureGoal(request.goal, trap.title),
|
|
142
|
+
module: trap.module,
|
|
143
|
+
owner: trap.owner,
|
|
144
|
+
});
|
|
145
|
+
const captured = this.sessions.addCandidate({
|
|
146
|
+
sessionId: session.id,
|
|
147
|
+
draft: capturedCandidateDraft(session, {
|
|
148
|
+
trap,
|
|
149
|
+
kind: request.kind,
|
|
150
|
+
relatedFiles: request.relatedFiles,
|
|
151
|
+
sourceRef: request.sourceRef,
|
|
152
|
+
evidenceNote: request.evidenceNote,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
const closed = createdSession ? this.sessions.closeSession(session.id, false) : null;
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
session: closed?.session ?? captured.session,
|
|
159
|
+
candidate: captured.candidate,
|
|
160
|
+
candidate_count: this.sessions.candidateDocument(session.id).candidates.length,
|
|
161
|
+
candidates_path: captured.candidates_path,
|
|
162
|
+
created_session: createdSession,
|
|
163
|
+
closed_session: closed !== null,
|
|
164
|
+
duplicate: captured.duplicate,
|
|
165
|
+
recap_path: closed?.recap_path ?? null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
80
169
|
candidateDocument(id?: string) {
|
|
81
170
|
return this.sessions.candidateDocument(id);
|
|
82
171
|
}
|
|
@@ -155,60 +244,31 @@ export class SessionOperations {
|
|
|
155
244
|
reason: request.reason,
|
|
156
245
|
});
|
|
157
246
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function candidateWithTrapEdits(candidate: CandidateTrap, edit: Record<string, unknown> | undefined): CandidateTrap {
|
|
161
|
-
return {
|
|
162
|
-
...candidate,
|
|
163
|
-
trap: normalizeCandidateTrap({
|
|
164
|
-
...candidate.trap,
|
|
165
|
-
...trapEdits(edit),
|
|
166
|
-
}) as CandidateTrap["trap"],
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function normalizeCandidateTrap(args: Record<string, unknown>) {
|
|
171
|
-
return buildTrapInput({
|
|
172
|
-
...args,
|
|
173
|
-
tags: stringArray(args.tags),
|
|
174
|
-
path_globs: stringArray(args.path_globs),
|
|
175
|
-
module: optionalText(args.module),
|
|
176
|
-
owner: optionalText(args.owner),
|
|
177
|
-
before_code: optionalText(args.before_code),
|
|
178
|
-
after_code: optionalText(args.after_code),
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
247
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (typeof value === "string") {
|
|
185
|
-
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
186
|
-
return values.length > 0 ? values : undefined;
|
|
248
|
+
deleteSession(sessionId: string): DeleteSessionResult {
|
|
249
|
+
return this.sessions.deleteSession(sessionId);
|
|
187
250
|
}
|
|
188
|
-
return undefined;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function optionalText(value: unknown): string | null | undefined {
|
|
192
|
-
if (value === undefined) return undefined;
|
|
193
|
-
if (value === null) return null;
|
|
194
|
-
const text = String(value).trim();
|
|
195
|
-
return text ? text : null;
|
|
196
|
-
}
|
|
197
251
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
252
|
+
pruneSessions(request: SessionPruneRequest): PruneSessionsResult {
|
|
253
|
+
const now = request.now ?? new Date();
|
|
254
|
+
const cutoff = new Date(now.getTime() - request.olderThanDays * 24 * 60 * 60 * 1000);
|
|
255
|
+
return this.sessions.pruneSessions({ cutoff, dryRun: !request.apply });
|
|
256
|
+
}
|
|
203
257
|
|
|
204
|
-
|
|
205
|
-
|
|
258
|
+
cleanupDeletedTrapCandidates(sessionId?: string): RemoveSessionCandidatesResult {
|
|
259
|
+
const document = this.sessions.candidateDocument(sessionId);
|
|
260
|
+
const missingCandidateIds = document.candidates
|
|
261
|
+
.filter((candidate) => candidate.status === "accepted")
|
|
262
|
+
.filter((candidate) => {
|
|
263
|
+
const trapId = candidate.accepted_trap_id;
|
|
264
|
+
if (trapId === undefined) return true;
|
|
265
|
+
return !this.traps.getTrapDetails(trapId, candidateAcceptedScope(candidate));
|
|
266
|
+
})
|
|
267
|
+
.map((candidate) => candidate.id);
|
|
268
|
+
return this.sessions.removeCandidates(sessionId, missingCandidateIds);
|
|
269
|
+
}
|
|
206
270
|
}
|
|
207
271
|
|
|
208
272
|
function candidateRelatedFiles(candidate: CandidateTrap): string[] {
|
|
209
273
|
return uniqueStrings(candidate.evidence.flatMap((evidence) => evidence.related_files ?? []));
|
|
210
274
|
}
|
|
211
|
-
|
|
212
|
-
function uniqueStrings(values: string[]): string[] {
|
|
213
|
-
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
214
|
-
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { CandidateTrap, SessionIndexEntry, SessionMetadata } from "../domain/session";
|
|
2
|
+
import type { Scope } from "./constants";
|
|
3
|
+
import { candidateAcceptedScope } from "./session-candidate-scope";
|
|
4
|
+
import {
|
|
5
|
+
CANDIDATES_FILE,
|
|
6
|
+
NOTES_FILE,
|
|
7
|
+
RECAP_FILE,
|
|
8
|
+
sessionRelativeDir,
|
|
9
|
+
sessionRelativeFile,
|
|
10
|
+
} from "./session-codec";
|
|
11
|
+
import type { CandidateConflict } from "./session-conflicts";
|
|
12
|
+
import type { TrapOperations } from "./trap-operations";
|
|
13
|
+
|
|
14
|
+
export type SessionPayload = SessionMetadata & {
|
|
15
|
+
session_dir: string;
|
|
16
|
+
notes_path: string;
|
|
17
|
+
recap_path: string;
|
|
18
|
+
candidate_traps_path: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SessionConflictPayload = {
|
|
22
|
+
success: false;
|
|
23
|
+
error: string;
|
|
24
|
+
session_id: string;
|
|
25
|
+
candidate_id: string;
|
|
26
|
+
possible_conflicts: CandidateConflict[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SessionCliConflictPayload = SessionConflictPayload & {
|
|
30
|
+
next_actions: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type SessionAcceptPayload = {
|
|
34
|
+
success: true;
|
|
35
|
+
session_id: string;
|
|
36
|
+
candidate_id: string;
|
|
37
|
+
status: CandidateTrap["status"];
|
|
38
|
+
trap_id: number;
|
|
39
|
+
scope: string;
|
|
40
|
+
evidence_id: number | null;
|
|
41
|
+
superseded_id: number | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SessionRejectPayload = {
|
|
45
|
+
success: true;
|
|
46
|
+
session_id: string;
|
|
47
|
+
candidate_id: string;
|
|
48
|
+
status: CandidateTrap["status"];
|
|
49
|
+
reason: string | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SessionCleanupPayload = {
|
|
53
|
+
success: true;
|
|
54
|
+
session_id: string;
|
|
55
|
+
removed_count: number;
|
|
56
|
+
removed_candidate_ids: string[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type SessionCandidateReview =
|
|
60
|
+
| { status: "pending"; label: string }
|
|
61
|
+
| {
|
|
62
|
+
status: "accepted";
|
|
63
|
+
label: string;
|
|
64
|
+
trap_id: number;
|
|
65
|
+
scope: string;
|
|
66
|
+
trap_present: true;
|
|
67
|
+
trap_status: string;
|
|
68
|
+
trap_title: string;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
status: "accepted_missing";
|
|
72
|
+
label: string;
|
|
73
|
+
trap_id?: number;
|
|
74
|
+
scope?: string;
|
|
75
|
+
trap_present: false;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
status: "rejected";
|
|
79
|
+
label: string;
|
|
80
|
+
rejected_at?: string;
|
|
81
|
+
rejection_reason?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ReviewedSessionCandidate = CandidateTrap & { review: SessionCandidateReview };
|
|
85
|
+
|
|
86
|
+
export type CandidateReviewCounts = {
|
|
87
|
+
candidate_count: number;
|
|
88
|
+
pending_count: number;
|
|
89
|
+
reviewed_count: number;
|
|
90
|
+
accepted_count: number;
|
|
91
|
+
rejected_count: number;
|
|
92
|
+
high_quality_pending_count: number;
|
|
93
|
+
needs_edit_count: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type SessionCandidateReviewSummary = CandidateReviewCounts & {
|
|
97
|
+
session_id: string;
|
|
98
|
+
goal: string;
|
|
99
|
+
status: SessionIndexEntry["status"];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ProjectCandidateReviewSummary = CandidateReviewCounts & {
|
|
103
|
+
session_count: number;
|
|
104
|
+
pending_session_count: number;
|
|
105
|
+
next_session_id: string | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type SessionIndexEntryWithReview = SessionIndexEntry & CandidateReviewCounts & {
|
|
109
|
+
candidate_review: SessionCandidateReviewSummary;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function sessionPayload(session: SessionMetadata): SessionPayload {
|
|
113
|
+
return {
|
|
114
|
+
...session,
|
|
115
|
+
session_dir: sessionRelativeDir(session.id),
|
|
116
|
+
notes_path: sessionRelativeFile(session.id, NOTES_FILE),
|
|
117
|
+
recap_path: sessionRelativeFile(session.id, RECAP_FILE),
|
|
118
|
+
candidate_traps_path: sessionRelativeFile(session.id, CANDIDATES_FILE),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function sessionConflictPayload(result: {
|
|
123
|
+
session_id: string;
|
|
124
|
+
candidate_id: string;
|
|
125
|
+
possible_conflicts: CandidateConflict[];
|
|
126
|
+
}): SessionConflictPayload {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: "Possible active trap conflict found.",
|
|
130
|
+
session_id: result.session_id,
|
|
131
|
+
candidate_id: result.candidate_id,
|
|
132
|
+
possible_conflicts: result.possible_conflicts,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sessionCliConflictPayload(payload: SessionConflictPayload): SessionCliConflictPayload {
|
|
137
|
+
return {
|
|
138
|
+
...payload,
|
|
139
|
+
next_actions: [
|
|
140
|
+
`codetrap session accept ${payload.candidate_id} --session ${payload.session_id} --accept-anyway`,
|
|
141
|
+
`codetrap session accept ${payload.candidate_id} --session ${payload.session_id} --supersedes <trap-id>`,
|
|
142
|
+
`codetrap session reject ${payload.candidate_id} --session ${payload.session_id} --reason <reason>`,
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function sessionConflictText(payload: SessionConflictPayload): string {
|
|
148
|
+
return [
|
|
149
|
+
"Possible active trap conflict found:",
|
|
150
|
+
...payload.possible_conflicts.map((conflict) => [
|
|
151
|
+
`#${conflict.trap_id} ${conflict.title}`,
|
|
152
|
+
` reason: ${conflict.reason}`,
|
|
153
|
+
` fix: ${conflict.fix}`,
|
|
154
|
+
].join("\n")),
|
|
155
|
+
"",
|
|
156
|
+
"Use --accept-anyway to save as a new trap, or --supersedes <trap-id> to preserve lifecycle history.",
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function sessionAcceptPayload(accepted: {
|
|
161
|
+
session: SessionMetadata;
|
|
162
|
+
candidate: CandidateTrap;
|
|
163
|
+
trap_id: number;
|
|
164
|
+
scope: string;
|
|
165
|
+
evidence_id: number | null;
|
|
166
|
+
superseded_id: number | null;
|
|
167
|
+
}): SessionAcceptPayload {
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
session_id: accepted.session.id,
|
|
171
|
+
candidate_id: accepted.candidate.id,
|
|
172
|
+
status: accepted.candidate.status,
|
|
173
|
+
trap_id: accepted.trap_id,
|
|
174
|
+
scope: accepted.scope,
|
|
175
|
+
evidence_id: accepted.evidence_id,
|
|
176
|
+
superseded_id: accepted.superseded_id,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function sessionRejectPayload(rejected: {
|
|
181
|
+
session: SessionMetadata;
|
|
182
|
+
candidate: CandidateTrap;
|
|
183
|
+
}): SessionRejectPayload {
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
session_id: rejected.session.id,
|
|
187
|
+
candidate_id: rejected.candidate.id,
|
|
188
|
+
status: rejected.candidate.status,
|
|
189
|
+
reason: rejected.candidate.rejection_reason ?? null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function sessionCleanupPayload(result: {
|
|
194
|
+
session: SessionMetadata;
|
|
195
|
+
removed_count: number;
|
|
196
|
+
removed_candidate_ids: string[];
|
|
197
|
+
}): SessionCleanupPayload {
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
session_id: result.session.id,
|
|
201
|
+
removed_count: result.removed_count,
|
|
202
|
+
removed_candidate_ids: result.removed_candidate_ids,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function reviewedSessionCandidates(
|
|
207
|
+
candidates: CandidateTrap[],
|
|
208
|
+
traps: TrapOperations
|
|
209
|
+
): ReviewedSessionCandidate[] {
|
|
210
|
+
return candidates.map((candidate) => ({
|
|
211
|
+
...candidate,
|
|
212
|
+
review: sessionCandidateReview(candidate, traps),
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function candidateReviewCounts(candidates: CandidateTrap[]): CandidateReviewCounts {
|
|
217
|
+
const pending = candidates.filter((candidate) => candidate.status === "proposed");
|
|
218
|
+
const accepted = candidates.filter((candidate) => candidate.status === "accepted");
|
|
219
|
+
const rejected = candidates.filter((candidate) => candidate.status === "rejected");
|
|
220
|
+
return {
|
|
221
|
+
candidate_count: candidates.length,
|
|
222
|
+
pending_count: pending.length,
|
|
223
|
+
reviewed_count: accepted.length + rejected.length,
|
|
224
|
+
accepted_count: accepted.length,
|
|
225
|
+
rejected_count: rejected.length,
|
|
226
|
+
high_quality_pending_count: pending.filter((candidate) =>
|
|
227
|
+
candidate.quality_score >= 0.8 && candidate.quality.suggested_action === "accept"
|
|
228
|
+
).length,
|
|
229
|
+
needs_edit_count: pending.filter((candidate) => candidate.quality.suggested_action === "edit").length,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function sessionCandidateReviewSummary(
|
|
234
|
+
session: Pick<SessionIndexEntry, "id" | "goal" | "status">,
|
|
235
|
+
candidates: CandidateTrap[]
|
|
236
|
+
): SessionCandidateReviewSummary {
|
|
237
|
+
return {
|
|
238
|
+
session_id: session.id,
|
|
239
|
+
goal: session.goal,
|
|
240
|
+
status: session.status,
|
|
241
|
+
...candidateReviewCounts(candidates),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function sessionIndexEntryWithReview(
|
|
246
|
+
session: SessionIndexEntry,
|
|
247
|
+
candidates: CandidateTrap[]
|
|
248
|
+
): SessionIndexEntryWithReview {
|
|
249
|
+
const review = sessionCandidateReviewSummary(session, candidates);
|
|
250
|
+
return {
|
|
251
|
+
...session,
|
|
252
|
+
...review,
|
|
253
|
+
candidate_review: review,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function projectCandidateReviewSummary(
|
|
258
|
+
sessions: SessionCandidateReviewSummary[]
|
|
259
|
+
): ProjectCandidateReviewSummary {
|
|
260
|
+
const empty = candidateReviewCounts([]);
|
|
261
|
+
const totals = sessions.reduce<CandidateReviewCounts>((acc, session) => ({
|
|
262
|
+
candidate_count: acc.candidate_count + session.candidate_count,
|
|
263
|
+
pending_count: acc.pending_count + session.pending_count,
|
|
264
|
+
reviewed_count: acc.reviewed_count + session.reviewed_count,
|
|
265
|
+
accepted_count: acc.accepted_count + session.accepted_count,
|
|
266
|
+
rejected_count: acc.rejected_count + session.rejected_count,
|
|
267
|
+
high_quality_pending_count: acc.high_quality_pending_count + session.high_quality_pending_count,
|
|
268
|
+
needs_edit_count: acc.needs_edit_count + session.needs_edit_count,
|
|
269
|
+
}), empty);
|
|
270
|
+
const nextPending = sessions.find((session) => session.pending_count > 0);
|
|
271
|
+
return {
|
|
272
|
+
...totals,
|
|
273
|
+
session_count: sessions.length,
|
|
274
|
+
pending_session_count: sessions.filter((session) => session.pending_count > 0).length,
|
|
275
|
+
next_session_id: nextPending?.session_id ?? null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function sessionCandidateReview(
|
|
280
|
+
candidate: CandidateTrap,
|
|
281
|
+
traps: TrapOperations
|
|
282
|
+
): SessionCandidateReview {
|
|
283
|
+
if (candidate.status === "proposed") {
|
|
284
|
+
return { status: "pending", label: "pending review" };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (candidate.status === "rejected") {
|
|
288
|
+
return {
|
|
289
|
+
status: "rejected",
|
|
290
|
+
label: "rejected",
|
|
291
|
+
rejected_at: candidate.rejected_at,
|
|
292
|
+
rejection_reason: candidate.rejection_reason,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const trapId = candidate.accepted_trap_id;
|
|
297
|
+
const scope = candidateAcceptedScope(candidate);
|
|
298
|
+
if (trapId === undefined) {
|
|
299
|
+
return {
|
|
300
|
+
status: "accepted_missing",
|
|
301
|
+
label: "accepted -> trap link missing",
|
|
302
|
+
scope,
|
|
303
|
+
trap_present: false,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const details = traps.getTrapDetails(trapId, scope);
|
|
308
|
+
if (!details) {
|
|
309
|
+
return {
|
|
310
|
+
status: "accepted_missing",
|
|
311
|
+
label: `accepted -> trap #${trapId} deleted`,
|
|
312
|
+
trap_id: trapId,
|
|
313
|
+
scope,
|
|
314
|
+
trap_present: false,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
status: "accepted",
|
|
320
|
+
label: `accepted -> trap #${trapId}`,
|
|
321
|
+
trap_id: trapId,
|
|
322
|
+
scope: details.scope,
|
|
323
|
+
trap_present: true,
|
|
324
|
+
trap_status: details.trap.status,
|
|
325
|
+
trap_title: details.trap.title,
|
|
326
|
+
};
|
|
327
|
+
}
|