31 lines
2.8 KiB
TypeScript

import type { DetailJob } from './types'
import { parseDetailFactsOutput } from './text-contract'
export const detailTextRequestId = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
export type DetailTextRecovery = { mode: 'query-only'; stage: 'product-analysis' | 'outline'; requestId: string }
export function detailTextDispatchKey(jobId: string, stage: string, checkpoint: Record<string, unknown>): string {
const attempt = checkpoint.copyAttempt as { id?: unknown; source?: unknown } | undefined
return stage === 'outline' && attempt?.source === 'known-failed-copy' && typeof attempt.id === 'string' && detailTextRequestId.test(attempt.id) ? `${jobId}-outline-${attempt.id}` : `${jobId}-${stage}`
}
export function readDetailTextRecovery(checkpoint: Record<string, unknown>): DetailTextRecovery | null {
const value = checkpoint.textRecovery as DetailTextRecovery | null
if (!value || value.mode !== 'query-only' || !['product-analysis', 'outline'].includes(value.stage) || checkpoint.stage !== value.stage || checkpoint.dispatchPending !== true || typeof value.requestId !== 'string' || !detailTextRequestId.test(value.requestId)) return null
return { mode: value.mode, stage: value.stage, requestId: value.requestId }
}
export function detailTextRecoveryOptions(job: Pick<DetailJob, 'id' | 'kind' | 'status' | 'checkpoint' | 'requestId'>) {
const unavailable = { canQuery: false, canContinue: false, needsRequestId: false }
if (job.kind !== 'analyze') return unavailable
// A definitely ended copy failure may be retried explicitly without redoing accepted analysis.
if (job.status === 'failed' && job.checkpoint.stage === 'outline' && job.checkpoint.dispatchPending === false) {
try { parseDetailFactsOutput(job.checkpoint.facts); return { canQuery: false, canContinue: true, needsRequestId: false } } catch { return unavailable }
}
if (job.status !== 'unknown') return unavailable
const recovered = job.checkpoint.recoveredText as { stage?: unknown; requestId?: unknown; dispatchKey?: unknown } | undefined
if (recovered?.stage === 'product-analysis' && recovered.dispatchKey === `${job.id}-product-analysis` && typeof recovered.requestId === 'string' && detailTextRequestId.test(recovered.requestId) && job.checkpoint.stage === 'selling-points' && job.checkpoint.dispatchPending === false) {
try { parseDetailFactsOutput(job.checkpoint.facts); return { canQuery: false, canContinue: true, needsRequestId: false } } catch { return unavailable }
}
if (!['product-analysis', 'outline'].includes(String(job.checkpoint.stage)) || job.checkpoint.dispatchPending !== true || job.checkpoint.dispatchKey !== detailTextDispatchKey(job.id, String(job.checkpoint.stage), job.checkpoint)) return unavailable
return { canQuery: true, canContinue: false, needsRequestId: !job.requestId }
}