/** * Smart-Merge-Diff zwischen zwei Versionen oder zwischen einer Version * und der Hash-Liste eines privaten geforkten Decks. * * Klassifikation: * - **unchanged**: content_hash identisch zwischen alt und neu → * unveränderte Karte, FSRS-State bleibt. * - **added**: Hash nur in neu → komplett neue Karte. * - **removed**: Hash nur in alt → Karte aus dem Deck entfernt. * - **changed** (Heuristik): Karte am selben `ord` ist „added" und * gleichzeitig an demselben `ord` eine „removed". Pull-Requests * bringen später eine echte Karten-Lineage; bis dahin reicht die * Position-Heuristik. * * Geport aus * `cards-decommission-base:services/cards-server/src/services/subscriptions.ts` * (`diffSince`-Funktion, ord-Pairing-Heuristik 1:1). */ export interface DiffCardForHash { contentHash: string; ord: number; } export interface DiffCardFull { contentHash: string; type: string; fields: Record; ord: number; } export interface DiffPayload { from: { semver?: string; versionId?: string }; to: { semver: string; versionId: string }; added: DiffCardFull[]; changed: { previous: { contentHash: string }; next: DiffCardFull }[]; unchanged: { contentHash: string; ord: number }[]; removed: { contentHash: string }[]; } export interface ComputeDiffInput { from: DiffCardForHash[]; to: DiffCardFull[]; fromInfo: { semver?: string; versionId?: string }; toInfo: { semver: string; versionId: string }; } export function computeDiff(input: ComputeDiffInput): DiffPayload { const { from, to, fromInfo, toInfo } = input; const fromHashes = new Set(from.map((c) => c.contentHash)); const toHashes = new Set(to.map((c) => c.contentHash)); const unchanged: { contentHash: string; ord: number }[] = []; // Build ord → hash map of removed cards (in `from` but not in `to`). const removedByOrd = new Map(); for (const c of from) { if (!toHashes.has(c.contentHash)) removedByOrd.set(c.ord, c.contentHash); } const added: DiffCardFull[] = []; const changed: { previous: { contentHash: string }; next: DiffCardFull }[] = []; for (const c of to) { if (fromHashes.has(c.contentHash)) { unchanged.push({ contentHash: c.contentHash, ord: c.ord }); } else if (removedByOrd.has(c.ord)) { const previousHash = removedByOrd.get(c.ord)!; removedByOrd.delete(c.ord); changed.push({ previous: { contentHash: previousHash }, next: c }); } else { added.push(c); } } // Was in removedByOrd übrig bleibt = echte Removals (nicht ord-paired). const removed: { contentHash: string }[] = [...removedByOrd.values()].map((h) => ({ contentHash: h, })); return { from: fromInfo, to: toInfo, added, changed, unchanged, removed, }; }