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
+41
View File
@@ -0,0 +1,41 @@
import Anthropic from "@anthropic-ai/sdk";
/**
* Zentrale, gekapselte LLM-Anbindung (Anthropic). Erste und derzeit einzige
* Nutzung: KI-Entwurf der Umsetzungsbeschreibungen (VDA-ISA-ABGABE) im
* Audit-Wizard. Bewusst als Singleton mit lazy-Init, damit ein fehlender
* API-Key den restlichen Serverstart nicht beeinträchtigt.
*
* Konfiguration ausschließlich über Umgebungsvariablen:
* - ANTHROPIC_API_KEY Pflicht für den KI-Betrieb. Fehlt er, ist die
* KI-Anbindung deaktiviert (graceful degradation):
* `getAnthropic()` liefert `null`, Aufrufer fallen auf
* „manuell zu erfassen" zurück.
* - ANTHROPIC_MODEL Optionale Modell-ID; Default `claude-opus-5`.
*
* Das offizielle SDK liest `ANTHROPIC_API_KEY` selbst aus der Umgebung; wir
* prüfen die Existenz dennoch explizit, um den Client nur bei vorhandenem Key
* zu instanziieren und die Deaktivierung eindeutig zu machen.
*/
const DEFAULT_MODEL = "claude-opus-5";
/** Modell-ID für KI-Aufrufe (ENV-Override, sonst Default). */
export const AI_MODEL = process.env.ANTHROPIC_MODEL?.trim() || DEFAULT_MODEL;
let client: Anthropic | null | undefined;
/** True, wenn ein API-Key gesetzt ist und die KI-Anbindung genutzt werden kann. */
export function isAiConfigured(): boolean {
return Boolean(process.env.ANTHROPIC_API_KEY?.trim());
}
/**
* Liefert den (gecachten) Anthropic-Client oder `null`, wenn kein API-Key
* konfiguriert ist. Aufrufer müssen den `null`-Fall behandeln (kein Entwurf).
*/
export function getAnthropic(): Anthropic | null {
if (client !== undefined) return client;
client = isAiConfigured() ? new Anthropic() : null;
return client;
}
+127
View File
@@ -0,0 +1,127 @@
import { getAnthropic, AI_MODEL } from "./client";
/**
* KI-Entwurf einer „Beschreibung der Umsetzung" je ISA-Teilanforderung im
* VDA-ISA-ABGABE-Stil. Aus dem Anforderungstext (MUSS/SOLL) sowie den
* verknüpften Dokumenten/Nachweisen wird EIN konkreter „wie-umgesetzt"-Satz mit
* Dokumentverweis formuliert. Die Konfidenz wird deterministisch aus der
* Quellenlage abgeleitet (nicht vom Modell), damit sie nachvollziehbar bleibt.
*
* Graceful degradation: Ohne konfigurierten API-Key (oder bei einem Fehler des
* Aufrufs) liefert `draftControlDescription` `null` — der Aufrufer belässt den
* Status dann bei „manuell zu erfassen".
*/
export interface DraftDocument {
code: string;
title: string;
version: string;
/** PolicyStatus (FREIGEGEBEN = validiert). */
status: string;
}
export interface DraftInput {
control: string;
reqId: string;
/** MUSS | SOLL */
obligation: string;
/** Anforderungstext (PolicyRequirement.requirement). */
requirement: string;
/** Umsetzungshinweis (PolicyRequirement.implementation). */
implementation: string;
/** Verknüpfte Richtlinien/Verfahren. */
documents: DraftDocument[];
/** Titel vorhandener Nachweise (Evidence) zum Control. */
evidence: string[];
}
export interface DraftResult {
/** Umsetzungssatz inkl. Dokumentverweis (ABGABE-Stil). */
draftText: string;
/** Kompakter Quellenverweis (Dokumentliste), semikolon-getrennt. */
sourceRef: string | null;
confidence: "high" | "medium" | "low";
}
/**
* Konfidenz aus der Quellenlage: ein freigegebenes (validiertes) Dokument UND
* ein Nachweis ⇒ high; irgendein verknüpftes Dokument ⇒ medium; sonst low.
*/
function deriveConfidence(input: DraftInput): DraftResult["confidence"] {
const hasApprovedDoc = input.documents.some((d) => d.status === "FREIGEGEBEN");
const hasEvidence = input.evidence.length > 0;
if (hasApprovedDoc && hasEvidence) return "high";
if (input.documents.length > 0) return "medium";
return "low";
}
/** Semikolon-getrennter Dokumentverweis („R08 – Titel, Version 1.0"). */
function buildSourceRef(docs: DraftDocument[]): string | null {
if (docs.length === 0) return null;
return docs.map((d) => `${d.code} – ${d.title}, Version ${d.version}`).join("; ");
}
const SYSTEM_PROMPT = [
"Du unterstützt bei der Erstellung einer VDA-ISA-Prüfungsdokumentation (TISAX).",
"Formuliere für eine einzelne Anforderung EINEN präzisen deutschen Satz im ABGABE-Stil,",
"der beschreibt, WIE die Anforderung umgesetzt ist. Der Satz nennt konkrete Spezifika",
"aus dem Umsetzungshinweis und schließt mit einem Dokumentverweis in Klammern ab,",
"Form: (<Dokument-Code> – <Titel>, Version <X>, Abschnitt <passend>).",
"Erfinde keine Fakten, keine Zahlen und keine Abschnitte, die sich nicht aus den",
"Eingaben ableiten lassen. Wenn kein Dokument verknüpft ist, lasse den Klammerzusatz weg.",
"Antworte ausschließlich mit dem Satz — ohne Aufzählungszeichen, ohne Anführungszeichen,",
"ohne Vor- oder Nachbemerkung und ohne interne XML-Tags.",
].join(" ");
function buildUserPrompt(input: DraftInput): string {
const docs =
input.documents.length > 0
? input.documents
.map((d) => `- ${d.code}: ${d.title} (Version ${d.version}, Status ${d.status})`)
.join("\n")
: "- (keine verknüpften Dokumente)";
const evidence =
input.evidence.length > 0 ? input.evidence.map((e) => `- ${e}`).join("\n") : "- (keine Nachweise vorhanden)";
return [
`Control: ${input.control} (${input.reqId}), Verbindlichkeit: ${input.obligation}`,
`Anforderung: ${input.requirement}`,
`Umsetzungshinweis: ${input.implementation || "(keiner)"}`,
`Verknüpfte Dokumente:\n${docs}`,
`Vorhandene Nachweise:\n${evidence}`,
].join("\n\n");
}
/**
* Erzeugt den KI-Entwurf. Liefert `null`, wenn keine KI konfiguriert ist oder
* der Aufruf fehlschlägt (kein Fehler-Spam; der Aufrufer degradiert sauber).
*/
export async function draftControlDescription(input: DraftInput): Promise<DraftResult | null> {
const client = getAnthropic();
if (!client) return null;
try {
const response = await client.messages.create({
model: AI_MODEL,
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: [{ role: "user", content: buildUserPrompt(input) }],
});
if (response.stop_reason === "refusal") return null;
const text = response.content
.filter((b): b is Extract<typeof b, { type: "text" }> => b.type === "text")
.map((b) => b.text)
.join("")
.trim();
if (!text) return null;
return {
draftText: text,
sourceRef: buildSourceRef(input.documents),
confidence: deriveConfidence(input),
};
} catch {
// Netzwerk-/API-Fehler dürfen die UI nicht blockieren.
return null;
}
}