Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

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>
This commit is contained in:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
/**
* 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", „ Kunden­daten " → 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;