feat(transition): Transition Reasoning v0 (RS-005) — Transition Planning Engine
Second reasoning mode, scope per user: the engine owns the INFORMATION GAPS, not the
questions. assess_transition(context, target_requirements, company_profile) emits
ranked TransitionQuestionRequest {capability, control, reason, question_intent,
expected_evidence, priority, information_gain} -- NOT rendered question text. Rendering
(intent+subject->sentence) is a separate swappable layer (RS-005.1), not here.
Consumes the Company Capability Profile (2A) as "have" + injected TargetRequirement
(Execution-owned placeholder) as "required" -- no required-capability data in product
code (EMPTY_REQUIREMENTS, mocks only in tests). A certification-derived capability is
probably_covered (Welt 1) -> a confirmation request, never already_covered/"erfuellt".
Deterministic, computed-not-stored, no percentages.
Activates 2A/2C/RCI (first consumer of the Company profile). Freeze-respecting: additive
package, no new graph/base class/meta-model class. 9 tests, mypy --strict clean, LOC ok.
No endpoint/UI/RAG; question rendering deliberately deferred to RS-005.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""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
|
||||
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
|
||||
Reference in New Issue
Block a user