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
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import type { CandidateTrap } from "../domain/session";
|
|
2
|
-
import { buildTrapInput } from "../domain/trap";
|
|
3
2
|
import type { Scope } from "./constants";
|
|
4
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";
|
|
5
11
|
import { findCandidateConflicts, type CandidateConflict } from "./session-conflicts";
|
|
12
|
+
import {
|
|
13
|
+
projectCandidateReviewSummary,
|
|
14
|
+
sessionCandidateReviewSummary,
|
|
15
|
+
sessionIndexEntryWithReview,
|
|
16
|
+
type ProjectCandidateReviewSummary,
|
|
17
|
+
} from "./session-review";
|
|
6
18
|
import type {
|
|
7
19
|
AcceptCandidateResult,
|
|
8
20
|
AddSessionNoteArgs,
|
|
@@ -13,6 +25,7 @@ import type {
|
|
|
13
25
|
SessionStore,
|
|
14
26
|
StartSessionArgs,
|
|
15
27
|
} from "./session-store";
|
|
28
|
+
import { uniqueStrings } from "./string-list";
|
|
16
29
|
|
|
17
30
|
export type SessionAcceptRequest = {
|
|
18
31
|
candidateId: string;
|
|
@@ -40,6 +53,15 @@ export type SessionPruneRequest = {
|
|
|
40
53
|
now?: Date;
|
|
41
54
|
};
|
|
42
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
|
+
|
|
43
65
|
export type SessionConflictResult = {
|
|
44
66
|
success: false;
|
|
45
67
|
session_id: string;
|
|
@@ -53,6 +75,18 @@ export type SessionAcceptSuccess = AcceptCandidateResult & {
|
|
|
53
75
|
|
|
54
76
|
export type SessionAcceptResult = SessionAcceptSuccess | SessionConflictResult;
|
|
55
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
|
+
|
|
56
90
|
export class SessionOperations {
|
|
57
91
|
constructor(
|
|
58
92
|
private readonly sessions: SessionStore,
|
|
@@ -68,11 +102,23 @@ export class SessionOperations {
|
|
|
68
102
|
}
|
|
69
103
|
|
|
70
104
|
status() {
|
|
71
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
...this.sessions.status(),
|
|
107
|
+
candidate_review: this.candidateReviewSummary(),
|
|
108
|
+
};
|
|
72
109
|
}
|
|
73
110
|
|
|
74
111
|
listSessions(args: { status?: string; limit?: number } = {}) {
|
|
75
|
-
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);
|
|
76
122
|
}
|
|
77
123
|
|
|
78
124
|
showSession(id: string) {
|
|
@@ -87,6 +133,39 @@ export class SessionOperations {
|
|
|
87
133
|
return this.sessions.closeSession(id, proposeTraps);
|
|
88
134
|
}
|
|
89
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
|
+
|
|
90
169
|
candidateDocument(id?: string) {
|
|
91
170
|
return this.sessions.candidateDocument(id);
|
|
92
171
|
}
|
|
@@ -183,69 +262,13 @@ export class SessionOperations {
|
|
|
183
262
|
.filter((candidate) => {
|
|
184
263
|
const trapId = candidate.accepted_trap_id;
|
|
185
264
|
if (trapId === undefined) return true;
|
|
186
|
-
return !this.traps.getTrapDetails(trapId,
|
|
265
|
+
return !this.traps.getTrapDetails(trapId, candidateAcceptedScope(candidate));
|
|
187
266
|
})
|
|
188
267
|
.map((candidate) => candidate.id);
|
|
189
268
|
return this.sessions.removeCandidates(sessionId, missingCandidateIds);
|
|
190
269
|
}
|
|
191
270
|
}
|
|
192
271
|
|
|
193
|
-
function candidateWithTrapEdits(candidate: CandidateTrap, edit: Record<string, unknown> | undefined): CandidateTrap {
|
|
194
|
-
return {
|
|
195
|
-
...candidate,
|
|
196
|
-
trap: normalizeCandidateTrap({
|
|
197
|
-
...candidate.trap,
|
|
198
|
-
...trapEdits(edit),
|
|
199
|
-
}) as CandidateTrap["trap"],
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function normalizeCandidateTrap(args: Record<string, unknown>) {
|
|
204
|
-
return buildTrapInput({
|
|
205
|
-
...args,
|
|
206
|
-
tags: stringArray(args.tags),
|
|
207
|
-
path_globs: stringArray(args.path_globs),
|
|
208
|
-
module: optionalText(args.module),
|
|
209
|
-
owner: optionalText(args.owner),
|
|
210
|
-
before_code: optionalText(args.before_code),
|
|
211
|
-
after_code: optionalText(args.after_code),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function stringArray(value: unknown): string[] | undefined {
|
|
216
|
-
if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
|
|
217
|
-
if (typeof value === "string") {
|
|
218
|
-
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
219
|
-
return values.length > 0 ? values : undefined;
|
|
220
|
-
}
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function optionalText(value: unknown): string | null | undefined {
|
|
225
|
-
if (value === undefined) return undefined;
|
|
226
|
-
if (value === null) return null;
|
|
227
|
-
const text = String(value).trim();
|
|
228
|
-
return text ? text : null;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function trapEdits(edit: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
232
|
-
if (!edit) return {};
|
|
233
|
-
const nested = edit.trap;
|
|
234
|
-
return isRecord(nested) ? nested : edit;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
238
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
272
|
function candidateRelatedFiles(candidate: CandidateTrap): string[] {
|
|
242
273
|
return uniqueStrings(candidate.evidence.flatMap((evidence) => evidence.related_files ?? []));
|
|
243
274
|
}
|
|
244
|
-
|
|
245
|
-
function acceptedScope(candidate: CandidateTrap): Scope {
|
|
246
|
-
return candidate.accepted_scope ?? (candidate.trap.scope === "global" ? "global" : "project");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function uniqueStrings(values: string[]): string[] {
|
|
250
|
-
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
251
|
-
}
|
|
@@ -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
|
+
}
|