ralph-review 0.2.2 → 0.2.4
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 +123 -16
- package/package.json +7 -5
- package/src/cli-core.ts +51 -88
- package/src/cli-rrr.ts +1 -4
- package/src/cli.ts +1 -2
- package/src/commands/apply.ts +35 -20
- package/src/commands/config-handlers.ts +68 -69
- package/src/commands/config-model.ts +147 -125
- package/src/commands/doctor.ts +2 -4
- package/src/commands/fix.ts +73 -51
- package/src/commands/handoff-selection.ts +6 -8
- package/src/commands/interactive-deps.ts +43 -0
- package/src/commands/list.ts +24 -7
- package/src/commands/log.ts +12 -12
- package/src/commands/run.ts +32 -33
- package/src/commands/status.ts +25 -4
- package/src/commands/stop.ts +99 -62
- package/src/commands/update.ts +2 -4
- package/src/lib/agents/claude.ts +4 -16
- package/src/lib/agents/core.ts +16 -0
- package/src/lib/agents/droid.ts +4 -15
- package/src/lib/agents/models.ts +9 -0
- package/src/lib/cli-parser.ts +19 -14
- package/src/lib/handoff.ts +16 -7
- package/src/lib/logging/session-log.ts +2 -1
- package/src/lib/prompts/defaults/review.md +1 -1
- package/src/lib/prompts/protocol.ts +2 -1
- package/src/lib/review-workflow/findings/artifact.ts +3 -1
- package/src/lib/review-workflow/findings/types.ts +1 -1
- package/src/lib/review-workflow/remediation/prompt.ts +7 -7
- package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
- package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
- package/src/lib/review-workflow/results/finalize-result.ts +20 -3
- package/src/lib/review-workflow/run-review-cycle.ts +1 -12
- package/src/lib/review-workflow/session-status.ts +13 -0
- package/src/lib/review-workflow/shared/framed-json.ts +2 -47
- package/src/lib/session/state.ts +50 -38
- package/src/lib/structured-output.ts +24 -9
- package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
- package/src/lib/tui/dashboard/ReviewModeOverlay.tsx +2 -2
- package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
- package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
- package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
- package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
- package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
- package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
- package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
- package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
- package/src/lib/tui/shared/CenteredModal.tsx +44 -0
- package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
- package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
- package/src/lib/tui/workspace/Workspace.tsx +6 -91
- package/src/lib/tui/workspace/use-workspace-state.ts +113 -61
- package/src/lib/types/fix.ts +15 -48
- package/src/lib/types/guards.ts +47 -0
- package/src/lib/types/review.ts +5 -39
|
@@ -2,7 +2,7 @@ import { basename } from "node:path";
|
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { loadEffectiveConfig } from "@/lib/config";
|
|
4
4
|
import { ensureGitRepositoryAsync } from "@/lib/git";
|
|
5
|
-
import type { LogIncrementalState } from "@/lib/logger";
|
|
5
|
+
import type { LogIncrementalState, LogSession } from "@/lib/logger";
|
|
6
6
|
import {
|
|
7
7
|
computeProjectStats,
|
|
8
8
|
computeSessionStats,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
listProjectLogSessions,
|
|
12
12
|
readLogIncremental,
|
|
13
13
|
} from "@/lib/logger";
|
|
14
|
-
import type { ActiveSession } from "@/lib/session-state";
|
|
14
|
+
import type { ActiveSession, SessionState } from "@/lib/session-state";
|
|
15
15
|
import {
|
|
16
16
|
getLatestProjectActiveSession,
|
|
17
17
|
listAllActiveSessions,
|
|
@@ -35,7 +35,91 @@ import {
|
|
|
35
35
|
import type { SessionGroupData, WorkspaceState } from "./workspace-types";
|
|
36
36
|
|
|
37
37
|
const DEFAULT_REFRESH_INTERVAL = 1000;
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
export interface WorkspaceStateDeps {
|
|
40
|
+
loadEffectiveConfig: typeof loadEffectiveConfig;
|
|
41
|
+
ensureGitRepositoryAsync: typeof ensureGitRepositoryAsync;
|
|
42
|
+
listAllActiveSessions: typeof listAllActiveSessions;
|
|
43
|
+
listProjectActiveSessions: typeof listProjectActiveSessions;
|
|
44
|
+
getLatestProjectActiveSession: (
|
|
45
|
+
storageRoot: string | undefined,
|
|
46
|
+
projectPath: string
|
|
47
|
+
) => Promise<SessionState | null>;
|
|
48
|
+
getLatestProjectLogSession: (
|
|
49
|
+
storageRoot: string | undefined,
|
|
50
|
+
projectPath: string
|
|
51
|
+
) => Promise<LogSession | null>;
|
|
52
|
+
readLogIncremental: typeof readLogIncremental;
|
|
53
|
+
listProjectLogSessions: typeof listProjectLogSessions;
|
|
54
|
+
computeSessionStats: typeof computeSessionStats;
|
|
55
|
+
computeProjectStats: typeof computeProjectStats;
|
|
56
|
+
getProjectName: typeof getProjectName;
|
|
57
|
+
shouldCaptureTmux: typeof shouldCaptureTmux;
|
|
58
|
+
getSessionOutput: (sessionName: string, lines: number) => Promise<string>;
|
|
59
|
+
computeNextTmuxCaptureInterval: typeof computeNextTmuxCaptureInterval;
|
|
60
|
+
tmuxCaptureMinIntervalMs: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const defaultWorkspaceStateDeps: WorkspaceStateDeps = {
|
|
64
|
+
loadEffectiveConfig,
|
|
65
|
+
ensureGitRepositoryAsync,
|
|
66
|
+
listAllActiveSessions,
|
|
67
|
+
listProjectActiveSessions,
|
|
68
|
+
getLatestProjectActiveSession,
|
|
69
|
+
getLatestProjectLogSession,
|
|
70
|
+
readLogIncremental,
|
|
71
|
+
listProjectLogSessions,
|
|
72
|
+
computeSessionStats,
|
|
73
|
+
computeProjectStats,
|
|
74
|
+
getProjectName,
|
|
75
|
+
shouldCaptureTmux,
|
|
76
|
+
getSessionOutput,
|
|
77
|
+
computeNextTmuxCaptureInterval,
|
|
78
|
+
tmuxCaptureMinIntervalMs: TMUX_CAPTURE_MIN_INTERVAL_MS,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function createInitialWorkspaceState(
|
|
82
|
+
overrides: Partial<WorkspaceState> = {}
|
|
83
|
+
): WorkspaceState {
|
|
84
|
+
return {
|
|
85
|
+
sessionGroups: [],
|
|
86
|
+
allSessions: [],
|
|
87
|
+
projectSessions: [],
|
|
88
|
+
selectedSessionId: null,
|
|
89
|
+
currentSession: null,
|
|
90
|
+
logEntries: [],
|
|
91
|
+
fixes: [],
|
|
92
|
+
skipped: [],
|
|
93
|
+
findings: [],
|
|
94
|
+
storedFindings: [],
|
|
95
|
+
selectedFindingIds: [],
|
|
96
|
+
selectedFindings: [],
|
|
97
|
+
unselectedFindings: [],
|
|
98
|
+
fixResults: [],
|
|
99
|
+
unresolvedSelectedFindings: [],
|
|
100
|
+
auditRegressionFindings: [],
|
|
101
|
+
iterationFixes: [],
|
|
102
|
+
iterationSkipped: [],
|
|
103
|
+
iterationFindings: [],
|
|
104
|
+
latestReviewIteration: null,
|
|
105
|
+
codexReviewText: null,
|
|
106
|
+
tmuxOutput: "",
|
|
107
|
+
elapsed: 0,
|
|
108
|
+
maxIterations: 0,
|
|
109
|
+
error: null,
|
|
110
|
+
liveRefreshError: null,
|
|
111
|
+
isLoading: true,
|
|
112
|
+
lastSessionStats: null,
|
|
113
|
+
projectStats: null,
|
|
114
|
+
config: null,
|
|
115
|
+
configWarning: null,
|
|
116
|
+
isGitRepo: true,
|
|
117
|
+
currentAgent: null,
|
|
118
|
+
reviewOptions: undefined,
|
|
119
|
+
outputVisible: false,
|
|
120
|
+
...overrides,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
39
123
|
|
|
40
124
|
function buildSessionGroups(
|
|
41
125
|
allSessions: ActiveSession[],
|
|
@@ -78,45 +162,10 @@ function buildSessionGroups(
|
|
|
78
162
|
export function useWorkspaceState(
|
|
79
163
|
projectPath: string,
|
|
80
164
|
_branch?: string,
|
|
81
|
-
refreshInterval: number = DEFAULT_REFRESH_INTERVAL
|
|
165
|
+
refreshInterval: number = DEFAULT_REFRESH_INTERVAL,
|
|
166
|
+
deps: WorkspaceStateDeps = defaultWorkspaceStateDeps
|
|
82
167
|
): WorkspaceState {
|
|
83
|
-
const [state, setState] = useState<WorkspaceState>(
|
|
84
|
-
sessionGroups: [],
|
|
85
|
-
allSessions: [],
|
|
86
|
-
projectSessions: [],
|
|
87
|
-
selectedSessionId: null,
|
|
88
|
-
currentSession: null,
|
|
89
|
-
logEntries: [],
|
|
90
|
-
fixes: [],
|
|
91
|
-
skipped: [],
|
|
92
|
-
findings: [],
|
|
93
|
-
storedFindings: [],
|
|
94
|
-
selectedFindingIds: [],
|
|
95
|
-
selectedFindings: [],
|
|
96
|
-
unselectedFindings: [],
|
|
97
|
-
fixResults: [],
|
|
98
|
-
unresolvedSelectedFindings: [],
|
|
99
|
-
auditRegressionFindings: [],
|
|
100
|
-
iterationFixes: [],
|
|
101
|
-
iterationSkipped: [],
|
|
102
|
-
iterationFindings: [],
|
|
103
|
-
latestReviewIteration: null,
|
|
104
|
-
codexReviewText: null,
|
|
105
|
-
tmuxOutput: "",
|
|
106
|
-
elapsed: 0,
|
|
107
|
-
maxIterations: 0,
|
|
108
|
-
error: null,
|
|
109
|
-
liveRefreshError: null,
|
|
110
|
-
isLoading: true,
|
|
111
|
-
lastSessionStats: null,
|
|
112
|
-
projectStats: null,
|
|
113
|
-
config: null,
|
|
114
|
-
configWarning: null,
|
|
115
|
-
isGitRepo: true,
|
|
116
|
-
currentAgent: null,
|
|
117
|
-
reviewOptions: undefined,
|
|
118
|
-
outputVisible: false,
|
|
119
|
-
});
|
|
168
|
+
const [state, setState] = useState<WorkspaceState>(() => createInitialWorkspaceState());
|
|
120
169
|
|
|
121
170
|
const stateRef = useRef(state);
|
|
122
171
|
stateRef.current = state;
|
|
@@ -126,7 +175,7 @@ export function useWorkspaceState(
|
|
|
126
175
|
const lastTmuxCaptureRef = useRef(0);
|
|
127
176
|
const lastTmuxOutputRef = useRef("");
|
|
128
177
|
const lastTmuxSessionRef = useRef<string | null>(null);
|
|
129
|
-
const tmuxCaptureIntervalRef = useRef(
|
|
178
|
+
const tmuxCaptureIntervalRef = useRef(deps.tmuxCaptureMinIntervalMs);
|
|
130
179
|
const lastLiveMetaRef = useRef<LiveRefreshMeta | null>(null);
|
|
131
180
|
const logIncrementalStateRef = useRef<LogIncrementalState | undefined>(undefined);
|
|
132
181
|
const lastLogSessionPathRef = useRef<string | null>(null);
|
|
@@ -138,12 +187,12 @@ export function useWorkspaceState(
|
|
|
138
187
|
try {
|
|
139
188
|
const [isGitRepo, allSessions, projectSessions, currentSession, logSession, configResult] =
|
|
140
189
|
await Promise.all([
|
|
141
|
-
ensureGitRepositoryAsync(projectPath),
|
|
142
|
-
listAllActiveSessions(),
|
|
143
|
-
listProjectActiveSessions(undefined, projectPath),
|
|
144
|
-
getLatestProjectActiveSession(undefined, projectPath),
|
|
145
|
-
getLatestProjectLogSession(undefined, projectPath),
|
|
146
|
-
loadWorkspaceConfigSafe(projectPath, loadEffectiveConfig),
|
|
190
|
+
deps.ensureGitRepositoryAsync(projectPath),
|
|
191
|
+
deps.listAllActiveSessions(),
|
|
192
|
+
deps.listProjectActiveSessions(undefined, projectPath),
|
|
193
|
+
deps.getLatestProjectActiveSession(undefined, projectPath),
|
|
194
|
+
deps.getLatestProjectLogSession(undefined, projectPath),
|
|
195
|
+
loadWorkspaceConfigSafe(projectPath, deps.loadEffectiveConfig),
|
|
147
196
|
]);
|
|
148
197
|
|
|
149
198
|
const sessionGroups = buildSessionGroups(allSessions, projectPath);
|
|
@@ -165,7 +214,7 @@ export function useWorkspaceState(
|
|
|
165
214
|
|
|
166
215
|
if (logPath) {
|
|
167
216
|
const logSessionChanged = logPath !== lastLogSessionPathRef.current;
|
|
168
|
-
const incrementalResult = await readLogIncremental(
|
|
217
|
+
const incrementalResult = await deps.readLogIncremental(
|
|
169
218
|
logPath,
|
|
170
219
|
logSessionChanged ? undefined : logIncrementalStateRef.current
|
|
171
220
|
);
|
|
@@ -202,11 +251,14 @@ export function useWorkspaceState(
|
|
|
202
251
|
let projectStats: ProjectStats | null = null;
|
|
203
252
|
|
|
204
253
|
if (!currentSession) {
|
|
205
|
-
const projectLogSessions = await listProjectLogSessions(undefined, projectPath);
|
|
254
|
+
const projectLogSessions = await deps.listProjectLogSessions(undefined, projectPath);
|
|
206
255
|
const latestSession = projectLogSessions[0];
|
|
207
256
|
if (latestSession) {
|
|
208
|
-
lastSessionStats = await computeSessionStats(latestSession);
|
|
209
|
-
projectStats = await computeProjectStats(
|
|
257
|
+
lastSessionStats = await deps.computeSessionStats(latestSession);
|
|
258
|
+
projectStats = await deps.computeProjectStats(
|
|
259
|
+
deps.getProjectName(projectPath),
|
|
260
|
+
projectLogSessions
|
|
261
|
+
);
|
|
210
262
|
}
|
|
211
263
|
}
|
|
212
264
|
|
|
@@ -254,14 +306,14 @@ export function useWorkspaceState(
|
|
|
254
306
|
} finally {
|
|
255
307
|
isHeavyRefreshingRef.current = false;
|
|
256
308
|
}
|
|
257
|
-
}, [projectPath]);
|
|
309
|
+
}, [deps, projectPath]);
|
|
258
310
|
|
|
259
311
|
const refreshLive = useCallback(async () => {
|
|
260
312
|
if (isLiveRefreshingRef.current) return;
|
|
261
313
|
isLiveRefreshingRef.current = true;
|
|
262
314
|
|
|
263
315
|
try {
|
|
264
|
-
const currentSession = await getLatestProjectActiveSession(undefined, projectPath);
|
|
316
|
+
const currentSession = await deps.getLatestProjectActiveSession(undefined, projectPath);
|
|
265
317
|
|
|
266
318
|
let tmuxOutput = lastTmuxOutputRef.current;
|
|
267
319
|
const liveMeta = getLiveRefreshMeta(currentSession);
|
|
@@ -274,10 +326,10 @@ export function useWorkspaceState(
|
|
|
274
326
|
lastTmuxOutputRef.current = "";
|
|
275
327
|
lastTmuxSessionRef.current = null;
|
|
276
328
|
lastTmuxCaptureRef.current = 0;
|
|
277
|
-
tmuxCaptureIntervalRef.current =
|
|
329
|
+
tmuxCaptureIntervalRef.current = deps.tmuxCaptureMinIntervalMs;
|
|
278
330
|
} else {
|
|
279
331
|
const sessionChanged = sessionName !== lastTmuxSessionRef.current;
|
|
280
|
-
const shouldCapture = shouldCaptureTmux({
|
|
332
|
+
const shouldCapture = deps.shouldCaptureTmux({
|
|
281
333
|
sessionChanged,
|
|
282
334
|
liveMetaChanged,
|
|
283
335
|
now,
|
|
@@ -286,14 +338,14 @@ export function useWorkspaceState(
|
|
|
286
338
|
});
|
|
287
339
|
|
|
288
340
|
if (shouldCapture) {
|
|
289
|
-
const capturedOutput = await getSessionOutput(sessionName, 1000);
|
|
341
|
+
const capturedOutput = await deps.getSessionOutput(sessionName, 1000);
|
|
290
342
|
const nextOutput = capturedOutput || (sessionChanged ? "" : lastTmuxOutputRef.current);
|
|
291
343
|
const outputChanged = nextOutput !== lastTmuxOutputRef.current;
|
|
292
344
|
tmuxOutput = nextOutput;
|
|
293
345
|
lastTmuxOutputRef.current = tmuxOutput;
|
|
294
346
|
lastTmuxSessionRef.current = sessionName;
|
|
295
347
|
lastTmuxCaptureRef.current = now;
|
|
296
|
-
tmuxCaptureIntervalRef.current = computeNextTmuxCaptureInterval({
|
|
348
|
+
tmuxCaptureIntervalRef.current = deps.computeNextTmuxCaptureInterval({
|
|
297
349
|
sessionChanged,
|
|
298
350
|
liveMetaChanged,
|
|
299
351
|
outputChanged,
|
|
@@ -322,7 +374,7 @@ export function useWorkspaceState(
|
|
|
322
374
|
} finally {
|
|
323
375
|
isLiveRefreshingRef.current = false;
|
|
324
376
|
}
|
|
325
|
-
}, [projectPath]);
|
|
377
|
+
}, [deps, projectPath]);
|
|
326
378
|
|
|
327
379
|
useEffect(() => {
|
|
328
380
|
void refreshHeavy();
|
|
@@ -338,9 +390,9 @@ export function useWorkspaceState(
|
|
|
338
390
|
}, [refreshLive]);
|
|
339
391
|
|
|
340
392
|
useEffect(() => {
|
|
341
|
-
const interval = setInterval(refreshLive,
|
|
393
|
+
const interval = setInterval(refreshLive, deps.tmuxCaptureMinIntervalMs);
|
|
342
394
|
return () => clearInterval(interval);
|
|
343
|
-
}, [refreshLive]);
|
|
395
|
+
}, [deps.tmuxCaptureMinIntervalMs, refreshLive]);
|
|
344
396
|
|
|
345
397
|
return state;
|
|
346
398
|
}
|
package/src/lib/types/fix.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FixDecision, Priority } from "./domain";
|
|
2
2
|
import { VALID_FIX_DECISIONS, VALID_PRIORITIES } from "./domain";
|
|
3
|
+
import { asRecord, isCodeLocation } from "./guards";
|
|
3
4
|
import type { CodeLocation } from "./review";
|
|
4
5
|
|
|
5
6
|
export interface FixEntry {
|
|
@@ -26,53 +27,27 @@ export interface FixSummary {
|
|
|
26
27
|
skipped: SkippedEntry[];
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function
|
|
30
|
-
if (typeof value !== "object" || value === null) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const obj = value as Record<string, unknown>;
|
|
35
|
-
|
|
30
|
+
function hasFixEntryHeader(obj: Record<string, unknown>): boolean {
|
|
36
31
|
return (
|
|
37
|
-
typeof obj.
|
|
38
|
-
|
|
39
|
-
obj.
|
|
40
|
-
|
|
41
|
-
Number.isInteger(obj.end) &&
|
|
42
|
-
obj.end >= obj.start
|
|
32
|
+
typeof obj.id === "number" &&
|
|
33
|
+
typeof obj.title === "string" &&
|
|
34
|
+
typeof obj.priority === "string" &&
|
|
35
|
+
VALID_PRIORITIES.includes(obj.priority as Priority)
|
|
43
36
|
);
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
function isCodeLocation(value: unknown): value is CodeLocation {
|
|
47
|
-
if (typeof value !== "object" || value === null) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const obj = value as Record<string, unknown>;
|
|
52
|
-
|
|
53
|
-
if (typeof obj.absolute_file_path !== "string") {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return isLineRange(obj.line_range);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
39
|
function isFixEntry(value: unknown): value is FixEntry {
|
|
61
|
-
|
|
40
|
+
const obj = asRecord(value);
|
|
41
|
+
if (obj === null) {
|
|
62
42
|
return false;
|
|
63
43
|
}
|
|
64
44
|
|
|
65
|
-
const obj = value as Record<string, unknown>;
|
|
66
|
-
|
|
67
45
|
return (
|
|
68
|
-
|
|
69
|
-
typeof obj.title === "string" &&
|
|
70
|
-
typeof obj.priority === "string" &&
|
|
71
|
-
VALID_PRIORITIES.includes(obj.priority as Priority) &&
|
|
46
|
+
hasFixEntryHeader(obj) &&
|
|
72
47
|
(obj.file === undefined || obj.file === null || typeof obj.file === "string") &&
|
|
73
48
|
(obj.code_location === undefined ||
|
|
74
49
|
obj.code_location === null ||
|
|
75
|
-
isCodeLocation(obj.code_location)) &&
|
|
50
|
+
isCodeLocation(obj.code_location, { requirePositive: true })) &&
|
|
76
51
|
typeof obj.claim === "string" &&
|
|
77
52
|
typeof obj.evidence === "string" &&
|
|
78
53
|
typeof obj.fix === "string"
|
|
@@ -80,28 +55,20 @@ function isFixEntry(value: unknown): value is FixEntry {
|
|
|
80
55
|
}
|
|
81
56
|
|
|
82
57
|
function isSkippedEntry(value: unknown): value is SkippedEntry {
|
|
83
|
-
|
|
58
|
+
const obj = asRecord(value);
|
|
59
|
+
if (obj === null) {
|
|
84
60
|
return false;
|
|
85
61
|
}
|
|
86
62
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
typeof obj.id === "number" &&
|
|
91
|
-
typeof obj.title === "string" &&
|
|
92
|
-
typeof obj.priority === "string" &&
|
|
93
|
-
VALID_PRIORITIES.includes(obj.priority as Priority) &&
|
|
94
|
-
typeof obj.reason === "string"
|
|
95
|
-
);
|
|
63
|
+
return hasFixEntryHeader(obj) && typeof obj.reason === "string";
|
|
96
64
|
}
|
|
97
65
|
|
|
98
66
|
export function isFixSummary(value: unknown): value is FixSummary {
|
|
99
|
-
|
|
67
|
+
const obj = asRecord(value);
|
|
68
|
+
if (obj === null) {
|
|
100
69
|
return false;
|
|
101
70
|
}
|
|
102
71
|
|
|
103
|
-
const obj = value as Record<string, unknown>;
|
|
104
|
-
|
|
105
72
|
// Check decision field
|
|
106
73
|
if (
|
|
107
74
|
typeof obj.decision !== "string" ||
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
2
|
+
if (typeof value !== "object" || value === null) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return value as Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isInteger(value: unknown): value is number {
|
|
10
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function asRecordWithStringField(value: unknown, field: string): Record<string, unknown> | null {
|
|
14
|
+
const obj = asRecord(value);
|
|
15
|
+
if (obj === null || typeof obj[field] !== "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLineRange(
|
|
23
|
+
value: unknown,
|
|
24
|
+
options: { requirePositive?: boolean } = {}
|
|
25
|
+
): value is { start: number; end: number } {
|
|
26
|
+
const obj = asRecord(value);
|
|
27
|
+
if (obj === null || !isInteger(obj.start) || !isInteger(obj.end) || obj.end < obj.start) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.requirePositive && (obj.start <= 0 || obj.end <= 0)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isCodeLocation(
|
|
39
|
+
value: unknown,
|
|
40
|
+
options: { requirePositive?: boolean } = {}
|
|
41
|
+
): value is {
|
|
42
|
+
absolute_file_path: string;
|
|
43
|
+
line_range: { start: number; end: number };
|
|
44
|
+
} {
|
|
45
|
+
const obj = asRecordWithStringField(value, "absolute_file_path");
|
|
46
|
+
return obj !== null && isLineRange(obj.line_range, options);
|
|
47
|
+
}
|
package/src/lib/types/review.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OverallCorrectness } from "./domain";
|
|
2
2
|
import { VALID_OVERALL_CORRECTNESS } from "./domain";
|
|
3
|
+
import { asRecord, isCodeLocation } from "./guards";
|
|
3
4
|
|
|
4
5
|
export interface LineRange {
|
|
5
6
|
start: number;
|
|
@@ -32,46 +33,12 @@ export interface CodexReviewSummary {
|
|
|
32
33
|
|
|
33
34
|
const DEFAULT_CODEX_CONFIDENCE = 0.69;
|
|
34
35
|
|
|
35
|
-
function isLineRange(value: unknown): value is LineRange {
|
|
36
|
-
if (typeof value !== "object" || value === null) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const obj = value as Record<string, unknown>;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
typeof obj.start === "number" &&
|
|
44
|
-
Number.isInteger(obj.start) &&
|
|
45
|
-
typeof obj.end === "number" &&
|
|
46
|
-
Number.isInteger(obj.end)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isCodeLocation(value: unknown): value is CodeLocation {
|
|
51
|
-
if (typeof value !== "object" || value === null) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const obj = value as Record<string, unknown>;
|
|
56
|
-
|
|
57
|
-
if (typeof obj.absolute_file_path !== "string") {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!isLineRange(obj.line_range)) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
36
|
function isFinding(value: unknown): value is Finding {
|
|
69
|
-
|
|
37
|
+
const obj = asRecord(value);
|
|
38
|
+
if (obj === null) {
|
|
70
39
|
return false;
|
|
71
40
|
}
|
|
72
41
|
|
|
73
|
-
const obj = value as Record<string, unknown>;
|
|
74
|
-
|
|
75
42
|
if (typeof obj.title !== "string" || typeof obj.body !== "string") {
|
|
76
43
|
return false;
|
|
77
44
|
}
|
|
@@ -99,12 +66,11 @@ function isFinding(value: unknown): value is Finding {
|
|
|
99
66
|
}
|
|
100
67
|
|
|
101
68
|
export function isReviewSummary(value: unknown): value is ReviewSummary {
|
|
102
|
-
|
|
69
|
+
const obj = asRecord(value);
|
|
70
|
+
if (obj === null) {
|
|
103
71
|
return false;
|
|
104
72
|
}
|
|
105
73
|
|
|
106
|
-
const obj = value as Record<string, unknown>;
|
|
107
|
-
|
|
108
74
|
if (!Array.isArray(obj.findings)) {
|
|
109
75
|
return false;
|
|
110
76
|
}
|