"""Strict run contracts for the composite workflow engine.
This module owns stages, purposes, deadline observations, step details, and terminal
run results. Records are frozen strict Pydantic models or closed enumerations, and
persisted presentation carries locale keys rather than rendered prose. Encrypted
workflow state, declaration pointers, and active-profile helpers live in the separate
``_state_models`` owner.
See Also:
:class:`~cadrumo.application.workflow.engine.WorkflowEngine`
Produces :class:`WorkflowResult` records and advances
:class:`WorkflowStage` values.
:class:`~cadrumo.application.workflow.run_models.WorkflowPurpose`
Selects the local FILE or VERIFY policy that controls deadline and
preflight treatment.
:class:`~cadrumo.application.workflow.persistence.WorkflowRunRepository`
Persists terminal :class:`WorkflowResult` records in secure storage.
:class:`~cadrumo.application.workflow.persistence.WorkflowStateRepository`
Persists the encrypted :class:`WorkflowState` envelope.
:mod:`cadrumo.application.modelo.workflow_gate`
Drives calculation revisions through the workflow and persists the
resulting run record before verification or local filing state changes.
"""
from __future__ import annotations
import re
from datetime import date, datetime
from decimal import Decimal
from enum import StrEnum
from typing import Annotated, Literal
from pydantic import AwareDatetime, BaseModel, BeforeValidator, Field, NonNegativeInt, field_validator, model_validator
from ...core.auth_provider import AuthProviderKind
from ...core.errors.hierarchy import SiteHealthState, SiteHealthStatusLike, pydantic_validation_boundary
from ...core.hashing import sha256_hex
from ...core.identifier_grammar import NamespacedId
from ...core.modelo import Modelo
from ...core.models import STRICT_FROZEN_CONFIG as _STRICT_FROZEN
from ...core.period import Period
from ...core.text_bounds import PositiveCount
from ...domain.deadlines.models import ModeloDeadline, ObligationStatus
from ...domain.submission.models import ModeloDraftStatus
from ..operator_actions.models import ConditionEvidence, PreconditionVerdict
from ._identity import period_identity_segment
from .abort import WorkflowAbortReason
from .engine_helpers import CertificateSeverityValue, DeadlineRole, FilingWindowState
_WORKFLOW_PROSE_EVIDENCE_KEY_TOKENS = frozenset(
{
"description",
"detail",
"exception",
"message",
"prose",
"reason",
"summary",
"text",
"traceback",
},
)
_WORKFLOW_EXCEPTION_TEXT_PATTERN = re.compile(
r"^[a-z_][a-z0-9_.]*(?:error|exception):",
flags=re.IGNORECASE,
)
_WORKFLOW_STABLE_FACT_ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_.:-]*$")
[docs]
class WorkflowStage(StrEnum):
"""The read-only stages of the composite workflow, in strict order."""
LOADING_PROFILE = "LOADING_PROFILE"
COMPUTING_DEADLINES = "COMPUTING_DEADLINES"
CHECKING_INBOX = "CHECKING_INBOX"
BUILDING_DRAFT = "BUILDING_DRAFT"
VALIDATING_DRAFT = "VALIDATING_DRAFT"
RUNNING_PREFLIGHT = "RUNNING_PREFLIGHT"
DONE = "DONE"
ABORTED = "ABORTED"
[docs]
class WorkflowPurpose(StrEnum):
"""Why the workflow engine is being driven.
The purpose decides whether the filing-window deadline is an abort
gate or merely informational context:
* ``FILE`` — the end-to-end filing pipeline (``work file`` and the
end-to-end ``WorkflowEngine`` run). Filing without a pending
obligation is refused: the ``COMPUTING_DEADLINES`` stage aborts
with :attr:`WorkflowAbortReason.NO_PENDING_OBLIGATION` when the
schedule carries no matching obligation and with
:attr:`WorkflowAbortReason.DEADLINE_PASSED` when the obligation
window has already closed.
* ``VERIFY`` — the ``work verify`` calculation check. Verification
asserts a calculation is internally sound against the registry's
verification expectations; it has no honest dependency on the
AEAT filing calendar. The ``COMPUTING_DEADLINES`` stage records
the filing-window state as informational context and never
aborts on it, so a correct calculation can be confirmed early,
offline, or for a past period.
"""
FILE = "FILE"
VERIFY = "VERIFY"
[docs]
class WorkflowSiteHealthFacts(BaseModel):
"""Locale-neutral persisted projection of one site-health observation.
Adapter evidence may include a probe URL, source-language marker text, and
a redacted HTML fragment. Those transient diagnostics never cross the
workflow-run persistence boundary. The projection retains only closed
state, stable identity, timestamp, numeric status, retry timing, and count
facts needed by renderers and operators.
"""
model_config = _STRICT_FROZEN
alert_code: str = Field(
pattern=r"^workflow\.site\.[a-z][a-z0-9_]*$",
min_length=3,
max_length=96,
)
state: SiteHealthState
observed_at: AwareDatetime
http_status: int = Field(ge=100, le=599)
retry_after_seconds: int | None = Field(default=None, ge=1)
detected_marker_count: NonNegativeInt
@model_validator(mode="after")
@pydantic_validation_boundary
def _validate_alert_identity(self) -> WorkflowSiteHealthFacts:
"""Bind the persisted alert code to the canonical closed state."""
expected = f"workflow.site.{self.state.value}"
if self.alert_code != expected:
raise ValueError("workflow site-health alert code must match its canonical state")
return self
[docs]
@classmethod
def from_status(cls, status: SiteHealthStatusLike) -> WorkflowSiteHealthFacts:
"""Project adapter status without URL, marker text, or HTML evidence."""
return cls(
alert_code=f"workflow.site.{status.state.value}",
state=status.state,
observed_at=status.observed_at,
http_status=status.evidence.http_status,
retry_after_seconds=status.retry_after_seconds,
detected_marker_count=len(status.evidence.detected_markers),
)
[docs]
class SiteHealthAlert(BaseModel):
"""Workflow-side alert carrying only stable site-health facts.
Attached to a :class:`WorkflowStep` when the AEAT browser health-check
adapter reports a non-nominal site status during a workflow run. ``stage``
identifies the workflow stage that observed the alert; ``run_id`` ties it
to the enclosing :class:`WorkflowResult`.
"""
model_config = _STRICT_FROZEN
stage: WorkflowStage
status: WorkflowSiteHealthFacts
run_id: str = Field(min_length=1, max_length=128)
[docs]
class WorkflowDeadlineRecoveryFacts(BaseModel):
"""Locale-neutral legal and amount facts for an overdue obligation.
The domain recovery record also carries an operator command. Workflow-run
persistence deliberately excludes that presentation-owned string and keeps
only registry identities and evaluated recargo facts.
"""
model_config = _STRICT_FROZEN
still_filable: bool
recargo_band_id: str = Field(min_length=1, max_length=64)
min_completed_months: NonNegativeInt
max_completed_months: int | None = Field(default=None, ge=0)
surcharge_pct: Decimal = Field(ge=Decimal("0"))
interest_applies: bool
legal_ref: str = Field(min_length=1, max_length=128)
@model_validator(mode="after")
@pydantic_validation_boundary
def _validate_stable_facts(self) -> WorkflowDeadlineRecoveryFacts:
"""Reject prose-shaped identifiers and an inverted month range."""
if _WORKFLOW_STABLE_FACT_ID_PATTERN.fullmatch(self.recargo_band_id) is None:
raise ValueError("recargo band id must be a stable locale-neutral identity")
if _WORKFLOW_STABLE_FACT_ID_PATTERN.fullmatch(self.legal_ref) is None:
raise ValueError("recargo legal reference must be a stable locale-neutral identity")
if self.max_completed_months is not None and self.max_completed_months < self.min_completed_months:
raise ValueError("recargo maximum completed months cannot precede the minimum")
return self
[docs]
class WorkflowObligationFacts(BaseModel):
"""Strict persisted projection of one domain filing obligation.
``ModeloDeadline`` contains source-language applicability prose and a raw
recovery command. Neither belongs in a durable workflow record. This
projection retains the canonical address, dates, status, legal identities,
and typed overdue facts needed by resume and rendering consumers.
"""
model_config = _STRICT_FROZEN
modelo: Modelo
period: Period
opens_on: date
closes_on: date
payment_cutoff_on: date | None = None
status: ObligationStatus
boe_references: tuple[str, ...] = ()
recovery: WorkflowDeadlineRecoveryFacts | None = None
@field_validator("boe_references")
@classmethod
@pydantic_validation_boundary
def _legal_references_are_stable(cls, value: tuple[str, ...]) -> tuple[str, ...]:
"""Keep only unique registry identities, never rendered citations."""
if len(set(value)) != len(value):
raise ValueError("workflow obligation legal references must be unique")
if any(_WORKFLOW_STABLE_FACT_ID_PATTERN.fullmatch(item) is None for item in value):
raise ValueError("workflow obligation legal references must be stable locale-neutral identities")
return tuple(sorted(value))
@model_validator(mode="after")
@pydantic_validation_boundary
def _validate_window(self) -> WorkflowObligationFacts:
"""Retain the domain window and overdue-recovery coherence."""
if self.opens_on > self.closes_on:
raise ValueError("workflow obligation opens_on cannot follow closes_on")
if self.payment_cutoff_on is not None and self.payment_cutoff_on > self.closes_on:
raise ValueError("workflow obligation payment cutoff cannot follow closes_on")
if self.recovery is not None and self.status is not ObligationStatus.OVERDUE:
raise ValueError("workflow obligation recovery facts require overdue status")
return self
[docs]
@classmethod
def from_deadline(cls, obligation: ModeloDeadline) -> WorkflowObligationFacts:
"""Project one domain deadline without its prose or raw command fields."""
recovery = obligation.recovery
recovery_facts = None
if recovery is not None:
band = recovery.recargo_band
recovery_facts = WorkflowDeadlineRecoveryFacts(
still_filable=recovery.still_filable,
recargo_band_id=band.id,
min_completed_months=band.min_completed_months,
max_completed_months=band.max_completed_months,
surcharge_pct=band.surcharge_pct,
interest_applies=band.interest_applies,
legal_ref=band.legal_ref,
)
return cls(
modelo=obligation.modelo,
period=obligation.period,
opens_on=obligation.opens_on,
closes_on=obligation.closes_on,
payment_cutoff_on=obligation.payment_cutoff_on,
status=obligation.status,
boe_references=obligation.boe_references,
recovery=recovery_facts,
)
[docs]
class WorkflowDiagnosticSkipReason(StrEnum):
"""Closed reasons a non-applicable workflow diagnostic was skipped."""
NOT_WIRED = "not_wired"
class _WorkflowStepDetail(BaseModel):
"""Frozen base for one closed, locale-neutral workflow detail shape."""
model_config = _STRICT_FROZEN
[docs]
class WorkflowDeadlineContextDetails(_WorkflowStepDetail):
"""The one of the closed deadline contexts a workflow step can carry."""
kind: Literal["deadline_context"]
modelo: Modelo
period: Period
opens_on: date | None = None
closes_on: date | None = None
filing_window: FilingWindowState | None = None
deadline_role: DeadlineRole | None = None
overdue: bool | None = None
extemporanea: bool | None = None
@model_validator(mode="after")
@pydantic_validation_boundary
def _validate_context(self) -> WorkflowDeadlineContextDetails:
"""Keep deadline metadata internally coherent rather than loosely optional.
A context carries exactly one of three mutually exclusive shapes — an
overdue observation, a binding filing window, or an informational one —
each with its own coherence rule below.
"""
if self.overdue is not None or self.extemporanea is not None:
self._validate_overdue_shape()
return self
if (self.filing_window is None) != (self.deadline_role is None):
raise ValueError("deadline_role and filing_window must be declared together")
if self.deadline_role is None:
if self.opens_on is None and self.closes_on is None:
raise ValueError("deadline context requires at least one boundary date")
return self
if self.deadline_role is DeadlineRole.BINDING:
self._validate_binding_shape()
return self
self._validate_informational_shape()
return self
def _both_boundary_dates_declared(self) -> bool:
"""Return whether the context carries both filing-window boundary dates."""
return self.opens_on is not None and self.closes_on is not None
def _validate_overdue_shape(self) -> None:
"""Require an overdue observation to carry only its closing date."""
if self.overdue is not True or self.extemporanea is not True:
raise ValueError("overdue deadline context requires overdue and extemporanea to be true")
if self.closes_on is None or self.opens_on is not None:
raise ValueError("overdue deadline context requires only closes_on")
if self.filing_window is not None or self.deadline_role is not None:
raise ValueError("overdue deadline context cannot carry filing-window metadata")
def _validate_binding_shape(self) -> None:
"""Require a binding role to name a future window bounded on both sides."""
if self.filing_window is not FilingWindowState.FUTURE:
raise ValueError("binding deadline context requires a future filing window")
if not self._both_boundary_dates_declared():
raise ValueError("binding deadline context requires opens_on and closes_on")
def _validate_informational_shape(self) -> None:
"""Require an informational window to be either absent or fully dated."""
if self.filing_window is FilingWindowState.ABSENT:
if self.opens_on is not None or self.closes_on is not None:
raise ValueError("absent informational windows cannot carry boundary dates")
return
if not self._both_boundary_dates_declared():
raise ValueError("dated informational windows require opens_on and closes_on")
[docs]
class WorkflowInboxSkippedDetails(_WorkflowStepDetail):
"""An inbox step intentionally skipped because its adapter is not wired."""
kind: Literal["inbox_skipped"]
skip_reason: Literal[WorkflowDiagnosticSkipReason.NOT_WIRED]
[docs]
class WorkflowInboxBlockedDetails(_WorkflowStepDetail):
"""A workflow inbox failure with a bounded, machine-readable first item."""
kind: Literal["inbox_blocked"]
blocker_count: PositiveCount
first_notificacion_id: str = Field(min_length=1, max_length=256)
[docs]
class WorkflowDraftBuiltDetails(_WorkflowStepDetail):
"""The durable identity of a successfully built draft."""
kind: Literal["draft_built"]
draft_id: str = Field(min_length=1, max_length=256)
[docs]
class WorkflowAlreadyFiledDetails(_WorkflowStepDetail):
"""The model and period already evidenced as filed."""
kind: Literal["already_filed"]
modelo: Modelo
period: Period
expediente_count: PositiveCount
[docs]
class WorkflowDraftNotReadyDetails(_WorkflowStepDetail):
"""A built draft whose lifecycle status does not permit filing."""
kind: Literal["draft_not_ready"]
draft_id: str = Field(min_length=1, max_length=256)
draft_status: ModeloDraftStatus
blocking_finding_codes: tuple[str, ...] = Field(default_factory=tuple)
@field_validator("blocking_finding_codes")
@classmethod
@pydantic_validation_boundary
def _finding_codes_are_stable(cls, value: tuple[str, ...]) -> tuple[str, ...]:
"""Require deterministic non-prose finding identities."""
if len(set(value)) != len(value):
raise ValueError("blocking finding codes must be unique")
if any(not code or code != code.strip() or any(character.isspace() for character in code) for code in value):
raise ValueError("blocking finding codes must be stable non-prose identities")
return tuple(sorted(value))
[docs]
class WorkflowDraftMismatchDetails(_WorkflowStepDetail):
"""A draft that cannot be used for the target obligation.
The mismatching facts themselves are condition evidence on the paired
:class:`PreconditionVerdict`, not a second free-form detail map.
"""
kind: Literal["draft_mismatch"]
draft_id: str = Field(min_length=1, max_length=256)
[docs]
class WorkflowValidationFailedDetails(_WorkflowStepDetail):
"""The number of blocking validation findings on one draft."""
kind: Literal["validation_failed"]
error_count: PositiveCount
[docs]
class WorkflowAuthCheckDetails(_WorkflowStepDetail):
"""Closed certificate/provider readiness facts for preflight."""
kind: Literal["auth_check"]
provider_kind: AuthProviderKind | None = None
provider_check_skipped: bool = False
skip_reason: Literal[WorkflowDiagnosticSkipReason.NOT_WIRED] | None = None
cert_not_after: date | None = None
cert_severity: CertificateSeverityValue | None = None
cert_days_until_expiry: int | None = None
@model_validator(mode="after")
@pydantic_validation_boundary
def _validate_provider_shape(self) -> WorkflowAuthCheckDetails:
"""Make configured and skipped provider observations disjoint shapes."""
if self.provider_check_skipped:
self._validate_skipped_shape()
return self
if self.provider_kind is None or self.skip_reason is not None:
raise ValueError("configured provider checks require provider_kind and no skip reason")
certificate_values = (self.cert_not_after, self.cert_severity, self.cert_days_until_expiry)
declared = tuple(value for value in certificate_values if value is not None)
if declared and len(declared) != len(certificate_values):
raise ValueError("certificate expiry facts must be provided together")
return self
def _validate_skipped_shape(self) -> None:
"""Require a skipped provider check to carry no provider or certificate facts."""
if self.skip_reason is not WorkflowDiagnosticSkipReason.NOT_WIRED:
raise ValueError("skipped provider checks require the not_wired reason")
if any(
value is not None
for value in (
self.provider_kind,
self.cert_not_after,
self.cert_severity,
self.cert_days_until_expiry,
)
):
raise ValueError("skipped provider checks cannot carry certificate facts")
[docs]
class WorkflowPreflightFailedDetails(_WorkflowStepDetail):
"""A preflight failure identified by a stable application error code."""
kind: Literal["preflight_failed"]
error_code: NamespacedId
auth_check: WorkflowAuthCheckDetails | None = None
[docs]
class WorkflowFailureDetails(_WorkflowStepDetail):
"""A non-precondition workflow failure represented without exception prose."""
kind: Literal["workflow_failure"]
error_code: NamespacedId
type WorkflowStepDetails = Annotated[
WorkflowDeadlineContextDetails
| WorkflowInboxSkippedDetails
| WorkflowInboxBlockedDetails
| WorkflowDraftBuiltDetails
| WorkflowAlreadyFiledDetails
| WorkflowDraftNotReadyDetails
| WorkflowDraftMismatchDetails
| WorkflowValidationFailedDetails
| WorkflowAuthCheckDetails
| WorkflowPreflightFailedDetails
| WorkflowFailureDetails,
Field(discriminator="kind"),
]
_PRECONDITION_DETAIL_TYPES = (
WorkflowInboxBlockedDetails,
WorkflowAlreadyFiledDetails,
WorkflowDraftNotReadyDetails,
WorkflowDraftMismatchDetails,
WorkflowValidationFailedDetails,
)
WORKFLOW_SUMMARY_LOCALE_KEYS: tuple[str, ...] = (
"application.workflow.results.aborted",
"application.workflow.results.completed",
"application.workflow.steps.already_filed",
"application.workflow.steps.auth_certificate_invalid",
"application.workflow.steps.auth_certificate_load_failed",
"application.workflow.steps.auth_provider_unavailable",
"application.workflow.steps.deadline_absent",
"application.workflow.steps.deadline_closed",
"application.workflow.steps.deadline_future",
"application.workflow.steps.deadline_informational",
"application.workflow.steps.deadline_missing",
"application.workflow.steps.deadline_open",
"application.workflow.steps.deadline_overdue",
"application.workflow.steps.draft_build_failed",
"application.workflow.steps.draft_built",
"application.workflow.steps.draft_identity_mismatch",
"application.workflow.steps.draft_not_ready",
"application.workflow.steps.inbox_blocked",
"application.workflow.steps.inbox_clear",
"application.workflow.steps.inbox_skipped",
"application.workflow.steps.preflight_completed",
"application.workflow.steps.preflight_failed",
"application.workflow.steps.profile_loaded",
"application.workflow.steps.site_unavailable",
"application.workflow.steps.validation_clean",
"application.workflow.steps.validation_failed",
"application.workflow.steps.workflow_failure",
)
"""Every persisted workflow summary locale identity emitted by engine producers.
The ``_LOCALE_KEYS`` suffix makes this closed producer set visible to the
static locale scanner even though a persisted result selects one key at
runtime. Keep it synchronized with the workflow engine, deadline stage, and
recording failure producers.
"""
def _parse_workflow_locale_key(value: object) -> str:
"""Return one stable abstract locale identity, never rendered prose."""
if not isinstance(value, str):
raise ValueError("workflow summary locale key must be a string")
if not re.fullmatch(r"[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+", value):
raise ValueError("workflow summary must carry a stable dotted locale key")
if value not in WORKFLOW_SUMMARY_LOCALE_KEYS:
raise ValueError("workflow summary locale key must be one of the closed workflow producer keys")
return value
type WorkflowLocaleKey = Annotated[str, BeforeValidator(_parse_workflow_locale_key)]
def _evidence_carries_prose(evidence: ConditionEvidence) -> bool:
"""Return whether one evidence record holds rendered or exception prose.
Workflow refusal evidence is a locale-neutral fact map. A prose-shaped key
token, a value carrying whitespace, or a value shaped like a rendered
``SomeError: ...`` line all mark presentation text that must not reach a
durable record.
"""
key_tokens = {token for key in evidence.values for token in re.split(r"[._]", key)}
if key_tokens & _WORKFLOW_PROSE_EVIDENCE_KEY_TOKENS:
return True
string_values = tuple(value for value in evidence.values.values() if isinstance(value, str))
return any(
any(character.isspace() for character in value) or _WORKFLOW_EXCEPTION_TEXT_PATTERN.match(value) is not None
for value in string_values
)
def _reject_prose_precondition_evidence(verdict: PreconditionVerdict) -> None:
"""Refuse a verdict whose evidence would persist rendered or exception prose."""
for evidence in verdict.evidence:
if _evidence_carries_prose(evidence):
raise ValueError("workflow precondition evidence cannot persist rendered or exception prose")
[docs]
class WorkflowStep(BaseModel):
"""A single step in a :class:`WorkflowResult`."""
model_config = _STRICT_FROZEN
stage: WorkflowStage
started_at: datetime
ended_at: datetime | None = None
success: bool | None = None
summary_locale_key: WorkflowLocaleKey
details: WorkflowStepDetails | None = None
precondition_verdict: PreconditionVerdict | None = None
site_health_alert: SiteHealthAlert | None = None
@model_validator(mode="after")
@pydantic_validation_boundary
def _check_timestamps(self) -> WorkflowStep:
if self.ended_at is not None:
if self.ended_at < self.started_at:
raise ValueError(f"ended_at ({self.ended_at}) precedes started_at ({self.started_at})")
if self.success is None:
raise ValueError("completed steps must set success explicitly")
if self.precondition_verdict is not None and self.success is not False:
raise ValueError("precondition verdicts belong only to explicitly failed workflow steps")
if isinstance(self.details, _PRECONDITION_DETAIL_TYPES) and self.precondition_verdict is None:
raise ValueError("refusal detail records require a typed precondition verdict")
if self.precondition_verdict is not None:
_reject_prose_precondition_evidence(self.precondition_verdict)
return self
[docs]
class WorkflowResult(BaseModel):
"""The full result of one :meth:`WorkflowEngine.run_next` invocation."""
model_config = _STRICT_FROZEN
run_id: str = Field(min_length=16, max_length=16)
started_at: datetime
ended_at: datetime
final_stage: WorkflowStage
aborted_reason: WorkflowAbortReason | None = None
obligation: WorkflowObligationFacts | None = None
draft_id: str | None = None
submission_id: str | None = None
steps: tuple[WorkflowStep, ...]
summary_locale_key: WorkflowLocaleKey
summary_details: WorkflowStepDetails | None = None
resumed_from: str | None = None
@model_validator(mode="after")
@pydantic_validation_boundary
def _check_terminal_consistency(self) -> WorkflowResult:
if self.ended_at < self.started_at:
raise ValueError("ended_at precedes started_at")
if self.final_stage not in {WorkflowStage.DONE, WorkflowStage.ABORTED}:
raise ValueError(f"final_stage must be DONE or ABORTED; got {self.final_stage.value}")
if self.final_stage is WorkflowStage.DONE:
if self.aborted_reason is not None:
raise ValueError("DONE results must not carry an aborted_reason")
return self
if self.aborted_reason is None:
raise ValueError("ABORTED results must carry an aborted_reason")
if not self.steps or self.steps[-1].success is not False:
raise ValueError("ABORTED results must end with an explicitly failed workflow step")
if self.steps[-1].precondition_verdict is None:
raise ValueError("ABORTED results must end with a typed precondition verdict")
return self
[docs]
def compute_run_id(
*,
tax_id: str,
modelo: str,
period: Period | None,
started_at: datetime,
) -> str:
"""Return a stable 16-char hex hash for a workflow run."""
period_segment = period_identity_segment(period) if period is not None else "-"
payload = "|".join([tax_id, modelo, period_segment, started_at.isoformat()])
return sha256_hex(payload.encode("utf-8"))[:16]
__all__ = [
"WORKFLOW_SUMMARY_LOCALE_KEYS",
"SiteHealthAlert",
"WorkflowAlreadyFiledDetails",
"WorkflowAuthCheckDetails",
"WorkflowDeadlineContextDetails",
"WorkflowDeadlineRecoveryFacts",
"WorkflowDiagnosticSkipReason",
"WorkflowDraftBuiltDetails",
"WorkflowDraftMismatchDetails",
"WorkflowDraftNotReadyDetails",
"WorkflowFailureDetails",
"WorkflowInboxBlockedDetails",
"WorkflowInboxSkippedDetails",
"WorkflowLocaleKey",
"WorkflowObligationFacts",
"WorkflowPreflightFailedDetails",
"WorkflowPurpose",
"WorkflowResult",
"WorkflowSiteHealthFacts",
"WorkflowStage",
"WorkflowStep",
"WorkflowStepDetails",
"WorkflowValidationFailedDetails",
"compute_run_id",
]