Files
breakpilot-compliance/admin-compliance/app/sdk/coverage/_helpers.ts
T
Benjamin Admin 926dc02a09
CI / test-python-backend (push) Successful in 30s
CI / test-python-document-crawler (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 12s
CI / validate-canonical-controls (push) Successful in 12s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / detect-changes (push) Successful in 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
feat(use-case-controls): relevant als Stufe statt Hard-Filter + Provenance
Der harte relevant=true-Filter versteckte ~25% des Korpus (40.926 Atome),
~70% davon echte Pflichten (500er-Validierung). relevant wird zur Stufe:

- Service: tier-Param (core=Default schuetzt Agent/CRA; all=alles inkl. review),
  ORDER BY relevant DESC; pro Control relevant/tier/source_type
  (own_library bei license_rule=3, sonst derived) + source_regulation/article;
  core_count/review_count. Pure Helper tier_label + source_type (+ Tests).
- Route: optionaler tier-Query (default core) — contract-safe (additiv).
- Frontend: Coverage-Drill-down /sdk/coverage/[useCase] — Kern-Pflichten vs.
  "zur fachlichen Pruefung", je mit Herkunfts-Badge; Uebersicht zeigt Delta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:58:25 +02:00

182 lines
4.9 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'
}
export interface ControlsResponse {
use_case: string
label: string
group: string
granularity: string
tier: string
total: number
core_count: number
review_count: number
limit: number
offset: number
sub_topic: string | null
subtopic_counts: Record<string, number>
controls: ControlItem[]
}
// 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),
}))
}