import type { GpoAnalysisSnapshot, GpoBackupInstance, GpoSnapshotCompleteness, GpoSnapshotStatus } from '../domain/gpo-ingestion'; import { gpoBackupCompleteness, gpoBackupInstanceValidationErrors } from '../domain/gpo-ingestion'; export interface GpoSnapshotBuildOptions { createdAt: string; maximumCollectionSkewMs?: number; } function hash(value: string): string { let result = 0x811c9dc5; for (let index = 0; index < value.length; index += 1) { result ^= value.charCodeAt(index); result = Math.imul(result, 0x01000193); } return (result >>> 0).toString(16).padStart(8, '0'); } function effectiveBackupSetId(instance: GpoBackupInstance): string { return instance.backupSet.id ?? 'gpo-backup-set:' + instance.sourceArtifactId; } function groupKey(instance: GpoBackupInstance): string { const domain = instance.backupSet.scope === 'selected-gpos' && instance.backupSet.evidence === 'user-selection' ? 'user-selected-backup-set' : instance.domain.status === 'resolved' ? instance.domain.key : `unknown:${instance.id}`; const backupSet = effectiveBackupSetId(instance); return JSON.stringify([instance.projectId, domain, backupSet]); } function setDeclaration(instance: GpoBackupInstance): string { return JSON.stringify([ instance.backupSet.scope, instance.backupSet.evidence, instance.backupSet.expectedGpoCount ?? null ]); } function collectionWindow(instances: GpoBackupInstance[]): { from: string; to: string } | undefined { if (instances.some(instance => instance.collection.status === 'unknown')) return undefined; const times = instances.map(instance => (instance.collection as Extract).collectedAt) .sort((left, right) => Date.parse(left) - Date.parse(right)); return times.length ? { from: times[0], to: times[times.length - 1] } : undefined; } function domainSplits(instances: GpoBackupInstance[]): Set { const domainsBySet = new Map>(); instances.forEach(instance => { const domain = instance.domain.status === 'resolved' ? instance.domain.key : `unknown:${instance.id}`; const backupSetId = effectiveBackupSetId(instance); const domains = domainsBySet.get(backupSetId) ?? new Set(); domains.add(domain); domainsBySet.set(backupSetId, domains); }); return new Set([...domainsBySet].filter(([, domains]) => domains.size > 1).map(([setId]) => setId)); } function snapshotForGroup( instances: GpoBackupInstance[], splitSetIds: ReadonlySet, createdAt: string, maximumCollectionSkewMs: number ): GpoAnalysisSnapshot { const first = instances[0]; const diagnostics = new Set(); let inconsistent = false; let indeterminate = false; if (first.domain.status === 'unknown') { diagnostics.add('gpo-domain-unknown'); indeterminate = true; } const derivedBackupSet = !first.backupSet.id; const backupSetId = effectiveBackupSetId(first); const selectedUpload = first.backupSet.scope === 'selected-gpos' && first.backupSet.evidence === 'user-selection'; const mixedDomains = splitSetIds.has(backupSetId); if (derivedBackupSet) diagnostics.add('gpo-backup-set-derived-from-source-artifact'); if (mixedDomains && selectedUpload) { diagnostics.add('gpo-backup-set-domain-mixed'); indeterminate = true; } else if (mixedDomains) { diagnostics.add('gpo-backup-set-domain-split'); inconsistent = true; } if (new Set(instances.map(setDeclaration)).size > 1) { diagnostics.add('gpo-backup-set-declaration-conflict'); inconsistent = true; } const resolvedGpoIds = instances.flatMap(instance => instance.identity.status === 'resolved' ? [instance.identity.gpoId] : []); if (resolvedGpoIds.length !== new Set(resolvedGpoIds).size) { diagnostics.add('gpo-duplicate-identity-in-snapshot'); inconsistent = true; } const resolvedBackupIds = instances.flatMap(instance => instance.identity.status === 'resolved' ? [instance.identity.backupId] : []); if (resolvedBackupIds.length !== new Set(resolvedBackupIds).size) { diagnostics.add('gpo-duplicate-backup-in-snapshot'); inconsistent = true; } if (instances.some(instance => instance.identity.status === 'unresolved')) { diagnostics.add('gpo-identity-unresolved'); indeterminate = true; } if (instances.some(instance => gpoBackupCompleteness(instance) !== 'complete')) { diagnostics.add('gpo-backup-instance-incomplete'); indeterminate = true; } const collectionRange = collectionWindow(instances); if (!collectionRange) { diagnostics.add('gpo-collection-time-unknown'); indeterminate = true; } else if (Date.parse(collectionRange.to) - Date.parse(collectionRange.from) > maximumCollectionSkewMs) { diagnostics.add('gpo-collection-range-exceeded'); inconsistent = true; } if (instances.some(instance => instance.collection.status === 'observed' && instance.collection.evidence !== 'collection-manifest')) { diagnostics.add('gpo-collection-time-not-manifest-backed'); indeterminate = true; } let completeness: GpoSnapshotCompleteness; if (inconsistent) completeness = 'inconsistent'; else if (first.backupSet.scope === 'selected-gpos' || derivedBackupSet) completeness = 'selected-subset'; else if (first.backupSet.scope !== 'complete-domain-export') completeness = 'unknown'; else { const expected = first.backupSet.expectedGpoCount ?? 0; const observed = new Set(resolvedGpoIds).size; if (observed > expected) { diagnostics.add('gpo-backup-set-count-conflict'); completeness = 'inconsistent'; inconsistent = true; } else if (observed < expected) { diagnostics.add('gpo-backup-set-incomplete'); completeness = 'unknown'; indeterminate = true; } else completeness = 'complete'; } const status: GpoSnapshotStatus = inconsistent ? 'inconsistent' : indeterminate ? 'indeterminate' : 'consistent'; const instanceIds = instances.map(instance => instance.id).sort(); return { version: 1, id: `gpo-snapshot-${hash(JSON.stringify([groupKey(first), instanceIds]))}`, projectId: first.projectId, domain: mixedDomains && selectedUpload ? { status: 'unknown' } : first.domain, backupSetId, collectionWindow: collectionRange, instanceIds, status, completeness, permitsSetAbsenceConclusions: status === 'consistent' && completeness === 'complete', diagnosticCodes: [...diagnostics].sort(), createdAt }; } export function buildGpoAnalysisSnapshots( values: GpoBackupInstance[], options: GpoSnapshotBuildOptions ): GpoAnalysisSnapshot[] { if (!Number.isFinite(Date.parse(options.createdAt))) throw new Error('Der Snapshot-Erstellungszeitpunkt ist ungültig.'); const maximumCollectionSkewMs = options.maximumCollectionSkewMs ?? 15 * 60 * 1000; if (!Number.isInteger(maximumCollectionSkewMs) || maximumCollectionSkewMs < 0 || maximumCollectionSkewMs > 24 * 60 * 60 * 1000) { throw new Error('Das zulässige Erhebungszeitfenster ist ungültig.'); } const ids = new Set(); values.forEach(instance => { const errors = gpoBackupInstanceValidationErrors(instance); if (errors.length) throw new Error(`Ungültige GPO-Backupinstanz ${instance.id || ''}: ${errors.join(' ')}`); if (ids.has(instance.id)) throw new Error(`Doppelte GPO-Backupinstanz-ID: ${instance.id}.`); ids.add(instance.id); }); const sorted = [...values].sort((left, right) => left.id.localeCompare(right.id)); const grouped = new Map(); sorted.forEach(instance => { const key = groupKey(instance); grouped.set(key, [...(grouped.get(key) ?? []), instance]); }); const splitSetIds = domainSplits(sorted); return [...grouped.values()] .map(group => snapshotForGroup(group, splitSetIds, options.createdAt, maximumCollectionSkewMs)) .sort((left, right) => left.id.localeCompare(right.id)); }