import type { Assessment, CanonicalAssessmentModel } from '../src/domain/canonical-model'; import type { StoredGpoBackupInstance } from '../src/domain/gpo-analysis-persistence'; import type { StoredPolicyEvaluation } from '../src/domain/policy-evaluation'; import type { PolicyAnalysisOverview } from '../src/domain/policy-analysis-overview'; import type { UnifiedResultQuery, UnifiedResultQueryResult } from '../src/domain/unified-result'; import { queryAllUnifiedResults } from '../src/exports/unified-result-jsonl'; import { buildPolicyWorkResults, type PolicyWorkResult } from '../src/evaluation/policy-work-results'; import type { ImportedDataset } from '../src/domain/dataset'; import { canonicalModelForScanRuns, currentPolicyEvaluationsForWork, loadCanonicalProjectWorkspace } from '../src/features/project-import/load-canonical-project-workspace'; import { RepositoryNotFoundError } from '../src/persistence/repository-errors'; /** One timeslot as Data Hub groups it: its assessments and the scanner runs selected inside them. */ export interface TimeslotSelection { assessmentIds: string[]; scanRunIds: string[]; } export interface TimeslotInputReader { listAssessments(projectId: string): Promise; loadAssessmentModel(assessmentId: string): Promise; queryUnifiedResults(query?: UnifiedResultQuery): Promise; listPolicyEvaluations(projectId: string): Promise; listGpoBackupInstances(projectId: string): Promise; getPolicyAnalysisOverview?(projectId: string): Promise>; } export interface TimeslotComparisonInput { assessments: Assessment[]; models: CanonicalAssessmentModel[]; scanRunIds: string[]; gpoResults: PolicyWorkResult[]; /** Latest GPO backup creation time; undefined unless every contributing backup states one. */ gpoObservedAt?: string; } /** * Core-owned adapter: builds one timeslot exactly as Work reads it (scanner models scoped to the * selected runs, GPO settings through the same current-evaluation selection and Work verdict * builder). Only the resulting records reach the Comparison Engine. Import times are never used as * observation times. */ export async function loadTimeslotComparisonInput( reader: TimeslotInputReader, projectId: string, selection: TimeslotSelection ): Promise { const projectAssessments = await reader.listAssessments(projectId); const assessments = selection.assessmentIds.map(id => { const assessment = projectAssessments.find(item => item.id === id); if (!assessment) throw new RepositoryNotFoundError(`Assessment ${id} is not in this project.`); return assessment; }); const loaded = await Promise.all(assessments.map(async assessment => { const model = await reader.loadAssessmentModel(assessment.id); if (!model || model.project.id !== projectId) throw new RepositoryNotFoundError(`Assessment ${assessment.id} is not in this project.`); return model; })); const models = loaded.map(model => canonicalModelForScanRuns(model, selection.scanRunIds)) .filter((model): model is CanonicalAssessmentModel => model !== null); const availableRuns = new Set(models.flatMap(model => model.scanRuns.map(run => run.id))); const missingRun = selection.scanRunIds.find(id => !availableRuns.has(id)); if (missingRun) throw new RepositoryNotFoundError(`Scan run ${missingRun} is not in the selected assessments.`); const observations = await queryAllUnifiedResults(query => reader.queryUnifiedResults(query), { audience: 'internal-analysis', projectId, assessmentIds: selection.assessmentIds, sourceKind: ['policy-observation'] }); let gpoResults: PolicyWorkResult[] = []; let gpoObservedAt: string | undefined; if (observations.length) { const [evaluations, overview, backups] = await Promise.all([ reader.listPolicyEvaluations(projectId), reader.getPolicyAnalysisOverview?.(projectId), reader.listGpoBackupInstances(projectId) ]); gpoResults = buildPolicyWorkResults(observations, currentPolicyEvaluationsForWork(evaluations, new Set(observations.map(item => item.sourceResultId)), overview ? new Set(overview.current.evaluationRunIds) : undefined)); const backupIds = new Set(observations.flatMap(item => item.policyBackupInstanceId ?? [])); const times = backups.filter(item => backupIds.has(item.instance.id)).map(item => item.instance.backupCreatedAt); if (backupIds.size && times.length === backupIds.size && times.every(time => time && Number.isFinite(Date.parse(time)))) { gpoObservedAt = new Date(Math.max(...times.map(time => Date.parse(time!)))).toISOString(); } } return { assessments, models, scanRunIds: [...selection.scanRunIds], gpoResults, ...(gpoObservedAt ? { gpoObservedAt } : {}) }; } /** * Core-owned adapter: the Work dataset of one timeslot, built by the same loader Client Info uses, so the * Comparison Engine reads Client Info's records and result states for its Security Domain values. */ export async function loadTimeslotWorkspace( reader: TimeslotInputReader, projectId: string, selection: TimeslotSelection ): Promise { const { dataset } = await loadCanonicalProjectWorkspace({ listProjectAssessments: id => reader.listAssessments(id), loadAssessmentModel: async assessmentId => { const model = await reader.loadAssessmentModel(assessmentId); if (!model || model.project.id !== projectId) throw new RepositoryNotFoundError(`Assessment ${assessmentId} is not in this project.`); return model; }, queryUnifiedResults: query => reader.queryUnifiedResults(query), listPolicyEvaluations: async id => ({ records: await reader.listPolicyEvaluations(id) }), ...(reader.getPolicyAnalysisOverview ? { getPolicyAnalysisOverview: (id: string) => reader.getPolicyAnalysisOverview!(id) } : {}) }, projectId, selection.scanRunIds.length ? selection.scanRunIds : undefined, selection.assessmentIds, { includeHistoricalScores: false, includeBaselineGuidance: false }); return dataset; }