auto-claude: subtask-1-1 - Create XState v5 PR review state machine and state utils

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AndyMik90
2026-02-14 00:39:04 +01:00
parent d98ff7d19c
commit 45eebe960f
3 changed files with 238 additions and 0 deletions
@@ -7,3 +7,12 @@ export {
mapStateToLegacy,
} from './task-state-utils';
export type { TaskStateName } from './task-state-utils';
export { prReviewMachine } from './pr-review-machine';
export type { PRReviewContext, PRReviewEvent } from './pr-review-machine';
export {
PR_REVIEW_STATE_NAMES,
PR_REVIEW_SETTLED_STATES,
mapPRReviewStateToLegacy,
} from './pr-review-state-utils';
export type { PRReviewStateName } from './pr-review-state-utils';
@@ -0,0 +1,177 @@
import { assign, createMachine } from 'xstate';
import type { PRReviewProgress, PRReviewResult } from '../../preload/api/modules/github-api';
export interface PRReviewContext {
prNumber: number | null;
projectId: string | null;
startedAt: string | null;
isFollowup: boolean;
progress: PRReviewProgress | null;
result: PRReviewResult | null;
previousResult: PRReviewResult | null;
error: string | null;
isExternalReview: boolean;
}
export type PRReviewEvent =
| { type: 'START_REVIEW'; prNumber: number; projectId: string }
| { type: 'START_FOLLOWUP_REVIEW'; prNumber: number; projectId: string; previousResult: PRReviewResult }
| { type: 'SET_PROGRESS'; progress: PRReviewProgress }
| { type: 'REVIEW_COMPLETE'; result: PRReviewResult }
| { type: 'REVIEW_ERROR'; error: string }
| { type: 'CANCEL_REVIEW' }
| { type: 'DETECT_EXTERNAL_REVIEW' }
| { type: 'CLEAR_REVIEW' };
const initialContext: PRReviewContext = {
prNumber: null,
projectId: null,
startedAt: null,
isFollowup: false,
progress: null,
result: null,
previousResult: null,
error: null,
isExternalReview: false,
};
export const prReviewMachine = createMachine(
{
id: 'prReview',
initial: 'idle',
types: {} as {
context: PRReviewContext;
events: PRReviewEvent;
},
context: { ...initialContext },
states: {
idle: {
on: {
START_REVIEW: {
target: 'reviewing',
actions: 'setReviewStart',
},
START_FOLLOWUP_REVIEW: {
target: 'reviewing',
actions: 'setFollowupReviewStart',
},
},
},
reviewing: {
on: {
START_REVIEW: {
target: 'reviewing',
guard: 'isNotAlreadyReviewing',
},
SET_PROGRESS: {
actions: 'setProgress',
},
REVIEW_COMPLETE: {
target: 'completed',
actions: 'setResult',
},
REVIEW_ERROR: {
target: 'error',
actions: 'setError',
},
CANCEL_REVIEW: {
target: 'error',
actions: 'setCancelledError',
},
DETECT_EXTERNAL_REVIEW: {
target: 'externalReview',
actions: 'setExternalReview',
},
},
},
externalReview: {
on: {
REVIEW_COMPLETE: {
target: 'completed',
actions: 'setResult',
},
REVIEW_ERROR: {
target: 'error',
actions: 'setError',
},
},
},
completed: {
on: {
START_REVIEW: {
target: 'reviewing',
actions: 'setReviewStart',
},
START_FOLLOWUP_REVIEW: {
target: 'reviewing',
actions: 'setFollowupReviewStart',
},
CLEAR_REVIEW: {
target: 'idle',
actions: 'clearContext',
},
},
},
error: {
on: {
START_REVIEW: {
target: 'reviewing',
actions: 'setReviewStart',
},
CLEAR_REVIEW: {
target: 'idle',
actions: 'clearContext',
},
},
},
},
},
{
guards: {
isNotAlreadyReviewing: () => false,
},
actions: {
setReviewStart: assign({
prNumber: ({ event }) => (event as { prNumber: number }).prNumber,
projectId: ({ event }) => (event as { projectId: string }).projectId,
startedAt: () => new Date().toISOString(),
isFollowup: () => false,
progress: () => null,
result: () => null,
previousResult: () => null,
error: () => null,
isExternalReview: () => false,
}),
setFollowupReviewStart: assign({
prNumber: ({ event }) => (event as { prNumber: number }).prNumber,
projectId: ({ event }) => (event as { projectId: string }).projectId,
startedAt: () => new Date().toISOString(),
isFollowup: () => true,
progress: () => null,
result: () => null,
previousResult: ({ event }) => (event as { previousResult: PRReviewResult }).previousResult,
error: () => null,
isExternalReview: () => false,
}),
setProgress: assign({
progress: ({ event }) => (event as { progress: PRReviewProgress }).progress,
}),
setResult: assign({
result: ({ event }) => (event as { result: PRReviewResult }).result,
progress: () => null,
}),
setError: assign({
error: ({ event }) => (event as { error: string }).error,
progress: () => null,
}),
setCancelledError: assign({
error: () => 'Review cancelled by user',
progress: () => null,
}),
setExternalReview: assign({
isExternalReview: () => true,
}),
clearContext: assign(() => ({ ...initialContext })),
},
}
);
@@ -0,0 +1,52 @@
/**
* Shared XState PR review state utilities.
*
* Provides type-safe state names, settled states, and legacy status conversion
* derived from the PR review machine definition.
*/
/**
* All XState PR review state names.
*
* IMPORTANT: These must match the state keys in pr-review-machine.ts.
* If you add/remove a state in the machine, update this array.
*/
export const PR_REVIEW_STATE_NAMES = [
'idle', 'reviewing', 'externalReview', 'completed', 'error'
] as const;
export type PRReviewStateName = typeof PR_REVIEW_STATE_NAMES[number];
/**
* XState states where the PR review has "settled" — the review lifecycle
* has reached a terminal or resting state. Progress events should not
* overwrite these states.
*/
export const PR_REVIEW_SETTLED_STATES: ReadonlySet<string> = new Set<PRReviewStateName>([
'completed', 'error'
]);
/**
* Legacy review status values used by the existing Zustand store.
*/
type LegacyPRReviewStatus = 'idle' | 'reviewing' | 'completed' | 'error';
/**
* Convert XState PR review state to legacy status for backward compatibility.
*/
export function mapPRReviewStateToLegacy(state: string): LegacyPRReviewStatus {
switch (state) {
case 'idle':
return 'idle';
case 'reviewing':
return 'reviewing';
case 'externalReview':
return 'reviewing';
case 'completed':
return 'completed';
case 'error':
return 'error';
default:
return 'idle';
}
}