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>
146 lines
5.9 KiB
TypeScript
146 lines
5.9 KiB
TypeScript
import type { TenantDb } from "@/server/db";
|
|
import type { ControlDescription } from "@prisma/client";
|
|
import { controlTitle, compareControl } from "@/lib/control-titles";
|
|
import type { DraftInput } from "@/server/ai/draft-control-description";
|
|
|
|
/**
|
|
* Gemeinsamer Server-Kontext für die Control-Beschreibungen (VDA-ISA-Spalte 4).
|
|
* Bündelt je Control die Kontrollfrage, die Anforderungs-Bullets aus
|
|
* `PolicyRequirement`, die verknüpften Richtlinien/Verfahren (`PolicyDocument`)
|
|
* sowie den vorhandenen Nachweisstand (`Evidence`) und die bereits erfassten
|
|
* `ControlDescription`-Zeilen. Wird von der Controls-Seite, der ABGABE-Vorschau
|
|
* und den KI-/Speicher-Actions geteilt (eine Aufbereitungsstelle).
|
|
*/
|
|
|
|
export type DescriptionStatus = "open" | "draft" | "confirmed";
|
|
|
|
export interface RequirementBullet {
|
|
reqId: string;
|
|
obligation: string; // MUSS | SOLL
|
|
requirement: string;
|
|
implementation: string;
|
|
/** Verknüpfte Dokumente (policyCode + vaCodes → PolicyDocument). */
|
|
documents: { code: string; title: string; version: string; status: string }[];
|
|
/** Bereits erfasste Beschreibung (Entwurf/übernommen) oder null. */
|
|
description: {
|
|
draftText: string | null;
|
|
sourceRef: string | null;
|
|
confidence: string | null;
|
|
status: DescriptionStatus;
|
|
openAnswer: string | null;
|
|
} | null;
|
|
/** True, wenn zum Control mindestens ein Nachweis existiert. */
|
|
hasEvidence: boolean;
|
|
}
|
|
|
|
/** Status-Ampel je Control: grün (alle übernommen) / orange (teils) / rot (keine). */
|
|
export type ControlLamp = "green" | "amber" | "red";
|
|
|
|
export interface ControlGroup {
|
|
control: string;
|
|
frage: string;
|
|
bullets: RequirementBullet[];
|
|
/** Nachweistitel des Controls (Evidence). */
|
|
evidence: string[];
|
|
lamp: ControlLamp;
|
|
}
|
|
|
|
function descStatus(row: ControlDescription | undefined): DescriptionStatus {
|
|
const s = row?.status;
|
|
return s === "confirmed" || s === "draft" ? s : "open";
|
|
}
|
|
|
|
/** Ampel aus den Beschreibungs-Status der Bullets. */
|
|
function lampOf(bullets: RequirementBullet[]): ControlLamp {
|
|
if (bullets.length === 0) return "red";
|
|
const confirmed = bullets.filter((b) => b.description?.status === "confirmed").length;
|
|
if (confirmed === bullets.length) return "green";
|
|
const started = bullets.filter((b) => b.description && b.description.status !== "open").length;
|
|
return started > 0 ? "amber" : "red";
|
|
}
|
|
|
|
/** Lädt alle Controls (mit Anforderungs-Bullets, Dokumenten, Nachweisen, Beschreibungen). */
|
|
export async function buildControlGroups(db: TenantDb): Promise<ControlGroup[]> {
|
|
const [reqs, docs, evidence, descriptions] = await Promise.all([
|
|
db.policyRequirement.findMany({
|
|
where: { archivedAt: null },
|
|
select: { reqId: true, control: true, obligation: true, requirement: true, implementation: true, policyCode: true, vaCodes: true },
|
|
}),
|
|
db.policyDocument.findMany({ where: { archivedAt: null }, select: { code: true, title: true, version: true, status: true } }),
|
|
db.evidence.findMany({ where: { control: { not: null } }, select: { control: true, title: true } }),
|
|
db.controlDescription.findMany(),
|
|
]);
|
|
|
|
const docByCode = new Map(docs.map((d) => [d.code, d]));
|
|
const descByReq = new Map(descriptions.map((d) => [d.reqId, d]));
|
|
const evidenceByControl = new Map<string, string[]>();
|
|
for (const e of evidence) {
|
|
if (!e.control) continue;
|
|
const list = evidenceByControl.get(e.control) ?? [];
|
|
list.push(e.title);
|
|
evidenceByControl.set(e.control, list);
|
|
}
|
|
|
|
const byControl = new Map<string, RequirementBullet[]>();
|
|
for (const r of reqs) {
|
|
const codes = [r.policyCode, ...r.vaCodes].filter(Boolean);
|
|
const documents = [...new Set(codes)]
|
|
.map((c) => docByCode.get(c))
|
|
.filter((d): d is NonNullable<typeof d> => Boolean(d))
|
|
.map((d) => ({ code: d.code, title: d.title, version: d.version, status: d.status }));
|
|
const row = descByReq.get(r.reqId);
|
|
const bullet: RequirementBullet = {
|
|
reqId: r.reqId,
|
|
obligation: r.obligation,
|
|
requirement: r.requirement,
|
|
implementation: r.implementation,
|
|
documents,
|
|
description: row
|
|
? { draftText: row.draftText, sourceRef: row.sourceRef, confidence: row.confidence, status: descStatus(row), openAnswer: row.openAnswer }
|
|
: null,
|
|
hasEvidence: (evidenceByControl.get(r.control)?.length ?? 0) > 0,
|
|
};
|
|
const list = byControl.get(r.control) ?? [];
|
|
list.push(bullet);
|
|
byControl.set(r.control, list);
|
|
}
|
|
|
|
return [...byControl.entries()]
|
|
.sort((a, b) => compareControl(a[0], b[0]))
|
|
.map(([control, bullets]) => ({
|
|
control,
|
|
frage: controlTitle(control),
|
|
bullets: bullets.sort((a, b) => a.reqId.localeCompare(b.reqId, undefined, { numeric: true })),
|
|
evidence: evidenceByControl.get(control) ?? [],
|
|
lamp: lampOf(bullets),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Baut den KI-Eingabekontext für eine einzelne Anforderung (reqId). Liefert
|
|
* `null`, wenn die Anforderung nicht existiert. Wird von der Draft-Action genutzt.
|
|
*/
|
|
export async function buildDraftInput(db: TenantDb, reqId: string): Promise<DraftInput | null> {
|
|
const r = await db.policyRequirement.findFirst({
|
|
where: { reqId, archivedAt: null },
|
|
select: { reqId: true, control: true, obligation: true, requirement: true, implementation: true, policyCode: true, vaCodes: true },
|
|
});
|
|
if (!r) return null;
|
|
|
|
const codes = [...new Set([r.policyCode, ...r.vaCodes].filter(Boolean))];
|
|
const [docs, evidence] = await Promise.all([
|
|
db.policyDocument.findMany({ where: { code: { in: codes }, archivedAt: null }, select: { code: true, title: true, version: true, status: true } }),
|
|
db.evidence.findMany({ where: { control: r.control }, select: { title: true } }),
|
|
]);
|
|
|
|
return {
|
|
control: r.control,
|
|
reqId: r.reqId,
|
|
obligation: r.obligation,
|
|
requirement: r.requirement,
|
|
implementation: r.implementation,
|
|
documents: docs.map((d) => ({ code: d.code, title: d.title, version: d.version, status: d.status })),
|
|
evidence: evidence.map((e) => e.title),
|
|
};
|
|
}
|