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}