feat(use-case-controls): Adressat-Achse — out-of-scope advisory + additiver GOV-Tag

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>
This commit is contained in:
Benjamin Admin
2026-06-16 06:58:37 +02:00
parent f6fe592164
commit 0a6e57ac02
7 changed files with 181 additions and 12 deletions
@@ -6,6 +6,7 @@ import {
provenanceBadgeClass,
severityBadgeClass,
splitByTier,
addresseeLabel,
} from '../_helpers'
const BACKEND_URL =
@@ -13,12 +14,15 @@ const BACKEND_URL =
export const dynamic = 'force-dynamic'
async function getControls(useCase: string): Promise<ControlsResponse | null> {
async function getControls(
useCase: string,
oos: boolean,
): Promise<ControlsResponse | null> {
try {
const res = await fetch(
`${BACKEND_URL}/api/compliance/v1/controls/use-cases/${encodeURIComponent(
useCase,
)}/controls?tier=all&limit=200`,
)}/controls?tier=all&limit=200&include_out_of_scope=${oos}`,
{ cache: 'no-store' },
)
return res.ok ? ((await res.json()) as ControlsResponse) : null
@@ -42,7 +46,23 @@ function ControlsTable({ rows }: { rows: ControlItem[] }) {
<tbody className="divide-y divide-gray-100 bg-white">
{rows.map((c) => (
<tr key={c.id}>
<td className="px-4 py-2 text-gray-900">{c.title}</td>
<td className="px-4 py-2 text-gray-900">
<div>{c.title}</div>
{c.is_gov || !c.applicable ? (
<div className="mt-1 flex flex-wrap gap-1">
{c.is_gov ? (
<span className="rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800">
GOV · Öffentliche Stelle
</span>
) : null}
{!c.applicable ? (
<span className="rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-800">
kein Kunden-Prüfaspekt · {addresseeLabel(c.addressee)}
</span>
) : null}
</div>
) : null}
</td>
<td className="px-4 py-2 text-xs text-gray-500">
{c.sub_topic || '—'}
</td>
@@ -77,11 +97,15 @@ function ControlsTable({ rows }: { rows: ControlItem[] }) {
export default async function UseCaseControlsPage({
params,
searchParams,
}: {
params: Promise<{ useCase: string }>
searchParams: Promise<{ [k: string]: string | string[] | undefined }>
}) {
const { useCase } = await params
const data = await getControls(useCase)
const sp = await searchParams
const oos = sp.oos === '1'
const data = await getControls(useCase, oos)
if (!data) {
return (
@@ -123,6 +147,25 @@ export default async function UseCaseControlsPage({
</span>{' '}
zur fachlichen Prüfung
</p>
{data.out_of_scope_count > 0 ? (
<p className="text-xs">
{data.include_out_of_scope ? (
<Link
href={`/sdk/coverage/${useCase}`}
className="text-purple-600 hover:underline"
>
← Nur Kunden-Prüfaspekte ({data.out_of_scope_count.toLocaleString('de-DE')} Behörde/Mitgliedstaat/Dritter ausblenden)
</Link>
) : (
<Link
href={`/sdk/coverage/${useCase}?oos=1`}
className="text-amber-700 hover:underline"
>
+ {data.out_of_scope_count.toLocaleString('de-DE')} ausgeblendet (kein Kunden-Prüfaspekt: Behörde/Mitgliedstaat/Dritter) — einblenden
</Link>
)}
</p>
) : null}
</header>
<section className="space-y-2">
@@ -7,6 +7,7 @@ import {
provenanceBadgeClass,
splitByTier,
severityBadgeClass,
addresseeLabel,
type UseCaseRow,
type ControlItem,
} from './_helpers'
@@ -17,6 +18,8 @@ const ctrl = (over: Partial<ControlItem>): ControlItem => ({
relevant: true,
tier: 'core',
source_type: 'derived',
applicable: true,
is_gov: false,
...over,
})
@@ -97,6 +100,14 @@ describe('coverage helpers', () => {
expect(severityBadgeClass(null)).toContain('gray')
})
it('addressee label maps keys to German labels', () => {
expect(addresseeLabel('oeffentliche_stelle')).toBe('Öffentliche Stelle')
expect(addresseeLabel('aufsichtsbefugnis')).toBe('Aufsichtsbehörde')
expect(addresseeLabel('staat_eu')).toBe('Mitgliedstaat/EU')
expect(addresseeLabel(null)).toBe('')
expect(addresseeLabel('unbekannt_neu')).toBe('unbekannt_neu')
})
it('splitByTier separates core (relevant) from review', () => {
const { core, review } = splitByTier([
ctrl({ id: 'a', relevant: true }),
@@ -94,6 +94,9 @@ export interface ControlItem {
relevant: boolean
tier: 'core' | 'review'
source_type: 'derived' | 'own_library'
addressee?: string | null
applicable: boolean
is_gov: boolean
}
export interface ControlsResponse {
@@ -105,6 +108,8 @@ export interface ControlsResponse {
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
@@ -112,6 +117,22 @@ export interface ControlsResponse {
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(