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