Skip to main content

app/
data_quality_hygiene.rs

1use serde::{Deserialize, Serialize};
2
3use domain::{data_quality, entities, operations, policy, source};
4
5/// Stable Workflow name constant for the data quality hygiene layer.
6pub const WORKFLOW_NAME: &str = "data-quality-hygiene";
7/// Stable Schema version constant for the data quality hygiene layer.
8pub const SCHEMA_VERSION: &str = "data-quality-hygiene-context-v1";
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11/// Issue ref carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
12pub struct IssueRef(String);
13
14impl IssueRef {
15    /// Builds try new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
16    pub fn try_new(value: impl Into<String>) -> Result<Self> {
17        trimmed_non_empty(value, Error::EmptyIssueRef).map(Self)
18    }
19
20    /// Returns the as str source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
27/// Action id carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
28pub struct ActionId(String);
29
30impl ActionId {
31    /// Builds try new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
32    pub fn try_new(value: impl Into<String>) -> Result<Self> {
33        trimmed_non_empty(value, Error::EmptyActionId).map(Self)
34    }
35
36    /// Returns the as str source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
37    pub fn as_str(&self) -> &str {
38        &self.0
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
43/// Context packet id carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
44pub struct ContextPacketId(String);
45
46impl ContextPacketId {
47    /// Builds try new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
48    pub fn try_new(value: impl Into<String>) -> Result<Self> {
49        trimmed_non_empty(value, Error::EmptyContextPacketId).map(Self)
50    }
51
52    /// Returns the as str source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
53    pub fn as_str(&self) -> &str {
54        &self.0
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
59/// Correlation id carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
60pub struct CorrelationId(String);
61
62impl CorrelationId {
63    /// Builds try new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
64    pub fn try_new(value: impl Into<String>) -> Result<Self> {
65        trimmed_non_empty(value, Error::EmptyCorrelationId).map(Self)
66    }
67
68    /// Returns the as str source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
75/// Action rationale carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
76pub struct ActionRationale(String);
77
78impl ActionRationale {
79    /// Builds try new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
80    pub fn try_new(value: impl Into<String>) -> Result<Self> {
81        trimmed_non_empty(value, Error::EmptyActionRationale).map(Self)
82    }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
86/// Labor minutes carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
87pub struct LaborMinutes(u16);
88
89impl LaborMinutes {
90    /// Builds try new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
91    pub const fn try_new(value: u16) -> Result<Self> {
92        if value == 0 {
93            return Err(Error::ZeroLaborMinutes);
94        }
95        Ok(Self(value))
96    }
97
98    /// Returns the get source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
99    pub const fn get(self) -> u16 {
100        self.0
101    }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
105/// Aggregate labor minutes carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
106pub struct AggregateLaborMinutes(u16);
107
108impl AggregateLaborMinutes {
109    /// Builds new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
110    pub const fn new(value: u16) -> Self {
111        Self(value)
112    }
113
114    /// Returns the get source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
115    pub const fn get(self) -> u16 {
116        self.0
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
121/// Decision taxonomy for hygiene persona in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
122pub enum HygienePersona {
123    /// Represents general manager in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
124    GeneralManager,
125    /// Represents assistant general manager in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
126    AssistantGeneralManager,
127    /// Represents front desk lead in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
128    FrontDeskLead,
129    /// Represents front desk agent in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
130    FrontDeskAgent,
131    /// Represents regional operator in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
132    RegionalOperator,
133    /// Represents operations analyst in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
134    OperationsAnalyst,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
138/// Decision taxonomy for candidate kind in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
139pub enum CandidateKind {
140    /// Represents source issue in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
141    SourceIssue,
142    /// Represents duplicate candidate in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
143    DuplicateCandidate,
144    /// Represents profile gap in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
145    ProfileGap,
146    /// Represents service line mapping in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
147    ServiceLineMapping,
148    /// Represents source freshness in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
149    SourceFreshness,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
153/// Decision taxonomy for source freshness in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
154pub enum SourceFreshness {
155    /// Represents current in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
156    Current,
157    /// Represents stale in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
158    Stale,
159    /// Represents conflicting in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
160    Conflicting,
161    /// Represents missing in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
162    Missing,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
166/// Decision taxonomy for sensitivity in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
167pub enum Sensitivity {
168    /// Represents standard operational evidence in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
169    StandardOperationalEvidence,
170    /// Represents vaccine evidence in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
171    VaccineEvidence,
172    /// Represents incident or behavior evidence in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
173    IncidentOrBehaviorEvidence,
174    /// Represents payment evidence in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
175    PaymentEvidence,
176    /// Represents quarantined sensitive payload in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
177    QuarantinedSensitivePayload,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
181/// Candidate carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
182pub struct Candidate {
183    id: IssueRef,
184    kind: CandidateKind,
185    issue: data_quality::Issue,
186    #[builder(default)]
187    source_record_refs: Vec<source::RecordRef>,
188    source_freshness: SourceFreshness,
189    sensitivity: Sensitivity,
190}
191
192impl Candidate {
193    /// Returns the id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
194    pub const fn id(&self) -> &IssueRef {
195        &self.id
196    }
197
198    /// Returns the kind source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
199    pub const fn kind(&self) -> CandidateKind {
200        self.kind
201    }
202
203    /// Returns the issue source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
204    pub const fn issue(&self) -> &data_quality::Issue {
205        &self.issue
206    }
207
208    /// Returns the source record refs source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
209    pub fn source_record_refs(&self) -> &[source::RecordRef] {
210        &self.source_record_refs
211    }
212
213    /// Returns the source freshness source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
214    pub const fn source_freshness(&self) -> SourceFreshness {
215        self.source_freshness
216    }
217
218    /// Returns the sensitivity source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
219    pub const fn sensitivity(&self) -> Sensitivity {
220        self.sensitivity
221    }
222
223    fn effective_source_record_refs(&self) -> Vec<source::RecordRef> {
224        let mut refs = self.source_record_refs.clone();
225        let issue_ref = self.issue.source_record_ref().clone();
226        if !refs.contains(&issue_ref) {
227            refs.push(issue_ref);
228        }
229        refs
230    }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
234/// Decision taxonomy for action kind in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
235pub enum ActionKind {
236    /// Represents investigate missing source evidence in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
237    InvestigateMissingSourceEvidence,
238    /// Represents reconcile duplicate customer or pet candidate in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
239    ReconcileDuplicateCustomerOrPetCandidate,
240    /// Represents complete missing pet or customer profile fields in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
241    CompleteMissingPetOrCustomerProfileFields,
242    /// Represents review stale vaccination source freshness in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
243    ReviewStaleVaccinationSourceFreshness,
244    /// Represents normalize ambiguous service line naming in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
245    NormalizeAmbiguousServiceLineNaming,
246    /// Represents review checkout or unclosed reservation evidence in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
247    ReviewCheckoutOrUnclosedReservationEvidence,
248    /// Represents escalate sensitive or quarantined payload in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
249    EscalateSensitiveOrQuarantinedPayload,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
253/// Decision taxonomy for action priority in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
254pub enum ActionPriority {
255    /// Represents high in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
256    High,
257    /// Represents medium in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
258    Medium,
259    /// Represents low in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
260    Low,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
264/// Decision taxonomy for removed manual work in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
265pub enum RemovedManualWork {
266    /// Represents missing evidence investigation in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
267    MissingEvidenceInvestigation,
268    /// Represents duplicate candidate reconciliation in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
269    DuplicateCandidateReconciliation,
270    /// Represents incomplete profile cleanup preparation in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
271    IncompleteProfileCleanupPreparation,
272    /// Represents source freshness review in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
273    SourceFreshnessReview,
274    /// Represents service line normalization review in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
275    ServiceLineNormalizationReview,
276    /// Represents checkout evidence review in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
277    CheckoutEvidenceReview,
278    /// Represents sensitive payload escalation in the data-quality hygiene decision model so the app can choose the correct evidence, review, or draft path without taking live action.
279    SensitivePayloadEscalation,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
283/// Review-safe agent tasks allowed to save staff time without crossing mutation or send boundaries.
284pub enum SafeAgentAction {
285    /// Allows agents to summarize source evidence for staff review without mutating records or contacting customers.
286    SummarizeSourceEvidence,
287    /// Allows agents to rank hygiene actions for staff review without mutating records or contacting customers.
288    RankHygieneActions,
289    /// Allows agents to draft internal cleanup task for staff review without mutating records or contacting customers.
290    DraftInternalCleanupTask,
291    /// Allows agents to preserve ambiguity for review for staff review without mutating records or contacting customers.
292    PreserveAmbiguityForReview,
293    /// Allows agents to estimate reconciliation minutes saved for staff review without mutating records or contacting customers.
294    EstimateReconciliationMinutesSaved,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
298/// Actions the agent must never perform without a human/operator system of record.
299pub enum BlockedAction {
300    /// Blocks agents from send customer message until staff or the system of record performs the action.
301    SendCustomerMessage,
302    /// Blocks agents from mutate provider or pms record until staff or the system of record performs the action.
303    MutateProviderOrPmsRecord,
304    /// Blocks agents from change staff schedule until staff or the system of record performs the action.
305    ChangeStaffSchedule,
306    /// Blocks agents from move refund discount or payment until staff or the system of record performs the action.
307    MoveRefundDiscountOrPayment,
308    /// Blocks agents from hide or auto resolve source ambiguity until staff or the system of record performs the action.
309    HideOrAutoResolveSourceAmbiguity,
310    /// Blocks agents from expose quarantined sensitive payload until staff or the system of record performs the action.
311    ExposeQuarantinedSensitivePayload,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315/// Labor impact estimate carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
316pub struct LaborImpactEstimate {
317    before_minutes: LaborMinutes,
318    after_minutes: LaborMinutes,
319}
320
321impl LaborImpactEstimate {
322    /// Builds new for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
323    pub const fn new(before_minutes: LaborMinutes, after_minutes: LaborMinutes) -> Self {
324        Self {
325            before_minutes,
326            after_minutes,
327        }
328    }
329
330    /// Returns the before minutes source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
331    pub const fn before_minutes(&self) -> LaborMinutes {
332        self.before_minutes
333    }
334
335    /// Returns the after minutes source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
336    pub const fn after_minutes(&self) -> LaborMinutes {
337        self.after_minutes
338    }
339
340    /// Returns the minutes saved source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
341    pub const fn minutes_saved(&self) -> u16 {
342        self.before_minutes.0.saturating_sub(self.after_minutes.0)
343    }
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
347/// Action carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
348pub struct Action {
349    id: ActionId,
350    kind: ActionKind,
351    priority: ActionPriority,
352    owner_persona: HygienePersona,
353    removed_manual_work: RemovedManualWork,
354    rationale: ActionRationale,
355    #[builder(default)]
356    source_record_refs: Vec<source::RecordRef>,
357    #[builder(default)]
358    issue_refs: Vec<IssueRef>,
359    #[builder(default)]
360    required_review_gates: Vec<policy::ReviewGate>,
361    labor_impact: LaborImpactEstimate,
362}
363
364impl Action {
365    /// Returns the id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
366    pub const fn id(&self) -> &ActionId {
367        &self.id
368    }
369
370    /// Returns the kind source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
371    pub const fn kind(&self) -> ActionKind {
372        self.kind
373    }
374
375    /// Returns the priority source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
376    pub const fn priority(&self) -> ActionPriority {
377        self.priority
378    }
379
380    /// Returns the owner persona source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
381    pub const fn owner_persona(&self) -> HygienePersona {
382        self.owner_persona
383    }
384
385    /// Returns the removed manual work source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
386    pub const fn removed_manual_work(&self) -> RemovedManualWork {
387        self.removed_manual_work
388    }
389
390    /// Returns the rationale source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
391    pub const fn rationale(&self) -> &ActionRationale {
392        &self.rationale
393    }
394
395    /// Returns the source record refs source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
396    pub fn source_record_refs(&self) -> &[source::RecordRef] {
397        &self.source_record_refs
398    }
399
400    /// Returns the issue refs source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
401    pub fn issue_refs(&self) -> &[IssueRef] {
402        &self.issue_refs
403    }
404
405    /// Returns the required review gates source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
406    pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
407        &self.required_review_gates
408    }
409
410    /// Returns the labor impact source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
411    pub const fn labor_impact(&self) -> &LaborImpactEstimate {
412        &self.labor_impact
413    }
414
415    /// Reports whether the data-quality hygiene workflow satisfies the is source grounded safety condition.
416    pub fn is_source_grounded(&self) -> bool {
417        !self.source_record_refs.is_empty() && !self.issue_refs.is_empty()
418    }
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
422/// Input contract for building the workflow packet from source-grounded records.
423pub struct Request {
424    location_id: entities::LocationId,
425    operating_day: operations::operating_day::Date,
426    prepared_for: HygienePersona,
427    #[builder(default)]
428    candidates: Vec<Candidate>,
429}
430
431impl Request {
432    /// Returns the location id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
433    pub const fn location_id(&self) -> entities::LocationId {
434        self.location_id
435    }
436
437    /// Returns the operating day source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
438    pub const fn operating_day(&self) -> operations::operating_day::Date {
439        self.operating_day
440    }
441
442    /// Returns the prepared for source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
443    pub const fn prepared_for(&self) -> HygienePersona {
444        self.prepared_for
445    }
446
447    /// Returns the candidates source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
448    pub fn candidates(&self) -> &[Candidate] {
449        &self.candidates
450    }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454/// Reviewable packet handed to staff or agents with deterministic gates already applied.
455pub struct Packet {
456    workflow: &'static str,
457    schema_version: &'static str,
458    context_packet_id: ContextPacketId,
459    correlation_id: CorrelationId,
460    location_id: entities::LocationId,
461    operating_day: operations::operating_day::Date,
462    prepared_for: HygienePersona,
463    candidates: Vec<Candidate>,
464    actions: Vec<Action>,
465    safe_agent_actions: Vec<SafeAgentAction>,
466    blocked_actions: Vec<BlockedAction>,
467    before_minutes: AggregateLaborMinutes,
468    after_minutes: AggregateLaborMinutes,
469}
470
471impl Packet {
472    /// Returns the workflow source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
473    pub const fn workflow(&self) -> &'static str {
474        self.workflow
475    }
476
477    /// Returns the schema version source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
478    pub const fn schema_version(&self) -> &'static str {
479        self.schema_version
480    }
481
482    /// Returns the context packet id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
483    pub const fn context_packet_id(&self) -> &ContextPacketId {
484        &self.context_packet_id
485    }
486
487    /// Returns the correlation id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
488    pub const fn correlation_id(&self) -> &CorrelationId {
489        &self.correlation_id
490    }
491
492    /// Returns the location id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
493    pub const fn location_id(&self) -> entities::LocationId {
494        self.location_id
495    }
496
497    /// Returns the operating day source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
498    pub const fn operating_day(&self) -> operations::operating_day::Date {
499        self.operating_day
500    }
501
502    /// Returns the prepared for source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
503    pub const fn prepared_for(&self) -> HygienePersona {
504        self.prepared_for
505    }
506
507    /// Returns the candidates source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
508    pub fn candidates(&self) -> &[Candidate] {
509        &self.candidates
510    }
511
512    /// Returns the actions source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
513    pub fn actions(&self) -> &[Action] {
514        &self.actions
515    }
516
517    /// Returns the safe agent actions source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
518    pub fn safe_agent_actions(&self) -> &[SafeAgentAction] {
519        &self.safe_agent_actions
520    }
521
522    /// Returns the blocked actions source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
523    pub fn blocked_actions(&self) -> &[BlockedAction] {
524        &self.blocked_actions
525    }
526
527    /// Returns the before minutes source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
528    pub const fn before_minutes(&self) -> AggregateLaborMinutes {
529        self.before_minutes
530    }
531
532    /// Returns the after minutes source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
533    pub const fn after_minutes(&self) -> AggregateLaborMinutes {
534        self.after_minutes
535    }
536
537    /// Returns the minutes saved source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
538    pub const fn minutes_saved(&self) -> u16 {
539        self.before_minutes.0.saturating_sub(self.after_minutes.0)
540    }
541
542    /// Returns the all actions are source grounded source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
543    pub fn all_actions_are_source_grounded(&self) -> bool {
544        self.actions.iter().all(Action::is_source_grounded)
545    }
546
547    /// Returns the validate draft source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
548    pub fn validate_draft(&self, draft: &DraftSubmission) -> DraftValidation {
549        let mut rejection_reasons = Vec::new();
550
551        if draft.context_packet_id != self.context_packet_id
552            || draft.correlation_id != self.correlation_id
553        {
554            rejection_reasons.push(DraftRejectionReason::StaleOrUnknownContextPacket);
555        }
556
557        for action in &draft.actions {
558            validate_draft_action(self, action, &mut rejection_reasons);
559        }
560
561        rejection_reasons.dedup();
562        DraftValidation { rejection_reasons }
563    }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567/// Draft action carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
568pub struct DraftAction {
569    action_id: ActionId,
570    kind: ActionKind,
571    source_record_refs: Vec<source::RecordRef>,
572    issue_refs: Vec<IssueRef>,
573    required_review_gates: Vec<policy::ReviewGate>,
574    requested_side_effects: Vec<String>,
575    attempted_ambiguity_resolution: bool,
576}
577
578impl DraftAction {
579    /// Builds from action for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
580    pub fn from_action(action: Action) -> Self {
581        Self {
582            action_id: action.id,
583            kind: action.kind,
584            source_record_refs: action.source_record_refs,
585            issue_refs: action.issue_refs,
586            required_review_gates: action.required_review_gates,
587            requested_side_effects: Vec::new(),
588            attempted_ambiguity_resolution: false,
589        }
590    }
591
592    /// Returns the with requested side effect source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
593    pub fn with_requested_side_effect(mut self, side_effect: impl Into<String>) -> Self {
594        self.requested_side_effects.push(side_effect.into());
595        self
596    }
597
598    /// Returns the with attempted ambiguity resolution source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
599    pub const fn with_attempted_ambiguity_resolution(mut self) -> Self {
600        self.attempted_ambiguity_resolution = true;
601        self
602    }
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
606/// Draft submission carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
607pub struct DraftSubmission {
608    context_packet_id: ContextPacketId,
609    correlation_id: CorrelationId,
610    #[builder(default)]
611    actions: Vec<DraftAction>,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615/// Draft validation carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
616pub struct DraftValidation {
617    rejection_reasons: Vec<DraftRejectionReason>,
618}
619
620impl DraftValidation {
621    /// Reports whether the data-quality hygiene workflow satisfies the is accepted safety condition.
622    pub fn is_accepted(&self) -> bool {
623        self.rejection_reasons.is_empty()
624    }
625
626    /// Returns the rejection reasons source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
627    pub fn rejection_reasons(&self) -> &[DraftRejectionReason] {
628        &self.rejection_reasons
629    }
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
633/// Decision taxonomy for draft rejection reason in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
634pub enum DraftRejectionReason {
635    /// Uses stale or unknown context packet as source-grounded evidence for the deterministic decision.
636    StaleOrUnknownContextPacket,
637    /// Uses unsupported action kind as source-grounded evidence for the deterministic decision.
638    UnsupportedActionKind,
639    /// Uses missing source refs as source-grounded evidence for the deterministic decision.
640    MissingSourceRefs,
641    /// Uses source refs not present in context packet as source-grounded evidence for the deterministic decision.
642    SourceRefsNotPresentInContextPacket,
643    /// Uses missing data quality issue refs as source-grounded evidence for the deterministic decision.
644    MissingDataQualityIssueRefs,
645    /// Uses wrong review gate as source-grounded evidence for the deterministic decision.
646    WrongReviewGate,
647    /// Uses blocked side effect requested as source-grounded evidence for the deterministic decision.
648    BlockedSideEffectRequested,
649    /// Uses unsupported side effect requested as source-grounded evidence for the deterministic decision.
650    UnsupportedSideEffectRequested,
651    /// Uses attempted ambiguity hiding as source-grounded evidence for the deterministic decision.
652    AttemptedAmbiguityHiding,
653    /// Uses sensitive payload exposure attempted as source-grounded evidence for the deterministic decision.
654    SensitivePayloadExposureAttempted,
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
658/// Decision taxonomy for feedback outcome in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
659pub enum FeedbackOutcome {
660    /// Records a completed result so follow-up impact is auditable.
661    Completed,
662    /// Records a deferred result so follow-up impact is auditable.
663    Deferred,
664    /// Records a suppressed by manager result so follow-up impact is auditable.
665    SuppressedByManager,
666    /// Records a source fact was wrong result so follow-up impact is auditable.
667    SourceFactWasWrong,
668    /// Records a not actionable result so follow-up impact is auditable.
669    NotActionable,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
673/// Outcome record carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
674pub struct OutcomeRecord {
675    action_id: ActionId,
676    recorded_by: entities::ActorRef,
677    outcome: FeedbackOutcome,
678    before_minutes: LaborMinutes,
679    actual_minutes: LaborMinutes,
680    #[builder(default)]
681    source_record_refs: Vec<source::RecordRef>,
682    #[builder(default)]
683    issue_refs: Vec<IssueRef>,
684    reviewed_resolution_status: Option<data_quality::ResolutionStatus>,
685}
686
687impl OutcomeRecord {
688    /// Returns the action id source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
689    pub const fn action_id(&self) -> &ActionId {
690        &self.action_id
691    }
692
693    /// Returns the recorded by source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
694    pub const fn recorded_by(&self) -> &entities::ActorRef {
695        &self.recorded_by
696    }
697
698    /// Returns the outcome source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
699    pub const fn outcome(&self) -> FeedbackOutcome {
700        self.outcome
701    }
702
703    /// Returns the before minutes source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
704    pub const fn before_minutes(&self) -> LaborMinutes {
705        self.before_minutes
706    }
707
708    /// Returns the actual minutes source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
709    pub const fn actual_minutes(&self) -> LaborMinutes {
710        self.actual_minutes
711    }
712
713    /// Returns the actual minutes saved source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
714    pub const fn actual_minutes_saved(&self) -> u16 {
715        self.before_minutes.0.saturating_sub(self.actual_minutes.0)
716    }
717
718    /// Returns the source record refs source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
719    pub fn source_record_refs(&self) -> &[source::RecordRef] {
720        &self.source_record_refs
721    }
722
723    /// Returns the issue refs source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
724    pub fn issue_refs(&self) -> &[IssueRef] {
725        &self.issue_refs
726    }
727
728    /// Returns the reviewed resolution status source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
729    pub const fn reviewed_resolution_status(&self) -> Option<data_quality::ResolutionStatus> {
730        self.reviewed_resolution_status
731    }
732
733    /// Returns the records feedback without external mutation source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
734    pub fn records_feedback_without_external_mutation(&self) -> bool {
735        true
736    }
737
738    /// Returns the blocked actions source evidence carried by this data-quality hygiene workflow artifact without changing provider, customer, payment, or schedule state.
739    pub fn blocked_actions(&self) -> Vec<BlockedAction> {
740        blocked_actions_for()
741    }
742}
743
744#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
745/// Decision taxonomy for error in the data-quality hygiene workflow; each value carries operational meaning for source-grounded routing and review.
746pub enum Error {
747    #[error("issue ref cannot be empty")]
748    /// Identifies empty issue ref as the reason the workflow must stop, retry, or request review.
749    EmptyIssueRef,
750    #[error("action id cannot be empty")]
751    /// Identifies empty action id as the reason the workflow must stop, retry, or request review.
752    EmptyActionId,
753    #[error("context packet id cannot be empty")]
754    /// Identifies empty context packet id as the reason the workflow must stop, retry, or request review.
755    EmptyContextPacketId,
756    #[error("correlation id cannot be empty")]
757    /// Identifies empty correlation id as the reason the workflow must stop, retry, or request review.
758    EmptyCorrelationId,
759    #[error("action rationale cannot be empty")]
760    /// Identifies empty action rationale as the reason the workflow must stop, retry, or request review.
761    EmptyActionRationale,
762    #[error("labor minutes must be greater than zero")]
763    /// Identifies zero labor minutes as the reason the workflow must stop, retry, or request review.
764    ZeroLaborMinutes,
765}
766
767/// Result type returned by fallible data quality hygiene operations.
768pub type Result<T> = std::result::Result<T, Error>;
769
770#[derive(Debug, Clone, Copy, PartialEq, Eq)]
771/// Workflow carried by the data-quality hygiene workflow; it finds duplicate, stale, or inconsistent records while blocking automatic provider-system mutation.
772pub struct Workflow;
773
774impl Workflow {
775    /// Builds evaluate for the data-quality hygiene workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
776    pub fn evaluate(request: Request) -> Packet {
777        let actions = request
778            .candidates
779            .iter()
780            .map(action_for_candidate)
781            .collect::<Vec<_>>();
782        let before_minutes = total_before_minutes(&actions);
783        let after_minutes = total_after_minutes(&actions);
784
785        Packet {
786            workflow: WORKFLOW_NAME,
787            schema_version: SCHEMA_VERSION,
788            context_packet_id: ContextPacketId::try_new(format!(
789                "data-quality-hygiene-context:{:?}:{:?}",
790                request.location_id, request.operating_day
791            ))
792            .expect("formatted context packet id is non-empty"),
793            correlation_id: CorrelationId::try_new(format!(
794                "data-quality-hygiene:{:?}:{:?}",
795                request.location_id, request.operating_day
796            ))
797            .expect("formatted correlation id is non-empty"),
798            location_id: request.location_id,
799            operating_day: request.operating_day,
800            prepared_for: request.prepared_for,
801            candidates: request.candidates,
802            actions,
803            safe_agent_actions: safe_agent_actions_for(),
804            blocked_actions: blocked_actions_for(),
805            before_minutes,
806            after_minutes,
807        }
808    }
809}
810
811fn action_for_candidate(candidate: &Candidate) -> Action {
812    let (kind, owner_persona, removed_manual_work, before, after) = action_shape_for(candidate);
813    Action::builder()
814        .id(
815            ActionId::try_new(format!("dq-action-{}", candidate.id().as_str()))
816                .expect("candidate ids are non-empty"),
817        )
818        .kind(kind)
819        .priority(priority_for(candidate))
820        .owner_persona(owner_persona)
821        .removed_manual_work(removed_manual_work)
822        .rationale(rationale_for(candidate))
823        .source_record_refs(candidate.effective_source_record_refs())
824        .issue_refs(vec![candidate.id.clone()])
825        .required_review_gates(review_gates_for(candidate))
826        .labor_impact(LaborImpactEstimate::new(
827            LaborMinutes::try_new(before).expect("static before minutes are valid"),
828            LaborMinutes::try_new(after).expect("static after minutes are valid"),
829        ))
830        .build()
831}
832
833fn action_shape_for(
834    candidate: &Candidate,
835) -> (ActionKind, HygienePersona, RemovedManualWork, u16, u16) {
836    if candidate.sensitivity == Sensitivity::QuarantinedSensitivePayload {
837        return (
838            ActionKind::EscalateSensitiveOrQuarantinedPayload,
839            HygienePersona::GeneralManager,
840            RemovedManualWork::SensitivePayloadEscalation,
841            20,
842            8,
843        );
844    }
845
846    match candidate.issue.kind() {
847        data_quality::Kind::MissingVaccinationRecord => (
848            ActionKind::ReviewStaleVaccinationSourceFreshness,
849            HygienePersona::FrontDeskLead,
850            RemovedManualWork::SourceFreshnessReview,
851            25,
852            10,
853        ),
854        data_quality::Kind::DuplicateSourceRecord => (
855            ActionKind::ReconcileDuplicateCustomerOrPetCandidate,
856            HygienePersona::GeneralManager,
857            RemovedManualWork::DuplicateCandidateReconciliation,
858            30,
859            12,
860        ),
861        data_quality::Kind::IncompletePetProfile
862        | data_quality::Kind::AmbiguousOwnerPetRelationship => (
863            ActionKind::CompleteMissingPetOrCustomerProfileFields,
864            HygienePersona::FrontDeskLead,
865            RemovedManualWork::IncompleteProfileCleanupPreparation,
866            20,
867            7,
868        ),
869        data_quality::Kind::UnmappedServiceType | data_quality::Kind::LocationScopeAmbiguity => (
870            ActionKind::NormalizeAmbiguousServiceLineNaming,
871            HygienePersona::GeneralManager,
872            RemovedManualWork::ServiceLineNormalizationReview,
873            20,
874            6,
875        ),
876        data_quality::Kind::CheckoutEvidenceMissing | data_quality::Kind::UnclosedReservation => (
877            ActionKind::ReviewCheckoutOrUnclosedReservationEvidence,
878            HygienePersona::FrontDeskLead,
879            RemovedManualWork::CheckoutEvidenceReview,
880            20,
881            8,
882        ),
883        data_quality::Kind::SensitivePayloadQuarantined => (
884            ActionKind::EscalateSensitiveOrQuarantinedPayload,
885            HygienePersona::GeneralManager,
886            RemovedManualWork::SensitivePayloadEscalation,
887            20,
888            8,
889        ),
890        data_quality::Kind::MissingRequiredField { .. }
891        | data_quality::Kind::AssumptionInForce { .. }
892        | data_quality::Kind::UnknownSourceStatus { .. }
893        | data_quality::Kind::ConflictingTimestamps
894        | data_quality::Kind::PaymentStateConflict => (
895            ActionKind::InvestigateMissingSourceEvidence,
896            HygienePersona::FrontDeskLead,
897            RemovedManualWork::MissingEvidenceInvestigation,
898            25,
899            8,
900        ),
901    }
902}
903
904fn priority_for(candidate: &Candidate) -> ActionPriority {
905    match candidate.issue.severity() {
906        data_quality::Severity::Critical | data_quality::Severity::Blocking => ActionPriority::High,
907        data_quality::Severity::Warning => ActionPriority::Medium,
908        data_quality::Severity::Informational => ActionPriority::Low,
909    }
910}
911
912fn rationale_for(candidate: &Candidate) -> ActionRationale {
913    let text = match candidate.issue.kind() {
914        data_quality::Kind::MissingVaccinationRecord => {
915            "Route stale or missing vaccination source evidence to staff review while preserving ambiguity; this workflow does not approve service eligibility or send the customer a message."
916        }
917        data_quality::Kind::DuplicateSourceRecord => {
918            "Prepare a source-grounded duplicate candidate for manager review without merging or mutating provider records."
919        }
920        data_quality::Kind::UnmappedServiceType => {
921            "Prepare ambiguous service-line naming for manager review before reporting or labor automation consumes the source value."
922        }
923        data_quality::Kind::SensitivePayloadQuarantined => {
924            "Escalate quarantined sensitive evidence as metadata only; do not expose raw payload contents to the agent."
925        }
926        _ => {
927            "Prepare a source-grounded internal data-quality hygiene task for human review without hiding ambiguity or mutating source systems."
928        }
929    };
930    ActionRationale::try_new(text).expect("static rationale is valid")
931}
932
933fn review_gates_for(candidate: &Candidate) -> Vec<policy::ReviewGate> {
934    match candidate.issue.kind() {
935        data_quality::Kind::MissingVaccinationRecord => vec![policy::ReviewGate::ManagerApproval],
936        data_quality::Kind::SensitivePayloadQuarantined => {
937            vec![policy::ReviewGate::ManagerApproval]
938        }
939        data_quality::Kind::PaymentStateConflict => vec![
940            policy::ReviewGate::ManagerApproval,
941            policy::ReviewGate::RefundOrDepositException,
942        ],
943        _ if matches!(
944            candidate.issue.severity(),
945            data_quality::Severity::Blocking | data_quality::Severity::Critical
946        ) =>
947        {
948            vec![policy::ReviewGate::ManagerApproval]
949        }
950        _ => vec![policy::ReviewGate::ManagerApproval],
951    }
952}
953
954fn validate_draft_action(
955    packet: &Packet,
956    action: &DraftAction,
957    rejection_reasons: &mut Vec<DraftRejectionReason>,
958) {
959    if action.source_record_refs.is_empty() {
960        rejection_reasons.push(DraftRejectionReason::MissingSourceRefs);
961    }
962    if action.issue_refs.is_empty() {
963        rejection_reasons.push(DraftRejectionReason::MissingDataQualityIssueRefs);
964    }
965    if action.attempted_ambiguity_resolution {
966        rejection_reasons.push(DraftRejectionReason::AttemptedAmbiguityHiding);
967    }
968
969    if action
970        .source_record_refs
971        .iter()
972        .any(|source_ref| !packet_has_source_ref(packet, source_ref))
973    {
974        rejection_reasons.push(DraftRejectionReason::SourceRefsNotPresentInContextPacket);
975    }
976
977    let matching_packet_action = packet.actions.iter().find(|packet_action| {
978        packet_action.id == action.action_id && packet_action.kind == action.kind
979    });
980    match matching_packet_action {
981        Some(packet_action)
982            if packet_action.required_review_gates != action.required_review_gates =>
983        {
984            rejection_reasons.push(DraftRejectionReason::WrongReviewGate);
985        }
986        Some(_) => {}
987        None => rejection_reasons.push(DraftRejectionReason::UnsupportedActionKind),
988    }
989
990    for side_effect in &action.requested_side_effects {
991        match classify_requested_side_effect(side_effect.as_str()) {
992            RequestedSideEffect::KnownBlocked => {
993                rejection_reasons.push(DraftRejectionReason::BlockedSideEffectRequested)
994            }
995            RequestedSideEffect::Unsupported => {
996                rejection_reasons.push(DraftRejectionReason::UnsupportedSideEffectRequested)
997            }
998        }
999    }
1000}
1001
1002fn packet_has_source_ref(packet: &Packet, source_ref: &source::RecordRef) -> bool {
1003    packet
1004        .candidates
1005        .iter()
1006        .flat_map(Candidate::effective_source_record_refs)
1007        .any(|packet_ref| packet_ref == *source_ref)
1008}
1009
1010enum RequestedSideEffect {
1011    KnownBlocked,
1012    Unsupported,
1013}
1014
1015fn classify_requested_side_effect(side_effect: &str) -> RequestedSideEffect {
1016    match side_effect.trim() {
1017        "send_customer_message"
1018        | "mutate_provider_or_pms_record"
1019        | "change_staff_schedule"
1020        | "move_refund_discount_or_payment"
1021        | "hide_or_auto_resolve_source_ambiguity"
1022        | "expose_quarantined_sensitive_payload" => RequestedSideEffect::KnownBlocked,
1023        _ => RequestedSideEffect::Unsupported,
1024    }
1025}
1026
1027fn total_before_minutes(actions: &[Action]) -> AggregateLaborMinutes {
1028    AggregateLaborMinutes::new(
1029        actions
1030            .iter()
1031            .map(|action| action.labor_impact.before_minutes().get())
1032            .sum::<u16>(),
1033    )
1034}
1035
1036fn total_after_minutes(actions: &[Action]) -> AggregateLaborMinutes {
1037    AggregateLaborMinutes::new(
1038        actions
1039            .iter()
1040            .map(|action| action.labor_impact.after_minutes().get())
1041            .sum::<u16>(),
1042    )
1043}
1044
1045fn safe_agent_actions_for() -> Vec<SafeAgentAction> {
1046    vec![
1047        SafeAgentAction::SummarizeSourceEvidence,
1048        SafeAgentAction::RankHygieneActions,
1049        SafeAgentAction::DraftInternalCleanupTask,
1050        SafeAgentAction::PreserveAmbiguityForReview,
1051        SafeAgentAction::EstimateReconciliationMinutesSaved,
1052    ]
1053}
1054
1055fn blocked_actions_for() -> Vec<BlockedAction> {
1056    vec![
1057        BlockedAction::SendCustomerMessage,
1058        BlockedAction::MutateProviderOrPmsRecord,
1059        BlockedAction::ChangeStaffSchedule,
1060        BlockedAction::MoveRefundDiscountOrPayment,
1061        BlockedAction::HideOrAutoResolveSourceAmbiguity,
1062        BlockedAction::ExposeQuarantinedSensitivePayload,
1063    ]
1064}
1065
1066fn trimmed_non_empty(value: impl Into<String>, empty_error: Error) -> Result<String> {
1067    let value = value.into().trim().to_owned();
1068    if value.is_empty() {
1069        Err(empty_error)
1070    } else {
1071        Ok(value)
1072    }
1073}