import type { ImportFileRecord } from '../../domain/canonical-model'; import type { GpoBackupStatusRecord } from '../../api/assessor-api-contract'; import { currentProcessingManifest, processingReadiness, type ProcessingReadinessStatus } from '../../domain/processing-compatibility'; export type DatahubClientProcessingState = 'ready' | 'processing' | 'update' | 'error' | 'no-data'; export interface DatahubClientProcessingSummary { state: DatahubClientProcessingState; label: string; } const UPDATE_STATES = new Set(['reimport-recommended']); const ERROR_STATES = new Set(['incompatible', 'unknown']); const ATTENTION_STATES = new Set([...UPDATE_STATES, ...ERROR_STATES]); export interface DatahubScanProcessingSummary { state: 'ready' | 'update' | 'error'; statuses: ProcessingReadinessStatus[]; explanation: string; } export function summarizeScanProcessingReadiness(files: ImportFileRecord[]): DatahubScanProcessingSummary { const statuses = [...new Set(files.map(file => processingReadiness( file.detectedTool.id || file.processedWith.profileId, file.processedWith )))]; if (!statuses.some(status => ATTENTION_STATES.has(status))) { return { state: 'ready', statuses, explanation: 'The scan uses a current or compatible ASSESSOR processing profile.' }; } const hasError = statuses.some(status => ERROR_STATES.has(status)); const relevantStates = hasError ? ERROR_STATES : UPDATE_STATES; const representative = files.find(file => relevantStates.has(processingReadiness( file.detectedTool.id || file.processedWith.profileId, file.processedWith ))) ?? files[0]; const current = representative ? currentProcessingManifest(representative.detectedTool.id || representative.processedWith.profileId) : undefined; if (statuses.includes('unknown')) { return { state: 'error', statuses, explanation: 'The stored ASSESSOR processing profile is unknown and cannot be assessed safely.' }; } if (statuses.includes('incompatible')) { return { state: 'error', statuses, explanation: `The stored ASSESSOR processing profile is incompatible with the current ASSESSOR profile ${current?.profileVersion ?? 'unknown'}.` }; } const contextCapture = current?.affectedDimensions.includes('environment-context') ? ' The newer ASSESSOR profile captures additional environment context for the assessment.' : ''; return { state: 'update', statuses, explanation: `ASSESSOR profile update: ${representative?.processedWith.profileVersion ?? 'unknown'} → ${current?.profileVersion ?? 'unknown'}. Reimporting applies the newer ASSESSOR processing profile; the ${representative?.detectedTool.name ?? 'scanner'} report itself has not changed.${contextCapture}` }; } /** * Summarises all active scanner runs represented by the client card. Every * timeslot must use a comparable ASSESSOR processing contract before the * client can be considered fully ready for lifecycle comparison. */ export function summarizeClientProcessingReadiness( files: ImportFileRecord[], activeScanRunIds: readonly string[], gpoBackups: readonly Pick[] = [] ): DatahubClientProcessingSummary { if (gpoBackups.some(record => record.state === 'dead-letter' || record.state === 'recovery-required')) { return { state: 'error', label: 'GPO processing failed or requires recovery in a client timeslot.' }; } const runIds = new Set(activeScanRunIds); const statuses = files .filter(file => runIds.has(file.scanRunId)) .map(file => processingReadiness(file.detectedTool.id || file.processedWith.profileId, file.processedWith)); if (statuses.some(status => ERROR_STATES.has(status))) { return { state: 'error', label: 'Processing error requires attention in a client timeslot.' }; } if (statuses.some(status => UPDATE_STATES.has(status))) { return { state: 'update', label: 'ASSESSOR processing profile update available in a client timeslot.' }; } if (gpoBackups.some(record => record.state === 'queued' || record.state === 'running')) { return { state: 'processing', label: 'GPO processing in progress.' }; } return statuses.length || gpoBackups.length ? { state: 'ready', label: 'Processing ready across all client timeslots.' } : { state: 'no-data', label: 'No processing status available.' }; }