Assessor_2.0/src/gpo/snapshot-builder.ts

190 lines
7.8 KiB
TypeScript

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<GpoBackupInstance['collection'], { status: 'observed' }>).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<string> {
const domainsBySet = new Map<string, Set<string>>();
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<string>();
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<string>,
createdAt: string,
maximumCollectionSkewMs: number
): GpoAnalysisSnapshot {
const first = instances[0];
const diagnostics = new Set<string>();
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<string>();
values.forEach(instance => {
const errors = gpoBackupInstanceValidationErrors(instance);
if (errors.length) throw new Error(`Ungültige GPO-Backupinstanz ${instance.id || '<ohne 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<string, GpoBackupInstance[]>();
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));
}