Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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:
@@ -0,0 +1,145 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user