807a7002b2
The advisor was structurally correct but unusable: every question showed a snake_case capability id plus a
single generic fallback reason ("Keine Anhaltspunkte im Unternehmensprofil — klären"). The expert text
already EXISTED in the transition patterns (why_asked / reviewable_claim) — the pipeline just dropped it.
- transition_reasoning: TargetRequirement gains `rationale`; assess_transition uses it as the request
reason when present, else the generic fallback (additive, backward-compatible for all consumers).
- onboarding_service._target carries the pattern's why_asked (delta) and reviewable_claim (likely_covered)
into the requirement rationale -> the question's `why`.
- knowledge/onboarding/capability_labels.yaml: curated DE labels (id -> human), reusable across targets;
labels_for() + response.capability_labels expose them; the frontend renders label || prettified id.
Now ISO27001->TISAX reads "Auftragsverarbeitung (Art. 28 DSGVO) — If a TISAX data label is in scope, you
must show Art. 28 GDPR processing-on-behalf controls; ISO 27001 does not establish these." instead of
"data_protection_processing_on_behalf — klären". why_asked text is still EN (existing knowledge; translation
is curation). 34 onboarding+transition tests pass, mypy --strict clean (13 modules), check-loc 0.
112 lines
4.3 KiB
Python
112 lines
4.3 KiB
Python
"""Transition Reasoning v0 (RS-005) — domain objects.
|
|
|
|
The **Transition Planning Engine**: it answers „Was muss ich noch wissen, um vom
|
|
Ausgangszustand in den regulatorischen Zielzustand zu kommen?" — NOT „wie frage ich
|
|
das?". It therefore owns the **information gaps** (`TransitionQuestionRequest`), never
|
|
the rendered question text. Rendering (intent + subject -> sentence) is a separate,
|
|
swappable layer (RS-005.1 Question Generator) and is NOT part of this engine.
|
|
|
|
v0 consumes the Company Capability Profile (Phase 2A) as the „have" state and an
|
|
INJECTED list of `TargetRequirement` as the Execution-owned „required" side (no
|
|
required-capability data in product code — same discipline as 2A's EMPTY_MAPPING).
|
|
|
|
Welt-1-Grenze: a probable coverage (from a certification) is a hint, never „erfüllt";
|
|
it produces a confirmation request, not a verdict. Application/reasoning types, NOT
|
|
meta-model classes (freeze v1.0 untouched). Python 3.9 compatible (no `|` unions).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class TargetType(str, Enum):
|
|
REGULATION = "regulation"
|
|
CERTIFICATION = "certification"
|
|
FRAMEWORK = "framework"
|
|
|
|
|
|
class CoverageStatus(str, Enum):
|
|
ALREADY_COVERED = "already_covered" # confirmed in the company profile
|
|
PROBABLY_COVERED = "probably_covered" # inferred (e.g. from a certification)
|
|
NEEDS_CONFIRMATION = "needs_confirmation" # only declared / weak signal
|
|
MISSING = "missing" # no signal
|
|
NOT_APPLICABLE = "not_applicable"
|
|
UNSUPPORTED = "unsupported" # domain not yet in the corpus (future_corpus_needed)
|
|
|
|
|
|
class RequestPriority(str, Enum):
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class InformationGain(str, Enum):
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class TransitionGoal(BaseModel):
|
|
target_id: str # e.g. "CRA", "TISAX"
|
|
target_type: TargetType = TargetType.REGULATION
|
|
label: str = ""
|
|
|
|
|
|
class TransitionContext(BaseModel):
|
|
company_id: str = ""
|
|
known_certifications: List[str] = Field(default_factory=list)
|
|
known_regulations: List[str] = Field(default_factory=list)
|
|
target: TransitionGoal
|
|
|
|
|
|
# ── INJECTED (Execution-owned): what the target requires ───────────────────
|
|
class TargetRequirement(BaseModel):
|
|
"""One required capability for the target. In v0 injected; later resolved from
|
|
`Obligation -> Control -> Required Capability` + `Control -> question_intent`."""
|
|
|
|
capability_id: str # MCAP-...
|
|
question_intent: str = "verify_existence" # passed through to the request, not rendered
|
|
rationale: str = "" # curated human text (e.g. why_asked / reviewable_claim) — surfaced as the request reason
|
|
expected_evidence: List[str] = Field(default_factory=list)
|
|
source_control_id: Optional[str] = None
|
|
supports_obligations: List[str] = Field(default_factory=list)
|
|
unsupported: bool = False # domain not yet in the corpus
|
|
|
|
|
|
# ── the OWNED output: an information gap, NOT a question ───────────────────
|
|
class TransitionQuestionRequest(BaseModel):
|
|
capability_id: str
|
|
control_id: Optional[str] = None
|
|
reason: str
|
|
question_intent: str # verify_existence / determine_duration / ... (rendered later)
|
|
expected_evidence: List[str] = Field(default_factory=list)
|
|
priority: RequestPriority
|
|
information_gain: InformationGain
|
|
|
|
|
|
class CapabilityCoverage(BaseModel):
|
|
capability_id: str
|
|
status: CoverageStatus
|
|
have_status: Optional[str] = None # the 2A VerificationStatus that drove it
|
|
|
|
|
|
class TransitionSummary(BaseModel):
|
|
headline: str = "" # counts, NO percentage
|
|
what_to_clarify: List[str] = Field(default_factory=list) # capability_ids with a request
|
|
already_covered: List[str] = Field(default_factory=list)
|
|
probably_covered: List[str] = Field(default_factory=list)
|
|
missing: List[str] = Field(default_factory=list)
|
|
not_applicable: List[str] = Field(default_factory=list)
|
|
unsupported: List[str] = Field(default_factory=list)
|
|
|
|
|
|
class TransitionAssessment(BaseModel):
|
|
target_id: str
|
|
coverage: List[CapabilityCoverage] = Field(default_factory=list)
|
|
question_requests: List[TransitionQuestionRequest] = Field(default_factory=list) # ranked
|
|
summary: TransitionSummary
|