"""One-way projection of the wizard catalogue into flow definitions.
The one-shot wizard catalogue (:mod:`cadrumo.application.wizard`)
remains the authoring source of the setup questions: it keeps feeding
the ``compile_profile_keys`` projection and the
``register_wizard_catalogue`` core slot. This module
adds the one-way projection from that vocabulary into the substrate's
:class:`FlowDefinition` so the engine and its frontends can drive the
same questions without a second authored catalogue — one source, two
descriptor views, zero drift by construction.
The mapping is mechanical: question ids become page keys unchanged,
``profile_key`` becomes ``domain_key``, the translation-key prompt and
help become locale-key :class:`CopyRef` references, and the
``visible_when`` clause shapes carry over one-to-one. Nothing here
re-derives requirement or visibility semantics.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from ...core.flows import CheckpointAvailability, FlowMode, FlowWidgetKind
from .definition import (
FlowChoice,
FlowCondition,
FlowDefinition,
FlowPage,
FlowSection,
FlowVisibility,
)
from .definition import locale_copy_ref as _locale_ref
if TYPE_CHECKING:
from ..wizard.models import (
WizardChoice,
WizardCondition,
WizardFlow,
WizardQuestion,
WizardSection,
WizardVisibility,
)
[docs]
def flow_definition_from_wizard_flow(
flow: WizardFlow,
*,
checkpoint: dict[FlowMode, CheckpointAvailability],
) -> FlowDefinition:
"""Project a wizard descriptor into a substrate flow definition.
``checkpoint`` is supplied by the caller because the per-mode
declaration is a flow-surface decision the one-shot descriptor
never carried.
"""
return FlowDefinition(
id=flow.id,
title=_locale_ref(str(flow.title)),
description=_locale_ref(str(flow.description)),
sections=tuple(_section(section) for section in flow.sections),
answers_model=flow.answers_model,
checkpoint=checkpoint,
)
def _section(section: WizardSection) -> FlowSection:
return FlowSection(
id=section.id,
title=_locale_ref(str(section.title)),
items=tuple(_page(question) for question in section.questions),
)
def _page(question: WizardQuestion) -> FlowPage:
# The one-shot catalogue declares several closed sets as TEXT
# questions carrying choices (free text validated against the set).
# The substrate's page model renders a closed set as a selectable
# list, never free text, so the projection upgrades those to SELECT —
# same accepted values, same blank-optional semantics.
widget = FlowWidgetKind(question.widget.value)
if widget is FlowWidgetKind.TEXT and question.choices:
widget = FlowWidgetKind.SELECT
return FlowPage(
id=question.id,
widget=widget,
prompt=_locale_ref(str(question.prompt)),
help=_locale_ref(str(question.help)) if question.help is not None else None,
choices=tuple(_choice(choice) for choice in question.choices),
default=question.default,
required=question.required,
visible_when=_visibility(question.visible_when),
answer_type=question.answer_type,
domain_key=question.profile_key,
)
def _choice(choice: WizardChoice) -> FlowChoice:
return FlowChoice(
value=choice.value,
label=_locale_ref(str(choice.label)),
description=_locale_ref(str(choice.description)) if choice.description is not None else None,
)
def _visibility(
visible_when: WizardCondition | WizardVisibility | None,
) -> FlowCondition | FlowVisibility | None:
if visible_when is None:
return None
from ..wizard.models import WizardCondition as _WizardCondition
if isinstance(visible_when, _WizardCondition):
return _condition(visible_when)
return FlowVisibility(any_of=tuple(_condition(clause) for clause in visible_when.any_of))
def _condition(condition: WizardCondition) -> FlowCondition:
return FlowCondition(
page_id=condition.question_id,
equals=condition.equals,
contains=condition.contains,
)
__all__ = ["flow_definition_from_wizard_flow"]