0a6e57ac02
2-Pass-Haiku-Klassifikation (konservativ + Re-Confirm jeder Nicht-unternehmen- Einstufung) der Review-Tier-Atome: wer muss die Pflicht erfuellen? - Migration 155: atom_classification.addressee (unternehmen/oeffentliche_stelle/ aufsichtsbefugnis/staat_eu/dritter/meta), additiv, kein CHECK. [migration-approved] - Service: addressee + applicable + is_gov pro Control; include_out_of_scope-Param (Default false -> out-of-scope advisory ausgeblendet, NIE geloescht); out_of_scope_count. Pure Helper addressee_applicable/addressee_is_gov (+ Tests). - Route: optionaler include_out_of_scope-Query (contract-safe, additiv). - Frontend: GOV-Chip (additiv) + "kein Kunden-Pruefaspekt"-Chip + 1-Klick-Toggle zum Einblenden der out-of-scope-Atome. Daten: 40.859 Adressat-Tags auf macmini geladen (81% applicable, 19% advisory, 3.146 GOV). Konservativ: NULL/Unklar = applicable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
203 lines
5.6 KiB
TypeScript
203 lines
5.6 KiB
TypeScript
// Pure helpers for the Coverage page (#74). Kept separate so they are unit-testable
|
|
// without rendering the server component.
|
|
|
|
export interface UseCaseRow {
|
|
key: string
|
|
label: string
|
|
group: string
|
|
regulations: string[]
|
|
verification_methods: string[]
|
|
mapped_controls: number
|
|
atom_total: number
|
|
atom_relevant: number
|
|
}
|
|
|
|
export interface CorpusDoc {
|
|
source_regulation: string
|
|
license_rule: number | null
|
|
license_tier: string
|
|
atom_count: number
|
|
use_case: string | null
|
|
}
|
|
|
|
export interface LicenseSummaryRow {
|
|
license_rule: number | null
|
|
label: string
|
|
atom_count: number
|
|
}
|
|
|
|
export interface LicenseCatalogEntry {
|
|
source_id: string
|
|
title: string
|
|
publisher: string | null
|
|
url: string | null
|
|
version: string | null
|
|
license_id: string | null
|
|
license_name: string | null
|
|
commercial_use: string | null
|
|
ship_in_product: boolean | null
|
|
terms_url: string | null
|
|
}
|
|
|
|
export interface CorpusOverview {
|
|
license_summary: LicenseSummaryRow[]
|
|
documents: CorpusDoc[]
|
|
license_catalog: LicenseCatalogEntry[]
|
|
totals: { documents: number; catalog_sources: number }
|
|
}
|
|
|
|
export const USE_CASE_GROUP_LABELS: Record<string, string> = {
|
|
document: 'Dokument-Compliance',
|
|
security: 'Security',
|
|
cross_cutting: 'Querschnitt',
|
|
product: 'Produkt / Sektor',
|
|
}
|
|
|
|
export function licenseTierBadgeClass(rule: number | null): string {
|
|
switch (rule) {
|
|
case 1:
|
|
return 'bg-green-100 text-green-800'
|
|
case 2:
|
|
return 'bg-blue-100 text-blue-800'
|
|
case 3:
|
|
return 'bg-amber-100 text-amber-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-700'
|
|
}
|
|
}
|
|
|
|
export function commercialBadgeClass(commercial: string | null): string {
|
|
switch ((commercial || '').toLowerCase()) {
|
|
case 'allowed':
|
|
return 'bg-green-100 text-green-800'
|
|
case 'restricted':
|
|
return 'bg-amber-100 text-amber-800'
|
|
case 'prohibited':
|
|
return 'bg-red-100 text-red-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-700'
|
|
}
|
|
}
|
|
|
|
// --- Controls drill-down (#80 Stufe-Flip + Provenance) ---
|
|
|
|
export interface ControlItem {
|
|
id: string
|
|
control_id?: string | null
|
|
title: string
|
|
objective?: string | null
|
|
severity?: string | null
|
|
sub_topic?: string | null
|
|
canonical_obligation?: string | null
|
|
source_regulation?: string | null
|
|
source_article?: string | null
|
|
relevant: boolean
|
|
tier: 'core' | 'review'
|
|
source_type: 'derived' | 'own_library'
|
|
addressee?: string | null
|
|
applicable: boolean
|
|
is_gov: boolean
|
|
}
|
|
|
|
export interface ControlsResponse {
|
|
use_case: string
|
|
label: string
|
|
group: string
|
|
granularity: string
|
|
tier: string
|
|
total: number
|
|
core_count: number
|
|
review_count: number
|
|
out_of_scope_count: number
|
|
include_out_of_scope: boolean
|
|
limit: number
|
|
offset: number
|
|
sub_topic: string | null
|
|
subtopic_counts: Record<string, number>
|
|
controls: ControlItem[]
|
|
}
|
|
|
|
// Addressee axis: who must fulfil an obligation. out-of-scope (authority power /
|
|
// member-state-EU / third party / meta) is advisory — hidden by default, never
|
|
// deleted. oeffentliche_stelle = additive GOV hint (public-sector customer).
|
|
export const ADDRESSEE_LABELS: Record<string, string> = {
|
|
unternehmen: 'Unternehmen',
|
|
oeffentliche_stelle: 'Öffentliche Stelle',
|
|
aufsichtsbefugnis: 'Aufsichtsbehörde',
|
|
staat_eu: 'Mitgliedstaat/EU',
|
|
dritter: 'Dritter',
|
|
meta: 'Meta',
|
|
}
|
|
|
|
export function addresseeLabel(a?: string | null): string {
|
|
return a ? ADDRESSEE_LABELS[a] || a : ''
|
|
}
|
|
|
|
// Provenance line: own library vs derived-from-document (with the document, and
|
|
// article when known). The user wants to see WHERE a derived control came from.
|
|
export function provenanceLabel(
|
|
c: Pick<ControlItem, 'source_type' | 'source_regulation' | 'source_article'>,
|
|
): string {
|
|
if (c.source_type === 'own_library') return 'Eigene Bibliothek'
|
|
const doc = c.source_regulation?.trim()
|
|
if (!doc) return 'Abgeleitet'
|
|
const art = c.source_article?.trim()
|
|
return art ? `Abgeleitet · ${doc} ${art}` : `Abgeleitet · ${doc}`
|
|
}
|
|
|
|
export function provenanceBadgeClass(sourceType: string): string {
|
|
return sourceType === 'own_library'
|
|
? 'bg-amber-100 text-amber-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}
|
|
|
|
export function severityBadgeClass(sev: string | null | undefined): string {
|
|
switch ((sev || '').toLowerCase()) {
|
|
case 'critical':
|
|
return 'bg-red-100 text-red-800'
|
|
case 'high':
|
|
return 'bg-orange-100 text-orange-800'
|
|
case 'medium':
|
|
return 'bg-yellow-100 text-yellow-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-600'
|
|
}
|
|
}
|
|
|
|
// Split into the two display tiers: Kern-Pflichten (relevant) and the
|
|
// 'zur Prüfung' tier (shown but flagged) — never hidden.
|
|
export function splitByTier(controls: ControlItem[]): {
|
|
core: ControlItem[]
|
|
review: ControlItem[]
|
|
} {
|
|
const core: ControlItem[] = []
|
|
const review: ControlItem[] = []
|
|
for (const c of controls) (c.relevant ? core : review).push(c)
|
|
return { core, review }
|
|
}
|
|
|
|
export interface UseCaseGroup {
|
|
group: string
|
|
label: string
|
|
rows: UseCaseRow[]
|
|
}
|
|
|
|
// Group use-cases by their registry group (stable order), each group's rows
|
|
// sorted by how many relevant obligations it carries (desc).
|
|
export function groupUseCases(rows: UseCaseRow[]): UseCaseGroup[] {
|
|
const order = ['document', 'security', 'cross_cutting', 'product']
|
|
const by: Record<string, UseCaseRow[]> = {}
|
|
for (const r of rows) {
|
|
;(by[r.group] ||= []).push(r)
|
|
}
|
|
const groups = order.filter((g) => by[g]?.length)
|
|
for (const g of Object.keys(by)) {
|
|
if (!order.includes(g)) groups.push(g)
|
|
}
|
|
return groups.map((g) => ({
|
|
group: g,
|
|
label: USE_CASE_GROUP_LABELS[g] || g,
|
|
rows: [...by[g]].sort((a, b) => b.atom_relevant - a.atom_relevant),
|
|
}))
|
|
}
|