feat(navigator): Product Regulatory Navigator as a thin missing-facts layer
Step 2 of the convergence sequence. The Navigator sits over the CanonicalProductRegulatoryProfile (prefilled from company-profile / ProductWizard) and reports ONLY which facts are still missing + prioritized questions to collect them. It decides which facts are needed, NEVER what applies — that stays with the Scope Engine (step 3). No regulation logic, no UI, no Go, no RAG. - NavigatorQuestion (interaction type, NOT a compliance-meta-model class — freeze v1.0 untouched): question_id, target_field, label, why_needed, regulatory_domains_unblocked (static metadata), answer_type, options, priority. - QUESTION_CATALOG: 12 questions over canonical gaps — P0 (markets, role, lifecycle, machine/component), P1 (radio, usage-data, security-function, environmental wastewater/air/chemicals triggers), P2 (structured BOM). - engine: navigate() -> missing_facts + suggested_questions (priority-sorted) + completeness_summary (ready_for_scope = no P0 missing); apply_answers() -> updated profile. Pure field-presence; no scope import. - 8 tests: <=10 questions for a filled company-profile, known facts not re-asked, environmental = trigger questions only (no law evaluation), apply round-trip, P0 ordering, ready_for_scope. 41 tests green, mypy clean, LOC ok. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
"""Product Regulatory Navigator — thin missing-facts layer.
|
||||||
|
|
||||||
|
Sits above the CanonicalProductRegulatoryProfile (prefilled from company-profile /
|
||||||
|
ProductWizard) and reports only which facts are still missing + prioritized
|
||||||
|
questions to collect them. It decides which facts are needed, NOT what regulation
|
||||||
|
applies — that stays with the Scope Engine (step 3). No regulation logic, no UI,
|
||||||
|
no Go, no RAG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .engine import CompletenessSummary, NavigatorResult, apply_answers, navigate
|
||||||
|
from .questions import (
|
||||||
|
QUESTION_CATALOG,
|
||||||
|
AnswerType,
|
||||||
|
NavigatorQuestion,
|
||||||
|
QuestionPriority,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"navigate",
|
||||||
|
"apply_answers",
|
||||||
|
"NavigatorResult",
|
||||||
|
"CompletenessSummary",
|
||||||
|
"NavigatorQuestion",
|
||||||
|
"AnswerType",
|
||||||
|
"QuestionPriority",
|
||||||
|
"QUESTION_CATALOG",
|
||||||
|
]
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""Product Regulatory Navigator engine — missing-facts only.
|
||||||
|
|
||||||
|
`navigate(profile)` reports which canonical fields are still unknown and the
|
||||||
|
prioritized questions to fill them. `apply_answers(profile, answers)` returns the
|
||||||
|
updated profile. It NEVER decides what applies — that is the Scope Engine (step 3).
|
||||||
|
Pure field-presence checking; no scope-engine import, no regulation evaluation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Type
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from compliance.profile.canonical import (
|
||||||
|
CanonicalLifecyclePhase,
|
||||||
|
CanonicalProductRegulatoryProfile,
|
||||||
|
EconomicOperatorRole,
|
||||||
|
ProductComponent,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .questions import QUESTION_CATALOG, NavigatorQuestion, QuestionPriority
|
||||||
|
|
||||||
|
_ENUM_FIELDS: Dict[str, Type[Any]] = {
|
||||||
|
"economic_operator_role": EconomicOperatorRole,
|
||||||
|
"lifecycle_phase": CanonicalLifecyclePhase,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CompletenessSummary(BaseModel):
|
||||||
|
total_relevant: int
|
||||||
|
answered: int
|
||||||
|
missing: int
|
||||||
|
missing_by_priority: Dict[str, int] = Field(default_factory=dict)
|
||||||
|
ready_for_scope: bool # True once no P0 fact is missing
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class NavigatorResult(BaseModel):
|
||||||
|
missing_facts: List[str] = Field(default_factory=list) # canonical target fields
|
||||||
|
suggested_questions: List[NavigatorQuestion] = Field(default_factory=list)
|
||||||
|
completeness_summary: CompletenessSummary
|
||||||
|
|
||||||
|
|
||||||
|
def _value(profile: CanonicalProductRegulatoryProfile, dotted: str) -> Any:
|
||||||
|
if "." in dotted:
|
||||||
|
head, tail = dotted.split(".", 1)
|
||||||
|
return getattr(getattr(profile, head), tail, None)
|
||||||
|
return getattr(profile, dotted, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_unknown(profile: CanonicalProductRegulatoryProfile, q: NavigatorQuestion) -> bool:
|
||||||
|
value = _value(profile, q.target_field)
|
||||||
|
if value is None:
|
||||||
|
return True
|
||||||
|
if isinstance(value, list) and not value:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def navigate(profile: CanonicalProductRegulatoryProfile) -> NavigatorResult:
|
||||||
|
missing = [q for q in QUESTION_CATALOG if _is_unknown(profile, q)]
|
||||||
|
missing.sort(key=lambda q: q.order())
|
||||||
|
|
||||||
|
by_priority: Dict[str, int] = {}
|
||||||
|
for q in missing:
|
||||||
|
by_priority[q.priority.value] = by_priority.get(q.priority.value, 0) + 1
|
||||||
|
ready = QuestionPriority.P0.value not in by_priority
|
||||||
|
|
||||||
|
total = len(QUESTION_CATALOG)
|
||||||
|
summary = CompletenessSummary(
|
||||||
|
total_relevant=total,
|
||||||
|
answered=total - len(missing),
|
||||||
|
missing=len(missing),
|
||||||
|
missing_by_priority=by_priority,
|
||||||
|
ready_for_scope=ready,
|
||||||
|
note=(
|
||||||
|
"%d von %d Fakten vorhanden; %d offen. Scope-Engine startklar: %s."
|
||||||
|
% (total - len(missing), total, len(missing), "ja" if ready else "nein (P0 fehlt)")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return NavigatorResult(
|
||||||
|
missing_facts=[q.target_field for q in missing],
|
||||||
|
suggested_questions=missing,
|
||||||
|
completeness_summary=summary,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce(q: NavigatorQuestion, value: Any) -> Any:
|
||||||
|
if q.target_field in _ENUM_FIELDS:
|
||||||
|
return _ENUM_FIELDS[q.target_field](value)
|
||||||
|
if q.target_field == "components":
|
||||||
|
return [c if isinstance(c, ProductComponent) else ProductComponent(**c) for c in (value or [])]
|
||||||
|
if q.answer_type.value in {"country_list", "multiselect"}:
|
||||||
|
return list(value or [])
|
||||||
|
if q.answer_type.value == "bool":
|
||||||
|
return bool(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def apply_answers(
|
||||||
|
profile: CanonicalProductRegulatoryProfile, answers: Dict[str, Any]
|
||||||
|
) -> CanonicalProductRegulatoryProfile:
|
||||||
|
updated = profile.model_copy(deep=True)
|
||||||
|
by_id = {q.question_id: q for q in QUESTION_CATALOG}
|
||||||
|
for question_id, raw in answers.items():
|
||||||
|
q = by_id.get(question_id)
|
||||||
|
if q is None or raw is None:
|
||||||
|
continue
|
||||||
|
value = _coerce(q, raw)
|
||||||
|
if "." in q.target_field:
|
||||||
|
head, tail = q.target_field.split(".", 1)
|
||||||
|
setattr(getattr(updated, head), tail, value)
|
||||||
|
else:
|
||||||
|
setattr(updated, q.target_field, value)
|
||||||
|
return updated
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""Product Regulatory Navigator — question catalog.
|
||||||
|
|
||||||
|
The Navigator is a THIN missing-facts layer over CanonicalProductRegulatoryProfile.
|
||||||
|
It does NOT decide what applies — `regulatory_domains_unblocked` is static metadata
|
||||||
|
(which domains a fact would help the Scope Engine decide later), never an
|
||||||
|
evaluation. No regulation logic, no UI, no Go, no RAG.
|
||||||
|
|
||||||
|
`NavigatorQuestion` is an interaction type, NOT a compliance-meta-model class
|
||||||
|
(architecture freeze v1.0 untouched).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from compliance.profile.canonical import CanonicalLifecyclePhase, EconomicOperatorRole
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerType(str, Enum):
|
||||||
|
BOOL = "bool"
|
||||||
|
ENUM = "enum"
|
||||||
|
MULTISELECT = "multiselect"
|
||||||
|
TEXT = "text"
|
||||||
|
COUNTRY_LIST = "country_list"
|
||||||
|
COMPONENT_LIST = "component_list"
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionPriority(str, Enum):
|
||||||
|
P0 = "P0" # blocks scope: EU-vs-not, role, lifecycle, machine/component
|
||||||
|
P1 = "P1" # unblocks a specific domain: RED, Data Act, environment, security
|
||||||
|
P2 = "P2" # refinement: structured BOM
|
||||||
|
|
||||||
|
|
||||||
|
_PRIORITY_ORDER = {QuestionPriority.P0: 0, QuestionPriority.P1: 1, QuestionPriority.P2: 2}
|
||||||
|
|
||||||
|
|
||||||
|
class NavigatorQuestion(BaseModel):
|
||||||
|
question_id: str
|
||||||
|
target_field: str # dotted path into the canonical profile
|
||||||
|
label: str
|
||||||
|
why_needed: str
|
||||||
|
regulatory_domains_unblocked: List[str] = Field(default_factory=list)
|
||||||
|
answer_type: AnswerType
|
||||||
|
options: List[str] = Field(default_factory=list)
|
||||||
|
priority: QuestionPriority
|
||||||
|
|
||||||
|
def order(self) -> int:
|
||||||
|
return _PRIORITY_ORDER[self.priority]
|
||||||
|
|
||||||
|
|
||||||
|
_ROLE_OPTIONS = [e.value for e in EconomicOperatorRole]
|
||||||
|
_PHASE_OPTIONS = [e.value for e in CanonicalLifecyclePhase]
|
||||||
|
|
||||||
|
QUESTION_CATALOG: List[NavigatorQuestion] = [
|
||||||
|
# ── P0: block the scope decision itself ───────────────────────────
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="markets",
|
||||||
|
target_field="markets",
|
||||||
|
label="In welche Märkte / Länder liefern Sie das Produkt?",
|
||||||
|
why_needed="Bestimmt EU- vs. Nicht-EU-Anwendbarkeit und nationale Pflichten.",
|
||||||
|
regulatory_domains_unblocked=["cyber", "machine_safety", "data", "radio", "emv", "environment"],
|
||||||
|
answer_type=AnswerType.COUNTRY_LIST,
|
||||||
|
priority=QuestionPriority.P0,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="economic_operator_role",
|
||||||
|
target_field="economic_operator_role",
|
||||||
|
label="Welche Rolle nehmen Sie ein?",
|
||||||
|
why_needed="Pflichten hängen von der Rolle ab (Hersteller/Importeur/Händler/Betreiber/Service).",
|
||||||
|
regulatory_domains_unblocked=["cyber", "machine_safety", "data"],
|
||||||
|
answer_type=AnswerType.ENUM,
|
||||||
|
options=_ROLE_OPTIONS,
|
||||||
|
priority=QuestionPriority.P0,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="lifecycle_phase",
|
||||||
|
target_field="lifecycle_phase",
|
||||||
|
label="In welcher Lebenszyklusphase betrachten Sie das Produkt?",
|
||||||
|
why_needed="Manche Pflichten greifen nur beim Inverkehrbringen oder in der Wartung.",
|
||||||
|
regulatory_domains_unblocked=["cyber", "machine_safety"],
|
||||||
|
answer_type=AnswerType.ENUM,
|
||||||
|
options=_PHASE_OPTIONS,
|
||||||
|
priority=QuestionPriority.P0,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="is_machine",
|
||||||
|
target_field="is_machine",
|
||||||
|
label="Ist das Produkt eine (vollständige) Maschine?",
|
||||||
|
why_needed="Entscheidet die Anwendbarkeit der Maschinenverordnung.",
|
||||||
|
regulatory_domains_unblocked=["machine_safety"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P0,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="is_component",
|
||||||
|
target_field="is_component",
|
||||||
|
label="Ist das Produkt ein Bauteil / eine unvollständige Maschine?",
|
||||||
|
why_needed="Sicherheitsbauteil vs. vollständige Maschine ändert die Pflichten.",
|
||||||
|
regulatory_domains_unblocked=["machine_safety"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P0,
|
||||||
|
),
|
||||||
|
# ── P1: unblock one specific domain ───────────────────────────────
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="has_radio_module",
|
||||||
|
target_field="has_radio_module",
|
||||||
|
label="Enthält das Produkt ein Funkmodul (WLAN/Bluetooth/Mobilfunk)?",
|
||||||
|
why_needed="Ein Funkmodul löst die Funkanlagen-Richtlinie (RED) aus.",
|
||||||
|
regulatory_domains_unblocked=["radio"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P1,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="generates_usage_data",
|
||||||
|
target_field="generates_usage_data",
|
||||||
|
label="Erzeugt das vernetzte Produkt nutzbare Produkt-/Nutzungsdaten?",
|
||||||
|
why_needed="Erzeugte Nutzungsdaten entscheiden über Data-Act-Pflichten.",
|
||||||
|
regulatory_domains_unblocked=["data"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P1,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="has_security_function",
|
||||||
|
target_field="has_security_function",
|
||||||
|
label="Hat das Produkt eine dedizierte Security-Funktion (gegen böswillige Akteure)?",
|
||||||
|
why_needed="Trennt Security- von Safety-Funktion (CRA vs. MaschinenVO).",
|
||||||
|
regulatory_domains_unblocked=["cyber", "machine_safety"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P1,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="env_wastewater",
|
||||||
|
target_field="environmental.discharges_to_wastewater",
|
||||||
|
label="Gibt das Produkt Stoffe an Wasser / Abwasser ab?",
|
||||||
|
why_needed="Abwassereinleitung löst Abwasser-/Gewässerrecht aus.",
|
||||||
|
regulatory_domains_unblocked=["environment_water"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P1,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="env_air",
|
||||||
|
target_field="environmental.emits_to_air",
|
||||||
|
label="Entstehen Luftemissionen (VOC, Staub, Verbrennung, Aerosole)?",
|
||||||
|
why_needed="Luftemissionen lösen Immissionsschutzrecht aus.",
|
||||||
|
regulatory_domains_unblocked=["environment_air"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P1,
|
||||||
|
),
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="env_chemicals",
|
||||||
|
target_field="environmental.uses_cleaning_chemicals",
|
||||||
|
label="Werden Reinigungs-, Desinfektions- oder Biozidmittel verwendet/mitgeliefert?",
|
||||||
|
why_needed="Chemikalien lösen REACH/CLP/Detergenzien-/Biozidrecht aus.",
|
||||||
|
regulatory_domains_unblocked=["chemicals"],
|
||||||
|
answer_type=AnswerType.BOOL,
|
||||||
|
priority=QuestionPriority.P1,
|
||||||
|
),
|
||||||
|
# ── P2: refinement ────────────────────────────────────────────────
|
||||||
|
NavigatorQuestion(
|
||||||
|
question_id="components",
|
||||||
|
target_field="components",
|
||||||
|
label="Aus welchen wesentlichen Komponenten besteht das Produkt?",
|
||||||
|
why_needed="Eine strukturierte Stückliste verfeinert komponenten-abgeleitete Pflichten.",
|
||||||
|
regulatory_domains_unblocked=["radio", "emv", "environment_water", "chemicals"],
|
||||||
|
answer_type=AnswerType.COMPONENT_LIST,
|
||||||
|
priority=QuestionPriority.P2,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Tests for the Product Regulatory Navigator (missing-facts layer).
|
||||||
|
|
||||||
|
Acceptance: a well-filled company-profile yields <= 10 questions; known facts are
|
||||||
|
not re-asked; environmental questions are trigger-only (no law evaluation); the
|
||||||
|
Navigator decides which facts are missing, NOT what applies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from compliance.navigator import NavigatorResult, apply_answers, navigate
|
||||||
|
from compliance.navigator.questions import QUESTION_CATALOG, QuestionPriority
|
||||||
|
from compliance.profile import from_company_profile
|
||||||
|
from compliance.profile.canonical import CanonicalProductRegulatoryProfile, EconomicOperatorRole
|
||||||
|
|
||||||
|
COMPANY = {
|
||||||
|
"industry": "Maschinenbau",
|
||||||
|
"business_model": "B2B",
|
||||||
|
"company_size": "medium",
|
||||||
|
"target_markets": ["DE", "EU"],
|
||||||
|
"primary_jurisdiction": "DE",
|
||||||
|
"machine_builder": {
|
||||||
|
"productTypes": ["special_machine"],
|
||||||
|
"containsFirmware": True,
|
||||||
|
"hasSafetyFunction": True,
|
||||||
|
"isNetworked": True,
|
||||||
|
"hasRemoteAccess": True,
|
||||||
|
"hasOTAUpdates": True,
|
||||||
|
"hasRiskAssessment": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _empty() -> CanonicalProductRegulatoryProfile:
|
||||||
|
return CanonicalProductRegulatoryProfile(name="X")
|
||||||
|
|
||||||
|
|
||||||
|
# 1. well-filled company-profile -> at most 10 questions.
|
||||||
|
def test_filled_company_profile_at_most_10_questions():
|
||||||
|
result = navigate(from_company_profile(COMPANY))
|
||||||
|
assert len(result.suggested_questions) <= 10
|
||||||
|
|
||||||
|
|
||||||
|
# 2. known facts (markets, is_machine) are not re-asked; true gaps still are.
|
||||||
|
def test_known_facts_not_reasked():
|
||||||
|
result = navigate(from_company_profile(COMPANY))
|
||||||
|
assert "markets" not in result.missing_facts
|
||||||
|
assert "is_machine" not in result.missing_facts
|
||||||
|
# genuine gaps the company-profile cannot provide are still surfaced
|
||||||
|
assert "economic_operator_role" in result.missing_facts
|
||||||
|
assert "has_radio_module" in result.missing_facts
|
||||||
|
|
||||||
|
|
||||||
|
# 3. environmental questions are trigger-only — no environmental-law evaluation.
|
||||||
|
def test_environmental_questions_are_triggers_only():
|
||||||
|
result = navigate(_empty())
|
||||||
|
env = [q for q in result.suggested_questions if q.target_field.startswith("environmental.")]
|
||||||
|
assert len(env) >= 3
|
||||||
|
assert all(q.answer_type.value == "bool" for q in env)
|
||||||
|
|
||||||
|
|
||||||
|
# 4. the Navigator decides only missing facts, never what applies.
|
||||||
|
def test_navigator_decides_only_missing_facts():
|
||||||
|
assert set(NavigatorResult.model_fields.keys()) == {
|
||||||
|
"missing_facts",
|
||||||
|
"suggested_questions",
|
||||||
|
"completeness_summary",
|
||||||
|
}
|
||||||
|
# no question carries a verdict — only metadata about what it would unblock
|
||||||
|
for q in QUESTION_CATALOG:
|
||||||
|
assert q.regulatory_domains_unblocked # metadata, not a decision
|
||||||
|
assert hasattr(q, "answer_type")
|
||||||
|
|
||||||
|
|
||||||
|
# 5. apply_answers updates the profile; answered facts drop out of missing.
|
||||||
|
def test_apply_answers_updates_profile():
|
||||||
|
profile = from_company_profile(COMPANY)
|
||||||
|
updated = apply_answers(
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
"economic_operator_role": "manufacturer",
|
||||||
|
"markets": ["DE", "US"],
|
||||||
|
"has_radio_module": True,
|
||||||
|
"env_wastewater": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert updated.economic_operator_role == EconomicOperatorRole.MANUFACTURER
|
||||||
|
assert updated.markets == ["DE", "US"]
|
||||||
|
assert updated.has_radio_module is True
|
||||||
|
assert updated.environmental.discharges_to_wastewater is True
|
||||||
|
|
||||||
|
after = navigate(updated)
|
||||||
|
assert "economic_operator_role" not in after.missing_facts
|
||||||
|
assert "has_radio_module" not in after.missing_facts
|
||||||
|
assert "environmental.discharges_to_wastewater" not in after.missing_facts
|
||||||
|
|
||||||
|
|
||||||
|
# 6. questions are ordered P0 -> P1 -> P2.
|
||||||
|
def test_priority_ordering():
|
||||||
|
questions = navigate(_empty()).suggested_questions
|
||||||
|
orders = [q.order() for q in questions]
|
||||||
|
assert orders == sorted(orders)
|
||||||
|
assert questions[0].priority == QuestionPriority.P0
|
||||||
|
|
||||||
|
|
||||||
|
# 7. ready_for_scope flips once all P0 facts are answered.
|
||||||
|
def test_ready_for_scope_after_p0():
|
||||||
|
profile = _empty()
|
||||||
|
assert navigate(profile).completeness_summary.ready_for_scope is False
|
||||||
|
answered = apply_answers(
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
"markets": ["DE"],
|
||||||
|
"economic_operator_role": "manufacturer",
|
||||||
|
"lifecycle_phase": "placing_on_market",
|
||||||
|
"is_machine": True,
|
||||||
|
"is_component": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
summary = navigate(answered).completeness_summary
|
||||||
|
assert summary.ready_for_scope is True
|
||||||
|
|
||||||
|
|
||||||
|
# 8. empty profile asks the full (bounded) catalog.
|
||||||
|
def test_empty_profile_bounded_catalog():
|
||||||
|
result = navigate(_empty())
|
||||||
|
assert len(result.suggested_questions) == len(QUESTION_CATALOG)
|
||||||
|
assert result.completeness_summary.total_relevant == len(QUESTION_CATALOG)
|
||||||
Reference in New Issue
Block a user