Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
/**
|
||
* M2 Strukturanalyse — Dedup-Erkennung & Autovervollständigung für (primäre)
|
||
* Informations-Assets (IMPL-tisax-wizard-neustruktur.md §2.1).
|
||
*
|
||
* `normalizeAssetName` erzeugt den Dedup-Schlüssel `Asset.normalizedName`, auf dem
|
||
* die `@@unique([tenantId, normalizedName])` Exakt-Duplikate verhindert. Bei einem
|
||
* Treffer wird das bestehende Asset VERKNÜPFT (ProcessAsset) statt neu angelegt.
|
||
*
|
||
* Die Normalisierung ist bewusst reine, DB-agnostische Logik (kein Prisma) — dieselbe
|
||
* Funktion läuft im Server (Schreibpfad + searchAssets) und im Client (Combobox), damit
|
||
* Vorschau und Persistenz denselben Schlüssel sehen.
|
||
*
|
||
* Reihenfolge (fix, §2.1):
|
||
* trim → Mehrfach-Whitespace kollabieren → Unicode-NFC → toLowerCase("de") →
|
||
* Umlaut-Faltung (ä→ae, ö→oe, ü→ue, ß→ss) → Satzzeichen entfernen.
|
||
*/
|
||
|
||
/** Umlaut-/ß-Faltung nach der Kleinschreibung (Reihenfolge relevant). */
|
||
function foldUmlauts(s: string): string {
|
||
return s
|
||
.replace(/ä/g, "ae")
|
||
.replace(/ö/g, "oe")
|
||
.replace(/ü/g, "ue")
|
||
.replace(/ß/g, "ss");
|
||
}
|
||
|
||
/**
|
||
* Erzeugt den normalisierten Dedup-Schlüssel eines Asset-Namens.
|
||
* Beispiel: „Kundendaten", „kundendaten", „ Kundendaten " → alle `kundendaten`.
|
||
* Liefert `""` für leere/nur-Satzzeichen-Eingaben (Aufrufer behandelt das als „kein Schlüssel").
|
||
*/
|
||
export function normalizeAssetName(input: string): string {
|
||
if (!input) return "";
|
||
let s = input.trim();
|
||
// Weiches Trennzeichen / Zero-Width entfernen, bevor Whitespace kollabiert wird.
|
||
s = s.replace(/[-]/g, "");
|
||
s = s.replace(/\s+/g, " ");
|
||
s = s.normalize("NFC");
|
||
s = s.toLocaleLowerCase("de");
|
||
s = foldUmlauts(s);
|
||
// Satzzeichen/Sonderzeichen entfernen — nur Wortzeichen und Leerzeichen behalten,
|
||
// dann erneut Whitespace normalisieren und trimmen.
|
||
s = s.replace(/[^\p{L}\p{N} ]+/gu, "");
|
||
s = s.replace(/\s+/g, " ").trim();
|
||
return s;
|
||
}
|
||
|
||
/**
|
||
* Levenshtein-Distanz (DB-agnostische Fuzzy-Nähe, §2.1). Wird für die WEICHE Warnung
|
||
* („Meintest du …?") auf der Kandidatenliste genutzt — als Alternative zu Postgres
|
||
* `pg_trgm.similarity()` (siehe Integrationsbericht). Iterativ, O(n·m) Zeit, O(min)-Speicher.
|
||
*/
|
||
export function levenshtein(a: string, b: string): number {
|
||
if (a === b) return 0;
|
||
if (a.length === 0) return b.length;
|
||
if (b.length === 0) return a.length;
|
||
// Kürzere Zeichenkette als Spaltenachse (weniger Speicher).
|
||
if (a.length > b.length) [a, b] = [b, a];
|
||
let prev = Array.from({ length: a.length + 1 }, (_, i) => i);
|
||
let curr = new Array<number>(a.length + 1);
|
||
for (let j = 1; j <= b.length; j++) {
|
||
curr[0] = j;
|
||
for (let i = 1; i <= a.length; i++) {
|
||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||
curr[i] = Math.min(prev[i] + 1, curr[i - 1] + 1, prev[i - 1] + cost);
|
||
}
|
||
[prev, curr] = [curr, prev];
|
||
}
|
||
return prev[a.length];
|
||
}
|
||
|
||
/**
|
||
* Ähnlichkeit 0..1 auf Basis der normalisierten Levenshtein-Distanz.
|
||
* 1 = identisch. Für die weiche Fuzzy-Warnung ab Schwelle (Default 0.82).
|
||
*/
|
||
export function nameSimilarity(a: string, b: string): number {
|
||
const na = normalizeAssetName(a);
|
||
const nb = normalizeAssetName(b);
|
||
if (!na && !nb) return 1;
|
||
const max = Math.max(na.length, nb.length);
|
||
if (max === 0) return 1;
|
||
return 1 - levenshtein(na, nb) / max;
|
||
}
|
||
|
||
/** Schwelle, ab der ein Kandidat als Nah-Duplikat gewarnt wird (§2.1). */
|
||
export const FUZZY_SIMILARITY_THRESHOLD = 0.82;
|