feat(use-case-controls): relevant als Stufe statt Hard-Filter + Provenance
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
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
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>
This commit is contained in:
@@ -51,6 +51,12 @@ async def controls_for_use_case(
|
||||
use_case: str,
|
||||
primary_only: bool = Query(False, description="master-grain Fallback: nur Primaerzweck"),
|
||||
sub_topic: Optional[str] = Query(None, description="atom-grain: nur dieses Sub-Thema"),
|
||||
tier: str = Query(
|
||||
"core",
|
||||
pattern="^(core|all)$",
|
||||
description="atom-grain: 'core'=nur validierte Kern-Pflichten (Default), "
|
||||
"'all'=alle inkl. 'zur Prüfung'-Stufe",
|
||||
),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
svc: UseCaseControlsService = Depends(get_use_case_controls_service),
|
||||
@@ -58,4 +64,4 @@ async def controls_for_use_case(
|
||||
"""Controls for a topic. Atom-grain (Haiku: relevant + sub_topic) wenn vorhanden,
|
||||
sonst master-grain Seed."""
|
||||
with translate_domain_errors():
|
||||
return svc.controls_for_use_case(use_case, primary_only, limit, offset, sub_topic)
|
||||
return svc.controls_for_use_case(use_case, primary_only, limit, offset, sub_topic, tier)
|
||||
|
||||
@@ -43,6 +43,21 @@ def relevance_score(
|
||||
return round(min(score, 1.0), 3)
|
||||
|
||||
|
||||
def tier_label(relevant: bool) -> str:
|
||||
"""Soft tier instead of a hard filter: validated obligations are 'core',
|
||||
the rest are 'review' — shown but flagged for expert curation. The boundary
|
||||
'concrete vs. generic' is genuinely fuzzy; hiding 'review' dropped ~25% of
|
||||
the corpus, much of it real (filter validation 2026-06-15)."""
|
||||
return "core" if relevant else "review"
|
||||
|
||||
|
||||
def source_type(license_rule: Optional[int]) -> str:
|
||||
"""Provenance: 'own_library' = self-written (license_rule 3, no commercial
|
||||
source); 'derived' = lifted from a sourced document (license 1/2, the
|
||||
document is in source_regulation)."""
|
||||
return "own_library" if license_rule == 3 else "derived"
|
||||
|
||||
|
||||
# Representative member (most severe, then lowest control_id) carries the
|
||||
# human-readable title/objective — master_controls.canonical_name is only the
|
||||
# merge token, so we surface a real member control per master.
|
||||
@@ -76,8 +91,8 @@ _LIST_SQL = text("""
|
||||
# per-atom relevance + sub-topic. Far more precise + organized than the master
|
||||
# seed. Preferred whenever the use-case has been processed.
|
||||
_ATOM_LIST_SQL = text("""
|
||||
SELECT ac.control_uuid, ac.sub_topic, ac.canonical_obligation,
|
||||
cc.control_id, cc.title, cc.objective, cc.severity,
|
||||
SELECT ac.control_uuid, ac.sub_topic, ac.canonical_obligation, ac.relevant,
|
||||
cc.control_id, cc.title, cc.objective, cc.severity, cc.license_rule,
|
||||
cpl.source_regulation, cpl.source_article
|
||||
FROM atom_classification ac
|
||||
JOIN canonical_controls cc ON cc.id = ac.control_uuid
|
||||
@@ -86,9 +101,9 @@ _ATOM_LIST_SQL = text("""
|
||||
FROM control_parent_links cpl
|
||||
WHERE cpl.control_uuid = ac.control_uuid LIMIT 1
|
||||
) cpl ON true
|
||||
WHERE ac.use_case = :uc AND ac.relevant = true
|
||||
WHERE ac.use_case = :uc AND (:all = true OR ac.relevant = true)
|
||||
AND (:sub IS NULL OR ac.sub_topic = :sub)
|
||||
ORDER BY ac.sub_topic NULLS LAST,
|
||||
ORDER BY ac.relevant DESC, ac.sub_topic NULLS LAST,
|
||||
CASE cc.severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1
|
||||
WHEN 'medium' THEN 2 ELSE 3 END, cc.title
|
||||
LIMIT :lim OFFSET :off
|
||||
@@ -147,18 +162,24 @@ class UseCaseControlsService:
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
sub_topic: Optional[str] = None,
|
||||
tier: str = "core",
|
||||
) -> dict[str, Any]:
|
||||
"""Controls for ``use_case``. Prefers the atom-grain Haiku classification
|
||||
(precise + sub-topic-organized) when present; falls back to the
|
||||
master-grain seed otherwise."""
|
||||
master-grain seed otherwise.
|
||||
|
||||
``tier`` (atom-grain only): 'core' = validated obligations only (default,
|
||||
keeps the agent/CRA callers precise); 'all' = everything incl. the
|
||||
'review' tier (shown, flagged) so the human browse view loses nothing."""
|
||||
if not is_valid_use_case(use_case):
|
||||
raise NotFoundError(f"Unknown use_case '{use_case}'")
|
||||
uc = REGISTRY[use_case]
|
||||
lim = min(max(int(limit), 1), 200)
|
||||
off = max(int(offset), 0)
|
||||
tier = tier if tier in ("core", "all") else "core"
|
||||
|
||||
if self._has_atom_grain(use_case):
|
||||
return self._atom_grain(uc, lim, off, sub_topic)
|
||||
return self._atom_grain(uc, lim, off, sub_topic, tier)
|
||||
|
||||
# --- master-grain fallback (recall seed) ---
|
||||
count_sql = (
|
||||
@@ -204,23 +225,29 @@ class UseCaseControlsService:
|
||||
).scalar() or 0) > 0
|
||||
|
||||
def _atom_grain(
|
||||
self, uc, lim: int, off: int, sub_topic: Optional[str],
|
||||
self, uc, lim: int, off: int, sub_topic: Optional[str], tier: str = "core",
|
||||
) -> dict[str, Any]:
|
||||
total = self.db.execute(text(
|
||||
"SELECT count(*) FROM atom_classification "
|
||||
"WHERE use_case = :uc AND relevant = true "
|
||||
"AND (:sub IS NULL OR sub_topic = :sub)"
|
||||
), {"uc": uc.key, "sub": sub_topic}).scalar() or 0
|
||||
all_flag = tier == "all"
|
||||
counts = self.db.execute(text(
|
||||
"SELECT count(*) FILTER (WHERE relevant), "
|
||||
"count(*) FILTER (WHERE NOT relevant) "
|
||||
"FROM atom_classification "
|
||||
"WHERE use_case = :uc AND (:sub IS NULL OR sub_topic = :sub)"
|
||||
), {"uc": uc.key, "sub": sub_topic}).first()
|
||||
core_count = int((counts[0] if counts else 0) or 0)
|
||||
review_count = int((counts[1] if counts else 0) or 0)
|
||||
total = core_count + review_count if all_flag else core_count
|
||||
facet = {
|
||||
row[0]: int(row[1])
|
||||
for row in self.db.execute(text(
|
||||
"SELECT COALESCE(sub_topic, '(none)'), count(*) "
|
||||
"FROM atom_classification WHERE use_case = :uc AND relevant = true "
|
||||
"FROM atom_classification WHERE use_case = :uc "
|
||||
"AND (:all = true OR relevant = true) "
|
||||
"GROUP BY 1 ORDER BY 2 DESC"
|
||||
), {"uc": uc.key}).fetchall()
|
||||
), {"uc": uc.key, "all": all_flag}).fetchall()
|
||||
}
|
||||
rows = self.db.execute(_ATOM_LIST_SQL, {
|
||||
"uc": uc.key, "sub": sub_topic, "lim": lim, "off": off,
|
||||
"uc": uc.key, "all": all_flag, "sub": sub_topic, "lim": lim, "off": off,
|
||||
}).fetchall()
|
||||
controls = [
|
||||
{
|
||||
@@ -233,11 +260,16 @@ class UseCaseControlsService:
|
||||
"canonical_obligation": r.canonical_obligation,
|
||||
"source_regulation": r.source_regulation,
|
||||
"source_article": r.source_article,
|
||||
"relevant": bool(r.relevant),
|
||||
"tier": tier_label(r.relevant),
|
||||
"source_type": source_type(r.license_rule),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return {
|
||||
"use_case": uc.key, "label": uc.label, "group": uc.group,
|
||||
"granularity": "atom", "total": int(total), "limit": lim, "offset": off,
|
||||
"granularity": "atom", "tier": tier, "total": int(total),
|
||||
"core_count": core_count, "review_count": review_count,
|
||||
"limit": lim, "offset": off,
|
||||
"sub_topic": sub_topic, "subtopic_counts": facet, "controls": controls,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ from compliance.domain import NotFoundError
|
||||
from compliance.services.use_case_controls import (
|
||||
UseCaseControlsService,
|
||||
relevance_score,
|
||||
source_type,
|
||||
tier_label,
|
||||
)
|
||||
|
||||
_NET_KW = ("firewall", "tls", "port", "segmentation", "network", "header")
|
||||
@@ -55,3 +57,17 @@ def test_controls_for_unknown_use_case_raises_not_found():
|
||||
svc = UseCaseControlsService(db=None) # guard runs before any DB access
|
||||
with pytest.raises(NotFoundError):
|
||||
svc.controls_for_use_case("does_not_exist")
|
||||
|
||||
|
||||
def test_tier_label_maps_relevance_to_soft_tier():
|
||||
assert tier_label(True) == "core"
|
||||
assert tier_label(False) == "review"
|
||||
|
||||
|
||||
def test_source_type_own_library_vs_derived():
|
||||
# license_rule 3 = self-written framework, no commercial source
|
||||
assert source_type(3) == "own_library"
|
||||
# license 1 (public domain/EU/NIST) and 2 (CC-BY) are derived from a document
|
||||
assert source_type(1) == "derived"
|
||||
assert source_type(2) == "derived"
|
||||
assert source_type(None) == "derived"
|
||||
|
||||
Reference in New Issue
Block a user