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.
Files changed (56) hide show
  1. package/README.md +123 -16
  2. package/package.json +7 -5
  3. package/src/cli-core.ts +51 -88
  4. package/src/cli-rrr.ts +1 -4
  5. package/src/cli.ts +1 -2
  6. package/src/commands/apply.ts +35 -20
  7. package/src/commands/config-handlers.ts +68 -69
  8. package/src/commands/config-model.ts +147 -125
  9. package/src/commands/doctor.ts +2 -4
  10. package/src/commands/fix.ts +73 -51
  11. package/src/commands/handoff-selection.ts +6 -8
  12. package/src/commands/interactive-deps.ts +43 -0
  13. package/src/commands/list.ts +24 -7
  14. package/src/commands/log.ts +12 -12
  15. package/src/commands/run.ts +32 -33
  16. package/src/commands/status.ts +25 -4
  17. package/src/commands/stop.ts +99 -62
  18. package/src/commands/update.ts +2 -4
  19. package/src/lib/agents/claude.ts +4 -16
  20. package/src/lib/agents/core.ts +16 -0
  21. package/src/lib/agents/droid.ts +4 -15
  22. package/src/lib/agents/models.ts +9 -0
  23. package/src/lib/cli-parser.ts +19 -14
  24. package/src/lib/handoff.ts +16 -7
  25. package/src/lib/logging/session-log.ts +2 -1
  26. package/src/lib/prompts/defaults/review.md +1 -1
  27. package/src/lib/prompts/protocol.ts +2 -1
  28. package/src/lib/review-workflow/findings/artifact.ts +3 -1
  29. package/src/lib/review-workflow/findings/types.ts +1 -1
  30. package/src/lib/review-workflow/remediation/prompt.ts +7 -7
  31. package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
  32. package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
  33. package/src/lib/review-workflow/results/finalize-result.ts +20 -3
  34. package/src/lib/review-workflow/run-review-cycle.ts +1 -12
  35. package/src/lib/review-workflow/session-status.ts +13 -0
  36. package/src/lib/review-workflow/shared/framed-json.ts +2 -47
  37. package/src/lib/session/state.ts +50 -38
  38. package/src/lib/structured-output.ts +24 -9
  39. package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
  40. package/src/lib/tui/dashboard/ReviewModeOverlay.tsx +2 -2
  41. package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
  42. package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
  43. package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
  44. package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
  45. package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
  46. package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
  47. package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
  48. package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
  49. package/src/lib/tui/shared/CenteredModal.tsx +44 -0
  50. package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
  51. package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
  52. package/src/lib/tui/workspace/Workspace.tsx +6 -91
  53. package/src/lib/tui/workspace/use-workspace-state.ts +113 -61
  54. package/src/lib/types/fix.ts +15 -48
  55. package/src/lib/types/guards.ts +47 -0
  56. 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
- const LIVE_REFRESH_INTERVAL = TMUX_CAPTURE_MIN_INTERVAL_MS;
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(TMUX_CAPTURE_MIN_INTERVAL_MS);
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(getProjectName(projectPath), projectLogSessions);
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 = TMUX_CAPTURE_MIN_INTERVAL_MS;
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, LIVE_REFRESH_INTERVAL);
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
  }
@@ -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 isLineRange(value: unknown): value is CodeLocation["line_range"] {
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.start === "number" &&
38
- Number.isInteger(obj.start) &&
39
- obj.start > 0 &&
40
- typeof obj.end === "number" &&
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
- if (typeof value !== "object" || value === null) {
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
- typeof obj.id === "number" &&
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
- if (typeof value !== "object" || value === null) {
58
+ const obj = asRecord(value);
59
+ if (obj === null) {
84
60
  return false;
85
61
  }
86
62
 
87
- const obj = value as Record<string, unknown>;
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
- if (typeof value !== "object" || value === null) {
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
+ }
@@ -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
- if (typeof value !== "object" || value === null) {
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
- if (typeof value !== "object" || value === null) {
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
  }