Source code for cadrumo.application.wizard.descendant_door

"""The paged descendant door onto the setup flow's repeating group.

A dedicated, descendant-only :class:`~cadrumo.application.flows.definition.FlowDefinition`
that hosts the exact count page and
:class:`~cadrumo.application.flows.definition.FlowRepeatingGroup` the full setup flow uses
(:func:`~cadrumo.application.wizard.descendant_group.build_descendant_group` and the
entry-event cross-field validator id) — the pages and validators are *adopted*, not
re-authored, so the door and the setup flow can never diverge on the descendant
surface.

The door exists because modify-mode seeding cannot instantiate the repeating
group inside the main setup flow: instance pages are generated dynamically from
the count answer, so the render-time default-seed mechanism cannot reach them and
the group is withheld from a bridged modify definition. This door closes that gap
by seeding through :func:`~cadrumo.application.flows.resume.resume_flow`, whose walk
commits the count answer first (revealing the instance pages) and then seeds each
instance answer — the one seeding channel that re-instantiates the group from
persisted facts.

Lifecycle of one door invocation:

* **seed** — :func:`build_descendant_door` reads the active
  :class:`~cadrumo.domain.user_profile.values.UserProfileRecord`, re-projects its
  ``renta_family.descendiente.{n}.*`` facts to a page-keyed answer map through
  :func:`~cadrumo.application.wizard.persistence.descendant_answers_from_record`,
  and resumes a MODIFY-mode :class:`~cadrumo.application.flows.engine.FlowState` over the
  door definition so the operator opens on their existing descendants.
* **commit** — :func:`persist_descendant_door_answers` projects the submitted
  answers back through
  :func:`~cadrumo.application.wizard.persistence.descendant_facts_from_answers`
  and clears the orphaned rows a shrunk count leaves behind through
  :func:`~cadrumo.application.wizard._checkpoint_store.descendant_clearing_facts`,
  in one revision-bound record command. The door only commits to an
  already-registered, authenticated profile.

The door declares checkpointing UNAVAILABLE in both modes: the commit is owned
here, not by a frontend save-and-exit, so no checkpoint store is wired.
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING

from pydantic import BaseModel

from ...core.flows import CheckpointAvailability, FlowMode
from ...core.models import STRICT_FROZEN_CONFIG
from ..flows.definition import FlowDefinition, FlowSection
from ..flows.definition import locale_copy_ref as _locale_ref
from ..flows.engine import FlowState
from ..flows.resume import resume_flow
from ._checkpoint_store import descendant_clearing_facts
from .catalogue import FAMILIA_SECTION_ID as _FAMILIA_SECTION_ID
from .descendant_group import (
    DESCENDANT_ENTRY_EVENT_VALIDATOR_ID,
    build_descendant_count_page,
    build_descendant_group,
)
from .persistence import descendant_answers_from_record, descendant_facts_from_answers

if TYPE_CHECKING:
    from prompt_toolkit.input import Input
    from prompt_toolkit.output import Output

    from ...domain.calculations.registry.authority import PinnedAuthorityOperation
    from ...domain.user_profile.values import UserProfileRecord
    from ..flows.review import ReviewProjection

#: The door's flow and familia section ids.
DESCENDANT_DOOR_FLOW_ID = "descendiente-door"

# Copy references — both keys already ship in the four locale catalogues, so the
# door adds no new locale key. Declared as module constants so the static locale
# scanner treats them as live (referenced only inside the frozen literals below,
# never at a ``tr()`` call site).
_FLOW_TITLE_LOCALE_KEY = "wizard.setup.descendientes.title"
_FLOW_DESCRIPTION_LOCALE_KEY = "cli.config.profile.descendiente.help"


[docs] class DescendantDoorAnswers(BaseModel): """The door's declared ``answers_model``: the group carries every answer. The descendant surface is entirely a repeating group whose instance answers key the canonical map as ``descendientes#<index>.<page-id>`` — none is a top-level model field — so the door's typed answer model is empty. The commit path reads the engine's committed page-keyed answer map directly (through :func:`~cadrumo.application.wizard.persistence.descendant_facts_from_answers`), never a projection through this model; it exists only to satisfy the :class:`~cadrumo.application.flows.definition.FlowDefinition` contract. """ model_config = STRICT_FROZEN_CONFIG
[docs] def build_descendant_door_definition(*, operation: PinnedAuthorityOperation) -> FlowDefinition: """Return the descendant-only door :class:`FlowDefinition`. Adopts the operation-scoped count page and :func:`~cadrumo.application.wizard.descendant_group.build_descendant_group` verbatim into a single familia section, and names the entry-event cross-field validator on the definition so a bad entry date — or one the declared relación cannot carry — blocks submit — the same validators the setup flow runs. The count page's ``visible_when`` gate on the ``entity-type`` page is stripped: the door has no ``entity-type`` page (an operator who opened the descendant surface is declaring descendants), and a gate naming a page absent from the definition would fail the definition's earlier-page-reference validator at construction. """ count_page = build_descendant_count_page(operation).model_copy(update={"visible_when": None}) descendant_group = build_descendant_group(operation) section = FlowSection( id=_FAMILIA_SECTION_ID, title=_locale_ref(_FLOW_TITLE_LOCALE_KEY), items=(count_page, descendant_group), ) return FlowDefinition( id=DESCENDANT_DOOR_FLOW_ID, title=_locale_ref(_FLOW_TITLE_LOCALE_KEY), description=_locale_ref(_FLOW_DESCRIPTION_LOCALE_KEY), sections=(section,), answers_model=DescendantDoorAnswers, checkpoint={ FlowMode.CREATE: CheckpointAvailability.UNAVAILABLE, FlowMode.MODIFY: CheckpointAvailability.UNAVAILABLE, }, flow_validator_ids=(DESCENDANT_ENTRY_EVENT_VALIDATOR_ID,), )
[docs] def build_descendant_door( record: UserProfileRecord | None, *, operation: PinnedAuthorityOperation, ) -> tuple[FlowDefinition, FlowState]: """Return the door definition and a MODIFY-mode state seeded from ``record``. Re-projects the record's ``renta_family.descendiente.{n}.*`` facts into the page-keyed answer map through :func:`~cadrumo.application.wizard.persistence.descendant_answers_from_record`, then resumes a fresh :class:`~cadrumo.application.flows.engine.FlowState` over the door definition: :func:`~cadrumo.application.flows.resume.resume_flow` commits the seeded count answer first (revealing the instance pages) and then seeds each instance answer against the current definition, so the operator opens on their existing descendants. A childless record seeds an empty map and opens on the count page's zero default. Args: record: The :class:`UserProfileRecord` whose descendant facts seed the resumed flow state, or ``None`` for a childless record. operation: Caller-owned pinned authority operation used to compose and seed the descendant surface. """ definition = build_descendant_door_definition(operation=operation) seed = descendant_answers_from_record(record, operation=operation) resume_state = resume_flow(definition, seed, mode=FlowMode.MODIFY) return definition, resume_state
[docs] def load_active_descendant_record(*, operation: PinnedAuthorityOperation) -> UserProfileRecord: """Load the authoritative record that one descendant-door run will edit.""" from ...core.bucket_pointer import require_active_bucket_id from ..user_profile.profile_record_repository import ProfileRecordRepository profile_id = require_active_bucket_id() return ProfileRecordRepository.for_current_session( profile_id, profile_decode_context=operation.profile_decode_context(), ).load(profile_id)
[docs] def persist_descendant_door_answers( answers: Mapping[str, str], *, baseline: UserProfileRecord, operation: PinnedAuthorityOperation, ) -> UserProfileRecord: """Commit the door's submitted answers as the full descendant fact set. Projects the committed page-keyed answers into the canonical ``renta_family.descendiente.{n}.*`` facts and aggregates through :func:`~cadrumo.application.wizard.persistence.descendant_facts_from_answers`, and clears every on-record descendant path the fresh projection no longer covers (a shrunk count, a removed optional field) through :func:`~cadrumo.application.wizard._checkpoint_store.descendant_clearing_facts`, in one authenticated compare-and-swap command. The clearing reads the authoritative current record before constructing the replacement, so a count-shrink never strands a descendant index above the answered count. Core types: :class:`~cadrumo.domain.user_profile.values.UserProfileRecord`. """ from ...core.bucket_pointer import require_active_bucket_id from ...domain.user_profile.values import UserProfileFact from ..user_profile.fact_write import ProfileFactWriteDoor, apply_profile_fact_changes profile_id = require_active_bucket_id() if str(baseline.profile_id) != profile_id: raise ValueError("descendant door baseline does not belong to the active profile") facts = tuple( UserProfileFact(path=path, value=value) for path, value in descendant_facts_from_answers(answers, operation=operation) ) clearing = descendant_clearing_facts(baseline, answers, operation=operation) return apply_profile_fact_changes( profile_id=profile_id, changes=(*facts, *clearing), door=ProfileFactWriteDoor.DESCENDANTS, expected_record=baseline, profile_decode_context=operation.profile_decode_context(), )
[docs] def run_descendant_door( *, operation: PinnedAuthorityOperation, input: Input | None = None, output: Output | None = None, ) -> tuple[FlowState, ReviewProjection, UserProfileRecord]: """Drive and persist one descendant door invocation through the real line frontend. The door resolves and loads the authenticated active profile itself before prompting, binding the submitted write to that exact revision and digest. Headless callers bind prompt-toolkit devices, which exercises the same production frontend and the same atomic profile writer without replacing either boundary with a test callback. """ from ..flows.line_frontend import LineFlowFrontend # The caller owns the operation span across loading, seeding, the # interactive walk, and the compare-and-swap write. No relationship or # disability catalogue value can outlive the generation that admitted it. baseline = load_active_descendant_record(operation=operation) definition, resume_state = build_descendant_door(baseline, operation=operation) state, projection = LineFlowFrontend( definition, input=input, output=output, ).run( mode=FlowMode.MODIFY, resume_state=resume_state, ) persisted = persist_descendant_door_answers(state.answers, baseline=baseline, operation=operation) return state, projection, persisted
__all__ = [ "DESCENDANT_DOOR_FLOW_ID", "DescendantDoorAnswers", "build_descendant_door", "build_descendant_door_definition", "load_active_descendant_record", "persist_descendant_door_answers", "run_descendant_door", ]