ralph-review 0.2.2 → 0.2.3
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 +6 -4
- 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 +6 -14
- 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 +18 -0
- package/src/commands/log.ts +12 -12
- package/src/commands/run.ts +32 -33
- package/src/commands/stop.ts +6 -13
- 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/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/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 +44 -37
- package/src/lib/types/fix.ts +15 -48
- package/src/lib/types/guards.ts +47 -0
- package/src/lib/types/review.ts +5 -39
|
@@ -1,72 +1,6 @@
|
|
|
1
|
+
import { extractBalancedJsonObjectSlices } from "@/lib/structured-output";
|
|
1
2
|
import { isReviewSummary, type ReviewSummary } from "@/lib/types";
|
|
2
3
|
|
|
3
|
-
interface JsonObjectSlice {
|
|
4
|
-
start: number;
|
|
5
|
-
end: number;
|
|
6
|
-
value: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function extractBalancedJsonObjects(text: string): JsonObjectSlice[] {
|
|
10
|
-
const results: JsonObjectSlice[] = [];
|
|
11
|
-
let depth = 0;
|
|
12
|
-
let startIndex = -1;
|
|
13
|
-
let inString = false;
|
|
14
|
-
let isEscaped = false;
|
|
15
|
-
|
|
16
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
17
|
-
const char = text[index];
|
|
18
|
-
|
|
19
|
-
if (inString) {
|
|
20
|
-
if (isEscaped) {
|
|
21
|
-
isEscaped = false;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (char === "\\") {
|
|
26
|
-
isEscaped = true;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (char === '"') {
|
|
31
|
-
inString = false;
|
|
32
|
-
}
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (char === '"') {
|
|
37
|
-
inString = true;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (char === "{") {
|
|
42
|
-
if (depth === 0) {
|
|
43
|
-
startIndex = index;
|
|
44
|
-
}
|
|
45
|
-
depth += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (char === "}") {
|
|
50
|
-
if (depth === 0) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
depth -= 1;
|
|
55
|
-
if (depth === 0 && startIndex >= 0) {
|
|
56
|
-
const endIndex = index + 1;
|
|
57
|
-
results.push({
|
|
58
|
-
start: startIndex,
|
|
59
|
-
end: endIndex,
|
|
60
|
-
value: text.slice(startIndex, endIndex),
|
|
61
|
-
});
|
|
62
|
-
startIndex = -1;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return results;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
4
|
export function extractLatestReviewSummary(
|
|
71
5
|
text: string,
|
|
72
6
|
minIndex: number = 0
|
|
@@ -75,7 +9,7 @@ export function extractLatestReviewSummary(
|
|
|
75
9
|
return null;
|
|
76
10
|
}
|
|
77
11
|
|
|
78
|
-
const objects =
|
|
12
|
+
const objects = extractBalancedJsonObjectSlices(text);
|
|
79
13
|
for (let index = objects.length - 1; index >= 0; index -= 1) {
|
|
80
14
|
const candidate = objects[index];
|
|
81
15
|
if (!candidate) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface CenteredModalProps {
|
|
4
|
+
title: string;
|
|
5
|
+
width: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
padding?: number;
|
|
8
|
+
backgroundColor?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CenteredModal({
|
|
13
|
+
title,
|
|
14
|
+
width,
|
|
15
|
+
height,
|
|
16
|
+
padding = 2,
|
|
17
|
+
backgroundColor = "#1a1a2e",
|
|
18
|
+
children,
|
|
19
|
+
}: CenteredModalProps) {
|
|
20
|
+
return (
|
|
21
|
+
<box
|
|
22
|
+
position="absolute"
|
|
23
|
+
left={0}
|
|
24
|
+
top={0}
|
|
25
|
+
width="100%"
|
|
26
|
+
height="100%"
|
|
27
|
+
justifyContent="center"
|
|
28
|
+
alignItems="center"
|
|
29
|
+
>
|
|
30
|
+
<box
|
|
31
|
+
border
|
|
32
|
+
borderStyle="double"
|
|
33
|
+
title={title}
|
|
34
|
+
titleAlignment="left"
|
|
35
|
+
padding={padding}
|
|
36
|
+
width={width}
|
|
37
|
+
height={height}
|
|
38
|
+
backgroundColor={backgroundColor}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</box>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CenteredModal } from "@/lib/tui/shared/CenteredModal";
|
|
2
|
+
import { ShortcutHintList } from "@/lib/tui/shared/ShortcutHint";
|
|
3
|
+
|
|
4
|
+
interface KeyboardShortcutsModalProps {
|
|
5
|
+
shortcuts: readonly { keys: string; label: string }[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function KeyboardShortcutsModal({ shortcuts }: KeyboardShortcutsModalProps) {
|
|
9
|
+
return (
|
|
10
|
+
<CenteredModal title="Keyboard Shortcuts" width={44}>
|
|
11
|
+
<ShortcutHintList shortcuts={shortcuts} />
|
|
12
|
+
</CenteredModal>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TUI_COLORS } from "@/lib/tui/shared/colors";
|
|
2
|
+
|
|
3
|
+
interface ShortcutHintProps {
|
|
4
|
+
keys: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ShortcutHintListProps {
|
|
9
|
+
shortcuts: readonly ShortcutHintProps[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ShortcutHint({ keys, label }: ShortcutHintProps) {
|
|
13
|
+
return (
|
|
14
|
+
<text>
|
|
15
|
+
<span fg={TUI_COLORS.accent.key}>{keys}</span>
|
|
16
|
+
<span fg={TUI_COLORS.text.muted}> {label}</span>
|
|
17
|
+
</text>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ShortcutHintList({ shortcuts }: ShortcutHintListProps) {
|
|
22
|
+
return (
|
|
23
|
+
<box flexDirection="column" gap={1}>
|
|
24
|
+
{shortcuts.map((shortcut, index) => (
|
|
25
|
+
<ShortcutHint
|
|
26
|
+
key={`${shortcut.keys}-${index}`}
|
|
27
|
+
keys={shortcut.keys}
|
|
28
|
+
label={shortcut.label}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
</box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -1,51 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
FindingFixResult,
|
|
3
|
-
FindingId,
|
|
4
|
-
StoredFinding,
|
|
5
|
-
} from "@/lib/review-workflow/findings/types";
|
|
6
|
-
import type { SessionState } from "@/lib/session-state";
|
|
7
|
-
import type { DashboardStartupMode } from "@/lib/tui/dashboard/use-dashboard-run-control";
|
|
1
|
+
import type { DetailPaneProps } from "@/lib/tui/sessions/detail/DetailPane";
|
|
8
2
|
import { DetailPane } from "@/lib/tui/sessions/detail/DetailPane";
|
|
9
3
|
import { SessionSidebar } from "@/lib/tui/sessions/sidebar/SessionSidebar";
|
|
10
4
|
import { OutputDrawer } from "@/lib/tui/shared/OutputDrawer";
|
|
11
|
-
import type {
|
|
12
|
-
AgentRole,
|
|
13
|
-
Finding,
|
|
14
|
-
FixEntry,
|
|
15
|
-
ProjectStats,
|
|
16
|
-
ReviewOptions,
|
|
17
|
-
SessionStats,
|
|
18
|
-
SkippedEntry,
|
|
19
|
-
} from "@/lib/types";
|
|
20
5
|
import { resolveWorkspaceFocusState } from "./workspace-focus";
|
|
21
6
|
import type { FocusedPane, SessionGroupData } from "./workspace-types";
|
|
22
7
|
|
|
23
|
-
interface WorkspaceProps {
|
|
8
|
+
interface WorkspaceProps extends DetailPaneProps {
|
|
24
9
|
sessionGroups: SessionGroupData[];
|
|
25
10
|
selectedSessionId: string | null;
|
|
26
|
-
session: SessionState | null;
|
|
27
|
-
fixes: FixEntry[];
|
|
28
|
-
skipped: SkippedEntry[];
|
|
29
|
-
findings: Finding[];
|
|
30
|
-
storedFindings: StoredFinding[];
|
|
31
|
-
selectedFindingIds: FindingId[];
|
|
32
|
-
fixResults: FindingFixResult[];
|
|
33
|
-
unresolvedSelectedFindings: StoredFinding[];
|
|
34
|
-
auditRegressionFindings: StoredFinding[];
|
|
35
|
-
latestReviewIteration: number | null;
|
|
36
|
-
codexReviewText: string | null;
|
|
37
|
-
tmuxOutput: string;
|
|
38
|
-
maxIterations: number;
|
|
39
|
-
isLoading: boolean;
|
|
40
|
-
lastSessionStats: SessionStats | null;
|
|
41
|
-
projectStats: ProjectStats | null;
|
|
42
|
-
isGitRepo: boolean;
|
|
43
|
-
currentAgent: AgentRole | null;
|
|
44
|
-
reviewOptions: ReviewOptions | undefined;
|
|
45
|
-
startupMode: DashboardStartupMode;
|
|
46
|
-
isStopping: boolean;
|
|
47
|
-
activeSessionCount: number;
|
|
48
|
-
canFixPendingSession: boolean;
|
|
49
11
|
outputVisible: boolean;
|
|
50
12
|
focusedPane: FocusedPane;
|
|
51
13
|
overlayBlocked?: boolean;
|
|
@@ -54,32 +16,10 @@ interface WorkspaceProps {
|
|
|
54
16
|
export function Workspace({
|
|
55
17
|
sessionGroups,
|
|
56
18
|
selectedSessionId,
|
|
57
|
-
session,
|
|
58
|
-
fixes,
|
|
59
|
-
skipped,
|
|
60
|
-
findings,
|
|
61
|
-
storedFindings,
|
|
62
|
-
selectedFindingIds,
|
|
63
|
-
fixResults,
|
|
64
|
-
unresolvedSelectedFindings,
|
|
65
|
-
auditRegressionFindings,
|
|
66
|
-
latestReviewIteration,
|
|
67
|
-
codexReviewText,
|
|
68
|
-
tmuxOutput,
|
|
69
|
-
maxIterations,
|
|
70
|
-
isLoading,
|
|
71
|
-
lastSessionStats,
|
|
72
|
-
projectStats,
|
|
73
|
-
isGitRepo,
|
|
74
|
-
currentAgent,
|
|
75
|
-
reviewOptions,
|
|
76
|
-
startupMode,
|
|
77
|
-
isStopping,
|
|
78
|
-
activeSessionCount,
|
|
79
|
-
canFixPendingSession,
|
|
80
19
|
outputVisible,
|
|
81
20
|
focusedPane,
|
|
82
21
|
overlayBlocked = false,
|
|
22
|
+
...detailPaneProps
|
|
83
23
|
}: WorkspaceProps) {
|
|
84
24
|
const { sidebarFocused, detailFocused, outputFocused } = resolveWorkspaceFocusState(
|
|
85
25
|
focusedPane,
|
|
@@ -94,36 +34,11 @@ export function Workspace({
|
|
|
94
34
|
selectedSessionId={selectedSessionId}
|
|
95
35
|
focused={sidebarFocused}
|
|
96
36
|
/>
|
|
97
|
-
<DetailPane
|
|
98
|
-
session={session}
|
|
99
|
-
fixes={fixes}
|
|
100
|
-
skipped={skipped}
|
|
101
|
-
findings={findings}
|
|
102
|
-
storedFindings={storedFindings}
|
|
103
|
-
selectedFindingIds={selectedFindingIds}
|
|
104
|
-
fixResults={fixResults}
|
|
105
|
-
unresolvedSelectedFindings={unresolvedSelectedFindings}
|
|
106
|
-
auditRegressionFindings={auditRegressionFindings}
|
|
107
|
-
latestReviewIteration={latestReviewIteration}
|
|
108
|
-
codexReviewText={codexReviewText}
|
|
109
|
-
tmuxOutput={tmuxOutput}
|
|
110
|
-
maxIterations={maxIterations}
|
|
111
|
-
isLoading={isLoading}
|
|
112
|
-
lastSessionStats={lastSessionStats}
|
|
113
|
-
projectStats={projectStats}
|
|
114
|
-
isGitRepo={isGitRepo}
|
|
115
|
-
currentAgent={currentAgent}
|
|
116
|
-
reviewOptions={reviewOptions}
|
|
117
|
-
startupMode={startupMode}
|
|
118
|
-
isStopping={isStopping}
|
|
119
|
-
activeSessionCount={activeSessionCount}
|
|
120
|
-
canFixPendingSession={canFixPendingSession}
|
|
121
|
-
focused={detailFocused}
|
|
122
|
-
/>
|
|
37
|
+
<DetailPane {...detailPaneProps} focused={detailFocused} />
|
|
123
38
|
</box>
|
|
124
39
|
<OutputDrawer
|
|
125
|
-
output={tmuxOutput}
|
|
126
|
-
sessionName={session?.sessionName ?? null}
|
|
40
|
+
output={detailPaneProps.tmuxOutput}
|
|
41
|
+
sessionName={detailPaneProps.session?.sessionName ?? null}
|
|
127
42
|
visible={outputVisible}
|
|
128
43
|
focused={outputFocused}
|
|
129
44
|
/>
|
|
@@ -37,6 +37,49 @@ import type { SessionGroupData, WorkspaceState } from "./workspace-types";
|
|
|
37
37
|
const DEFAULT_REFRESH_INTERVAL = 1000;
|
|
38
38
|
const LIVE_REFRESH_INTERVAL = TMUX_CAPTURE_MIN_INTERVAL_MS;
|
|
39
39
|
|
|
40
|
+
export function createInitialWorkspaceState(
|
|
41
|
+
overrides: Partial<WorkspaceState> = {}
|
|
42
|
+
): WorkspaceState {
|
|
43
|
+
return {
|
|
44
|
+
sessionGroups: [],
|
|
45
|
+
allSessions: [],
|
|
46
|
+
projectSessions: [],
|
|
47
|
+
selectedSessionId: null,
|
|
48
|
+
currentSession: null,
|
|
49
|
+
logEntries: [],
|
|
50
|
+
fixes: [],
|
|
51
|
+
skipped: [],
|
|
52
|
+
findings: [],
|
|
53
|
+
storedFindings: [],
|
|
54
|
+
selectedFindingIds: [],
|
|
55
|
+
selectedFindings: [],
|
|
56
|
+
unselectedFindings: [],
|
|
57
|
+
fixResults: [],
|
|
58
|
+
unresolvedSelectedFindings: [],
|
|
59
|
+
auditRegressionFindings: [],
|
|
60
|
+
iterationFixes: [],
|
|
61
|
+
iterationSkipped: [],
|
|
62
|
+
iterationFindings: [],
|
|
63
|
+
latestReviewIteration: null,
|
|
64
|
+
codexReviewText: null,
|
|
65
|
+
tmuxOutput: "",
|
|
66
|
+
elapsed: 0,
|
|
67
|
+
maxIterations: 0,
|
|
68
|
+
error: null,
|
|
69
|
+
liveRefreshError: null,
|
|
70
|
+
isLoading: true,
|
|
71
|
+
lastSessionStats: null,
|
|
72
|
+
projectStats: null,
|
|
73
|
+
config: null,
|
|
74
|
+
configWarning: null,
|
|
75
|
+
isGitRepo: true,
|
|
76
|
+
currentAgent: null,
|
|
77
|
+
reviewOptions: undefined,
|
|
78
|
+
outputVisible: false,
|
|
79
|
+
...overrides,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
40
83
|
function buildSessionGroups(
|
|
41
84
|
allSessions: ActiveSession[],
|
|
42
85
|
currentProjectPath: string
|
|
@@ -80,43 +123,7 @@ export function useWorkspaceState(
|
|
|
80
123
|
_branch?: string,
|
|
81
124
|
refreshInterval: number = DEFAULT_REFRESH_INTERVAL
|
|
82
125
|
): 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
|
-
});
|
|
126
|
+
const [state, setState] = useState<WorkspaceState>(() => createInitialWorkspaceState());
|
|
120
127
|
|
|
121
128
|
const stateRef = useRef(state);
|
|
122
129
|
stateRef.current = state;
|
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
|
}
|