1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 ExecutionBoundary, FieldDependencyId, FieldId, FormEntity, FormFieldEntity, FormId,
5 FormOwnershipGraph, FormOwnershipNodeKey, SemanticId, SourceProvenance, ValidationGraph,
6 ValidationGraphEdgeKind, ValidationGraphNodeKey, ValidationPlanId, ValidationRule,
7 ValidationRuleId, ValidationRuleKind,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ValidationPlanningStatus {
13 Deferred,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct FieldValidationDependency {
20 pub id: FieldDependencyId,
21 pub plan: ValidationPlanId,
22 pub form: FormId,
23 pub component: SemanticId,
24 pub source_field: FieldId,
25 pub target_field: FieldId,
26 pub dependent_rule: ValidationRuleId,
27 pub rule_kind: ValidationRuleKind,
28 pub execution_boundary: ExecutionBoundary,
29 pub source_field_order: usize,
30 pub target_field_order: usize,
31 pub rule_order: usize,
32 pub provenance: SourceProvenance,
33 pub source_field_provenance: SourceProvenance,
34 pub target_field_provenance: SourceProvenance,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct FieldValidationSourceEntry {
39 pub source_field: FieldId,
40 pub form: FormId,
41 pub authored_field_order: usize,
42 pub directly_invalidated_rules: Vec<ValidationRuleId>,
43 pub directly_invalidated_target_fields: Vec<FieldId>,
44 pub dependencies: Vec<FieldDependencyId>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct FieldValidationTargetEntry {
49 pub target_field: FieldId,
50 pub form: FormId,
51 pub authored_field_order: usize,
52 pub cross_field_rules: Vec<ValidationRuleId>,
53 pub source_fields: Vec<FieldId>,
54 pub dependencies: Vec<FieldDependencyId>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
58pub enum FieldDependencyBlockReason {
59 MissingRule,
60 MissingSourceField,
61 MissingTargetField,
62 MissingForm,
63 RuleNotInValidationGraph,
64 RuleHasNoDependency,
65 RuleCycleExcluded,
66 RuleCandidateOnly,
67 SourceTargetFormMismatch,
68 ComponentMismatch,
69 UnsupportedBoundary,
70 MissingProvenance,
71 IdentityMismatch,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct BlockedFieldValidationDependency {
78 pub rule: Option<ValidationRuleId>,
79 pub source_field: Option<FieldId>,
80 pub target_field: Option<FieldId>,
81 pub form: Option<FormId>,
82 pub reason: FieldDependencyBlockReason,
83 pub provenance: Option<SourceProvenance>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct ValidationDependencyPlanIntegrityDiagnostic {
88 pub code: String,
89 pub kind: ValidationDependencyPlanIntegrityKind,
90 pub message: String,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
94pub enum ValidationDependencyPlanIntegrityKind {
95 DuplicatePlan,
96 MissingPlanForm,
97 UnknownForm,
98 UnknownSourceField,
99 UnknownTargetField,
100 UnknownRule,
101 RuleNotInValidationGraph,
102 MissingI6Dependency,
103 DuplicateDependencyProjection,
104 MissingDependencyProjection,
105 PlanFormMismatch,
106 FieldFormMismatch,
107 RuleTargetMismatch,
108 RuleFormMismatch,
109 ComponentMismatch,
110 SelfDependency,
111 CycleRulePromoted,
112 UnsupportedBoundary,
113 SourceIndexMismatch,
114 TargetIndexMismatch,
115 InvalidationIndexMismatch,
116 DuplicateScheduledRule,
117 DuplicateScheduledTarget,
118 TransitiveInvalidationLeak,
119 CandidateIdentityPromoted,
120 InstanceIdentityLeak,
121 MissingProvenance,
122 NonCanonicalOrdering,
123 PlanIdentityDrift,
124}
125
126impl ValidationDependencyPlanIntegrityKind {
127 #[must_use]
128 pub const fn code(self) -> &'static str {
129 match self {
130 Self::DuplicatePlan => "PSASM1242",
131 Self::MissingPlanForm => "PSASM1243",
132 Self::UnknownForm => "PSASM1244",
133 Self::UnknownSourceField => "PSASM1245",
134 Self::UnknownTargetField => "PSASM1246",
135 Self::UnknownRule => "PSASM1247",
136 Self::RuleNotInValidationGraph => "PSASM1248",
137 Self::MissingI6Dependency => "PSASM1249",
138 Self::DuplicateDependencyProjection => "PSASM1250",
139 Self::MissingDependencyProjection => "PSASM1251",
140 Self::PlanFormMismatch => "PSASM1252",
141 Self::FieldFormMismatch => "PSASM1253",
142 Self::RuleTargetMismatch => "PSASM1254",
143 Self::RuleFormMismatch => "PSASM1255",
144 Self::ComponentMismatch => "PSASM1256",
145 Self::SelfDependency => "PSASM1257",
146 Self::CycleRulePromoted => "PSASM1258",
147 Self::UnsupportedBoundary => "PSASM1259",
148 Self::SourceIndexMismatch => "PSASM1260",
149 Self::TargetIndexMismatch => "PSASM1261",
150 Self::InvalidationIndexMismatch => "PSASM1262",
151 Self::DuplicateScheduledRule => "PSASM1263",
152 Self::DuplicateScheduledTarget => "PSASM1264",
153 Self::TransitiveInvalidationLeak => "PSASM1265",
154 Self::CandidateIdentityPromoted => "PSASM1266",
155 Self::InstanceIdentityLeak => "PSASM1267",
156 Self::MissingProvenance => "PSASM1268",
157 Self::NonCanonicalOrdering => "PSASM1269",
158 Self::PlanIdentityDrift => "PSASM1270",
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ValidationDependencyPlanValidation {
165 pub diagnostics: Vec<ValidationDependencyPlanIntegrityDiagnostic>,
166 pub is_valid: bool,
167}
168
169impl Default for ValidationDependencyPlanValidation {
170 fn default() -> Self {
171 Self {
172 diagnostics: Vec::new(),
173 is_valid: true,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct FormValidationDependencyPlan {
182 pub id: ValidationPlanId,
183 pub form: FormId,
184 pub component: SemanticId,
185 pub fields: Vec<FieldId>,
186 pub dependencies: Vec<FieldDependencyId>,
187 pub source_entries: Vec<FieldValidationSourceEntry>,
188 pub target_entries: Vec<FieldValidationTargetEntry>,
189 pub initial_validation: ValidationPlanningStatus,
190 pub submission_validation: ValidationPlanningStatus,
191 pub validation: ValidationDependencyPlanValidation,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct ValidationDependencyPlans {
198 pub plans: BTreeMap<ValidationPlanId, FormValidationDependencyPlan>,
199 pub dependencies: BTreeMap<FieldDependencyId, FieldValidationDependency>,
200 pub blocked: Vec<BlockedFieldValidationDependency>,
201 pub validation: ValidationDependencyPlanValidation,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct FieldChangeSet {
206 pub form: FormId,
207 pub changed_fields: Vec<FieldId>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct FieldValidationChangePlan {
212 pub plan: ValidationPlanId,
213 pub form: FormId,
214 pub changed_field: FieldId,
215 pub scheduled_rules: Vec<ValidationRuleId>,
216 pub scheduled_target_fields: Vec<FieldId>,
217 pub dependencies: Vec<FieldDependencyId>,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct FieldChangeValidationSchedule {
222 pub plan: ValidationPlanId,
223 pub form: FormId,
224 pub changed_fields: Vec<FieldId>,
225 pub scheduled_rules: Vec<ValidationRuleId>,
226 pub scheduled_target_fields: Vec<FieldId>,
227 pub triggering_dependencies: Vec<FieldDependencyId>,
228}
229
230impl ValidationDependencyPlans {
231 #[must_use]
232 pub fn validation_plan(&self, form: &FormId) -> Option<&FormValidationDependencyPlan> {
233 self.plans.get(&ValidationPlanId::for_form(form))
234 }
235
236 #[must_use]
237 pub fn validation_plan_by_id(
238 &self,
239 id: &ValidationPlanId,
240 ) -> Option<&FormValidationDependencyPlan> {
241 self.plans.get(id)
242 }
243
244 #[must_use]
245 pub fn dependency(&self, id: &FieldDependencyId) -> Option<&FieldValidationDependency> {
246 self.dependencies.get(id)
247 }
248
249 #[must_use]
250 pub fn dependencies_of_plan(&self, plan: &ValidationPlanId) -> Vec<&FieldValidationDependency> {
251 let mut dependencies = self
252 .dependencies
253 .values()
254 .filter(|dependency| &dependency.plan == plan)
255 .collect::<Vec<_>>();
256 dependencies.sort_by(|left, right| dependency_order(left, right));
257 dependencies
258 }
259
260 #[must_use]
261 pub fn source_entry(&self, field: &FieldId) -> Option<&FieldValidationSourceEntry> {
262 self.plans.values().find_map(|plan| {
263 plan.source_entries
264 .iter()
265 .find(|entry| &entry.source_field == field)
266 })
267 }
268
269 #[must_use]
270 pub fn target_entry(&self, field: &FieldId) -> Option<&FieldValidationTargetEntry> {
271 self.plans.values().find_map(|plan| {
272 plan.target_entries
273 .iter()
274 .find(|entry| &entry.target_field == field)
275 })
276 }
277
278 #[must_use]
279 pub fn directly_invalidated_rules(&self, field: &FieldId) -> Vec<&ValidationRuleId> {
280 self.source_entry(field).map_or_else(Vec::new, |entry| {
281 entry.directly_invalidated_rules.iter().collect()
282 })
283 }
284
285 #[must_use]
286 pub fn directly_invalidated_target_fields(&self, field: &FieldId) -> Vec<&FieldId> {
287 self.source_entry(field).map_or_else(Vec::new, |entry| {
288 entry.directly_invalidated_target_fields.iter().collect()
289 })
290 }
291
292 #[must_use]
293 pub fn dependencies_from_field(&self, field: &FieldId) -> Vec<&FieldValidationDependency> {
294 let mut dependencies = self
295 .dependencies
296 .values()
297 .filter(|dependency| &dependency.source_field == field)
298 .collect::<Vec<_>>();
299 dependencies.sort_by(|left, right| dependency_order(left, right));
300 dependencies
301 }
302
303 #[must_use]
304 pub fn dependencies_to_field(&self, field: &FieldId) -> Vec<&FieldValidationDependency> {
305 let mut dependencies = self
306 .dependencies
307 .values()
308 .filter(|dependency| &dependency.target_field == field)
309 .collect::<Vec<_>>();
310 dependencies.sort_by(|left, right| dependency_order(left, right));
311 dependencies
312 }
313
314 #[must_use]
315 pub fn source_field_of_dependency(&self, id: &FieldDependencyId) -> Option<&FieldId> {
316 self.dependency(id)
317 .map(|dependency| &dependency.source_field)
318 }
319
320 #[must_use]
321 pub fn target_field_of_dependency(&self, id: &FieldDependencyId) -> Option<&FieldId> {
322 self.dependency(id)
323 .map(|dependency| &dependency.target_field)
324 }
325
326 #[must_use]
327 pub fn rule_of_dependency(&self, id: &FieldDependencyId) -> Option<&ValidationRuleId> {
328 self.dependency(id)
329 .map(|dependency| &dependency.dependent_rule)
330 }
331
332 #[must_use]
333 pub fn change_plan(&self, field: &FieldId) -> Option<FieldValidationChangePlan> {
334 let plan = self
335 .plans
336 .values()
337 .find(|plan| plan.fields.contains(field))?;
338 let schedule = self.schedule_change_set(&plan.form, std::slice::from_ref(field))?;
339 Some(FieldValidationChangePlan {
340 plan: schedule.plan,
341 form: schedule.form,
342 changed_field: field.clone(),
343 scheduled_rules: schedule.scheduled_rules,
344 scheduled_target_fields: schedule.scheduled_target_fields,
345 dependencies: schedule.triggering_dependencies,
346 })
347 }
348
349 #[must_use]
352 pub fn schedule_change_set(
353 &self,
354 form: &FormId,
355 changed_fields: &[FieldId],
356 ) -> Option<FieldChangeValidationSchedule> {
357 let plan = self.validation_plan(form)?;
358 if changed_fields
359 .iter()
360 .any(|field| !plan.fields.contains(field))
361 {
362 return None;
363 }
364
365 let orders = plan_field_orders(plan);
366 let mut normalized = changed_fields
367 .iter()
368 .cloned()
369 .collect::<BTreeSet<_>>()
370 .into_iter()
371 .collect::<Vec<_>>();
372 normalized.sort_by_key(|field| {
373 (
374 orders.get(field).copied().unwrap_or(usize::MAX),
375 field.clone(),
376 )
377 });
378
379 let triggered = normalized
380 .iter()
381 .flat_map(|field| self.dependencies_from_field(field).into_iter())
382 .collect::<Vec<_>>();
383 let mut dependency_ids = triggered
384 .iter()
385 .map(|dependency| dependency.id.clone())
386 .collect::<Vec<_>>();
387 dependency_ids.sort_by_key(|id| {
388 self.dependencies
389 .get(id)
390 .map(dependency_sort_key)
391 .unwrap_or((usize::MAX, usize::MAX, usize::MAX, id.clone()))
392 });
393
394 let mut rules = triggered
395 .iter()
396 .map(|dependency| dependency.dependent_rule.clone())
397 .collect::<BTreeSet<_>>()
398 .into_iter()
399 .collect::<Vec<_>>();
400 let rule_keys = triggered
401 .iter()
402 .map(|dependency| {
403 (
404 dependency.dependent_rule.clone(),
405 rule_schedule_key(dependency),
406 )
407 })
408 .collect::<BTreeMap<_, _>>();
409 rules.sort_by_key(|rule| {
410 rule_keys
411 .get(rule)
412 .cloned()
413 .unwrap_or((usize::MAX, usize::MAX, rule.clone()))
414 });
415
416 let mut target_fields = triggered
417 .iter()
418 .map(|dependency| dependency.target_field.clone())
419 .collect::<BTreeSet<_>>()
420 .into_iter()
421 .collect::<Vec<_>>();
422 target_fields.sort_by_key(|field| {
423 (
424 orders.get(field).copied().unwrap_or(usize::MAX),
425 field.clone(),
426 )
427 });
428
429 Some(FieldChangeValidationSchedule {
430 plan: plan.id.clone(),
431 form: form.clone(),
432 changed_fields: normalized,
433 scheduled_rules: rules,
434 scheduled_target_fields: target_fields,
435 triggering_dependencies: dependency_ids,
436 })
437 }
438
439 #[must_use]
440 pub fn plans_of_component(&self, component: &SemanticId) -> Vec<&FormValidationDependencyPlan> {
441 self.plans
442 .values()
443 .filter(|plan| &plan.component == component)
444 .collect()
445 }
446
447 #[must_use]
448 pub fn blocked_dependencies(&self) -> &[BlockedFieldValidationDependency] {
449 &self.blocked
450 }
451}
452
453#[must_use]
462pub fn collect_validation_dependency_plans(
463 forms: &BTreeMap<FormId, FormEntity>,
464 fields: &BTreeMap<FieldId, FormFieldEntity>,
465 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
466 form_ownership: &FormOwnershipGraph,
467 validation_graph: &ValidationGraph,
468) -> ValidationDependencyPlans {
469 let mut plans = initialize_validation_plans(forms, fields);
470 let (dependencies, mut blocked) =
471 collect_direct_dependencies(forms, fields, rules, validation_graph);
472 populate_dependency_entries(&mut plans, &dependencies);
473 canonicalize_plans(&mut plans, &dependencies);
474 blocked.sort_by(blocked_order);
475 let mut product = ValidationDependencyPlans {
476 plans,
477 dependencies,
478 blocked,
479 validation: ValidationDependencyPlanValidation::default(),
480 };
481 product.validation = validate_validation_dependency_plans(
482 &product,
483 forms,
484 fields,
485 rules,
486 form_ownership,
487 validation_graph,
488 );
489 for plan in product.plans.values_mut() {
490 plan.validation = product.validation.clone();
491 }
492 product
493}
494
495fn initialize_validation_plans(
496 forms: &BTreeMap<FormId, FormEntity>,
497 fields: &BTreeMap<FieldId, FormFieldEntity>,
498) -> BTreeMap<ValidationPlanId, FormValidationDependencyPlan> {
499 forms
500 .values()
501 .map(|form| {
502 let form_fields = ordered_fields_for_form(fields, &form.id);
503 let plan = FormValidationDependencyPlan {
504 id: ValidationPlanId::for_form(&form.id),
505 form: form.id.clone(),
506 component: form
507 .owner
508 .entity_id()
509 .cloned()
510 .expect("valid Form has a canonical Component owner"),
511 fields: form_fields.iter().map(|field| field.id.clone()).collect(),
512 dependencies: Vec::new(),
513 source_entries: form_fields
514 .iter()
515 .map(|field| FieldValidationSourceEntry {
516 source_field: field.id.clone(),
517 form: form.id.clone(),
518 authored_field_order: field.declaration_order,
519 directly_invalidated_rules: Vec::new(),
520 directly_invalidated_target_fields: Vec::new(),
521 dependencies: Vec::new(),
522 })
523 .collect(),
524 target_entries: form_fields
525 .iter()
526 .map(|field| FieldValidationTargetEntry {
527 target_field: field.id.clone(),
528 form: form.id.clone(),
529 authored_field_order: field.declaration_order,
530 cross_field_rules: Vec::new(),
531 source_fields: Vec::new(),
532 dependencies: Vec::new(),
533 })
534 .collect(),
535 initial_validation: ValidationPlanningStatus::Deferred,
536 submission_validation: ValidationPlanningStatus::Deferred,
537 validation: ValidationDependencyPlanValidation::default(),
538 };
539 (plan.id.clone(), plan)
540 })
541 .collect()
542}
543
544fn collect_direct_dependencies(
545 forms: &BTreeMap<FormId, FormEntity>,
546 fields: &BTreeMap<FieldId, FormFieldEntity>,
547 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
548 validation_graph: &ValidationGraph,
549) -> (
550 BTreeMap<FieldDependencyId, FieldValidationDependency>,
551 Vec<BlockedFieldValidationDependency>,
552) {
553 let mut dependencies = BTreeMap::new();
554 let mut blocked = Vec::new();
555 for rule in rules.values() {
556 let Some(source_id) = rule.dependency.clone() else {
557 continue;
558 };
559 match project_rule_dependency(rule, &source_id, forms, fields, validation_graph) {
560 Ok(dependency) => {
561 if dependencies
562 .insert(dependency.id.clone(), dependency)
563 .is_some()
564 {
565 blocked.push(blocked_for_rule(
566 rule,
567 Some(source_id),
568 FieldDependencyBlockReason::IdentityMismatch,
569 None,
570 ));
571 }
572 }
573 Err(blocked_dependency) => blocked.push(*blocked_dependency),
574 }
575 }
576 (dependencies, blocked)
577}
578
579fn project_rule_dependency(
580 rule: &ValidationRule,
581 source_id: &FieldId,
582 forms: &BTreeMap<FormId, FormEntity>,
583 fields: &BTreeMap<FieldId, FormFieldEntity>,
584 validation_graph: &ValidationGraph,
585) -> Result<FieldValidationDependency, Box<BlockedFieldValidationDependency>> {
586 let Some(source) = fields.get(source_id) else {
587 return Err(Box::new(blocked_for_rule(
588 rule,
589 Some(source_id.clone()),
590 FieldDependencyBlockReason::MissingSourceField,
591 None,
592 )));
593 };
594 let Some(target) = fields.get(&rule.target_field) else {
595 return Err(Box::new(blocked_for_rule(
596 rule,
597 Some(source.id.clone()),
598 FieldDependencyBlockReason::MissingTargetField,
599 None,
600 )));
601 };
602 let Some(form) = forms.get(&rule.owner_form) else {
603 return Err(Box::new(blocked_for_rule(
604 rule,
605 Some(source.id.clone()),
606 FieldDependencyBlockReason::MissingForm,
607 None,
608 )));
609 };
610 let edges = matching_i6_edges(validation_graph, rule, source);
611 if let Some(reason) =
612 dependency_block_reason(rule, source, target, form, validation_graph, &edges)
613 {
614 return Err(Box::new(blocked_for_rule(
615 rule,
616 Some(source.id.clone()),
617 reason,
618 edges.first(),
619 )));
620 }
621 let id = FieldDependencyId::for_rule_and_source(&rule.id, &source.id);
622 Ok(FieldValidationDependency {
623 id,
624 plan: ValidationPlanId::for_form(&rule.owner_form),
625 form: rule.owner_form.clone(),
626 component: rule.owner_component.clone(),
627 source_field: source.id.clone(),
628 target_field: target.id.clone(),
629 dependent_rule: rule.id.clone(),
630 rule_kind: rule.kind,
631 execution_boundary: rule.boundary,
632 source_field_order: source.declaration_order,
633 target_field_order: target.declaration_order,
634 rule_order: rule.rule_authored_order,
635 provenance: edges[0].provenance.clone(),
636 source_field_provenance: source.provenance.clone(),
637 target_field_provenance: target.provenance.clone(),
638 })
639}
640
641fn matching_i6_edges<'a>(
642 graph: &'a ValidationGraph,
643 rule: &ValidationRule,
644 source: &FormFieldEntity,
645) -> Vec<&'a crate::ValidationGraphEdge> {
646 graph
647 .edges
648 .iter()
649 .filter(|edge| {
650 edge.kind == ValidationGraphEdgeKind::RuleDependsOnField
651 && edge.source == ValidationGraphNodeKey::ValidationRule(rule.id.clone())
652 && edge.target == ValidationGraphNodeKey::FormField(source.id.clone())
653 })
654 .collect()
655}
656
657fn dependency_block_reason(
658 rule: &ValidationRule,
659 source: &FormFieldEntity,
660 target: &FormFieldEntity,
661 form: &FormEntity,
662 graph: &ValidationGraph,
663 edges: &[&crate::ValidationGraphEdge],
664) -> Option<FieldDependencyBlockReason> {
665 if !graph
666 .nodes
667 .contains_key(&ValidationGraphNodeKey::ValidationRule(rule.id.clone()))
668 {
669 return Some(FieldDependencyBlockReason::RuleNotInValidationGraph);
670 }
671 if edges.len() != 1 || source.id == target.id {
672 return Some(FieldDependencyBlockReason::IdentityMismatch);
673 }
674 if graph
675 .cycles
676 .iter()
677 .any(|cycle| cycle.candidates.contains(&rule.candidate_id))
678 {
679 return Some(FieldDependencyBlockReason::RuleCycleExcluded);
680 }
681 if source.owner_form != rule.owner_form || target.owner_form != rule.owner_form {
682 return Some(FieldDependencyBlockReason::SourceTargetFormMismatch);
683 }
684 if source.owner_component != rule.owner_component
685 || target.owner_component != rule.owner_component
686 || form.owner.entity_id() != Some(&rule.owner_component)
687 {
688 return Some(FieldDependencyBlockReason::ComponentMismatch);
689 }
690 if rule.boundary != ExecutionBoundary::Client {
691 return Some(FieldDependencyBlockReason::UnsupportedBoundary);
692 }
693 (!has_provenance(&rule.provenance)
694 || !has_provenance(&source.provenance)
695 || !has_provenance(&target.provenance)
696 || !has_provenance(&edges[0].provenance))
697 .then_some(FieldDependencyBlockReason::MissingProvenance)
698}
699
700fn populate_dependency_entries(
701 plans: &mut BTreeMap<ValidationPlanId, FormValidationDependencyPlan>,
702 dependencies: &BTreeMap<FieldDependencyId, FieldValidationDependency>,
703) {
704 for dependency in dependencies.values() {
705 let plan = plans
706 .get_mut(&dependency.plan)
707 .expect("a valid dependency has a canonical Form plan");
708 plan.dependencies.push(dependency.id.clone());
709 let source = plan
710 .source_entries
711 .iter_mut()
712 .find(|entry| entry.source_field == dependency.source_field)
713 .expect("dependency source belongs to its plan");
714 source.dependencies.push(dependency.id.clone());
715 source
716 .directly_invalidated_rules
717 .push(dependency.dependent_rule.clone());
718 source
719 .directly_invalidated_target_fields
720 .push(dependency.target_field.clone());
721 let target = plan
722 .target_entries
723 .iter_mut()
724 .find(|entry| entry.target_field == dependency.target_field)
725 .expect("dependency target belongs to its plan");
726 target.dependencies.push(dependency.id.clone());
727 target
728 .cross_field_rules
729 .push(dependency.dependent_rule.clone());
730 target.source_fields.push(dependency.source_field.clone());
731 }
732}
733
734#[allow(clippy::too_many_lines)]
735#[must_use]
736pub fn validate_validation_dependency_plans(
737 products: &ValidationDependencyPlans,
738 forms: &BTreeMap<FormId, FormEntity>,
739 fields: &BTreeMap<FieldId, FormFieldEntity>,
740 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
741 form_ownership: &FormOwnershipGraph,
742 validation_graph: &ValidationGraph,
743) -> ValidationDependencyPlanValidation {
744 let mut diagnostics = Vec::new();
745 let expected_plan_ids = forms
746 .keys()
747 .map(ValidationPlanId::for_form)
748 .collect::<BTreeSet<_>>();
749 let actual_plan_ids = products.plans.keys().cloned().collect::<BTreeSet<_>>();
750 if expected_plan_ids != actual_plan_ids {
751 push_integrity(
752 &mut diagnostics,
753 ValidationDependencyPlanIntegrityKind::DuplicatePlan,
754 "Validation Plans do not contain exactly one canonical plan per Form",
755 );
756 }
757
758 for plan in products.plans.values() {
759 if plan.id != ValidationPlanId::for_form(&plan.form) {
760 push_integrity(
761 &mut diagnostics,
762 ValidationDependencyPlanIntegrityKind::PlanIdentityDrift,
763 "Validation Plan identity does not derive from its Form",
764 );
765 }
766 let Some(form) = forms.get(&plan.form) else {
767 push_integrity(
768 &mut diagnostics,
769 ValidationDependencyPlanIntegrityKind::UnknownForm,
770 "Validation Plan references an unknown Form",
771 );
772 continue;
773 };
774 if form.owner.entity_id() != Some(&plan.component) {
775 push_integrity(
776 &mut diagnostics,
777 ValidationDependencyPlanIntegrityKind::ComponentMismatch,
778 "Validation Plan component does not match its Form owner",
779 );
780 }
781 let expected_fields = ordered_fields_for_form(fields, &plan.form)
782 .into_iter()
783 .map(|field| field.id.clone())
784 .collect::<Vec<_>>();
785 if plan.fields != expected_fields {
786 push_integrity(
787 &mut diagnostics,
788 ValidationDependencyPlanIntegrityKind::NonCanonicalOrdering,
789 "Validation Plan Fields do not preserve I3 authored Field order",
790 );
791 }
792 if plan.source_entries.len() != expected_fields.len()
793 || plan.target_entries.len() != expected_fields.len()
794 {
795 push_integrity(
796 &mut diagnostics,
797 ValidationDependencyPlanIntegrityKind::SourceIndexMismatch,
798 "Validation Plan must retain one source and target entry per Field",
799 );
800 }
801 for field in &expected_fields {
802 if !plan
803 .source_entries
804 .iter()
805 .any(|entry| &entry.source_field == field)
806 {
807 push_integrity(
808 &mut diagnostics,
809 ValidationDependencyPlanIntegrityKind::SourceIndexMismatch,
810 "Validation Plan is missing a source Field entry",
811 );
812 }
813 if !plan
814 .target_entries
815 .iter()
816 .any(|entry| &entry.target_field == field)
817 {
818 push_integrity(
819 &mut diagnostics,
820 ValidationDependencyPlanIntegrityKind::TargetIndexMismatch,
821 "Validation Plan is missing a target Field entry",
822 );
823 }
824 }
825 }
826
827 let mut pairs = BTreeSet::new();
828 for dependency in products.dependencies.values() {
829 if !pairs.insert((
830 dependency.dependent_rule.clone(),
831 dependency.source_field.clone(),
832 )) {
833 push_integrity(
834 &mut diagnostics,
835 ValidationDependencyPlanIntegrityKind::DuplicateDependencyProjection,
836 "multiple I7 dependencies share one Rule/source Field tuple",
837 );
838 }
839 validate_dependency(
840 dependency,
841 products,
842 forms,
843 fields,
844 rules,
845 form_ownership,
846 validation_graph,
847 &mut diagnostics,
848 );
849 }
850
851 for rule in rules.values() {
852 let Some(source) = rule.dependency.as_ref() else {
853 continue;
854 };
855 let exact_edges = validation_graph
856 .edges
857 .iter()
858 .filter(|edge| {
859 edge.kind == ValidationGraphEdgeKind::RuleDependsOnField
860 && edge.source == ValidationGraphNodeKey::ValidationRule(rule.id.clone())
861 && edge.target == ValidationGraphNodeKey::FormField(source.clone())
862 })
863 .count();
864 if exact_edges == 0 {
865 push_integrity(
866 &mut diagnostics,
867 ValidationDependencyPlanIntegrityKind::MissingI6Dependency,
868 "valid I6 cross-Field Rule has no exact RuleDependsOnField edge",
869 );
870 } else if exact_edges > 1 {
871 push_integrity(
872 &mut diagnostics,
873 ValidationDependencyPlanIntegrityKind::DuplicateDependencyProjection,
874 "I6 cross-Field Rule has multiple exact dependency edges",
875 );
876 } else if !products
877 .dependencies
878 .contains_key(&FieldDependencyId::for_rule_and_source(&rule.id, source))
879 {
880 push_integrity(
881 &mut diagnostics,
882 ValidationDependencyPlanIntegrityKind::MissingDependencyProjection,
883 "valid I6 cross-Field edge is absent from I7 planning",
884 );
885 }
886 }
887 for edge in validation_graph
888 .edges
889 .iter()
890 .filter(|edge| edge.kind == ValidationGraphEdgeKind::RuleDependsOnField)
891 {
892 let (
893 ValidationGraphNodeKey::ValidationRule(rule),
894 ValidationGraphNodeKey::FormField(source),
895 ) = (&edge.source, &edge.target)
896 else {
897 push_integrity(
898 &mut diagnostics,
899 ValidationDependencyPlanIntegrityKind::RuleNotInValidationGraph,
900 "I6 dependency edge does not have Rule-to-Field endpoints",
901 );
902 continue;
903 };
904 if let Some(rule_record) = rules.get(rule) {
905 if rule_record.dependency.as_ref() != Some(source) {
906 push_integrity(
907 &mut diagnostics,
908 ValidationDependencyPlanIntegrityKind::RuleTargetMismatch,
909 "I6 dependency edge does not match its Rule dependency Field",
910 );
911 }
912 } else {
913 push_integrity(
914 &mut diagnostics,
915 ValidationDependencyPlanIntegrityKind::UnknownRule,
916 "I6 dependency edge references an unknown Rule",
917 );
918 }
919 }
920
921 validate_entry_reciprocity(products, &mut diagnostics);
922 validate_product_ordering(products, &mut diagnostics);
923 diagnostics
924 .sort_by(|left, right| (&left.code, &left.message).cmp(&(&right.code, &right.message)));
925 ValidationDependencyPlanValidation {
926 is_valid: diagnostics.is_empty(),
927 diagnostics,
928 }
929}
930
931#[allow(clippy::too_many_arguments)]
932fn validate_dependency(
933 dependency: &FieldValidationDependency,
934 products: &ValidationDependencyPlans,
935 forms: &BTreeMap<FormId, FormEntity>,
936 fields: &BTreeMap<FieldId, FormFieldEntity>,
937 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
938 form_ownership: &FormOwnershipGraph,
939 validation_graph: &ValidationGraph,
940 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
941) {
942 let Some(plan) = products.plans.get(&dependency.plan) else {
943 push_integrity(
944 diagnostics,
945 ValidationDependencyPlanIntegrityKind::MissingPlanForm,
946 "Field dependency references a missing Validation Plan",
947 );
948 return;
949 };
950 let Some(source) = fields.get(&dependency.source_field) else {
951 push_integrity(
952 diagnostics,
953 ValidationDependencyPlanIntegrityKind::UnknownSourceField,
954 "Field dependency references an unknown source Field",
955 );
956 return;
957 };
958 let Some(target) = fields.get(&dependency.target_field) else {
959 push_integrity(
960 diagnostics,
961 ValidationDependencyPlanIntegrityKind::UnknownTargetField,
962 "Field dependency references an unknown target Field",
963 );
964 return;
965 };
966 let Some(rule) = rules.get(&dependency.dependent_rule) else {
967 push_integrity(
968 diagnostics,
969 ValidationDependencyPlanIntegrityKind::UnknownRule,
970 "Field dependency references an unknown Validation Rule",
971 );
972 return;
973 };
974 validate_dependency_scope(dependency, plan, source, target, rule, diagnostics);
975 validate_dependency_backing(
976 dependency,
977 source,
978 rule,
979 forms,
980 form_ownership,
981 validation_graph,
982 diagnostics,
983 );
984 validate_dependency_provenance(dependency, diagnostics);
985}
986
987fn validate_dependency_scope(
988 dependency: &FieldValidationDependency,
989 plan: &FormValidationDependencyPlan,
990 source: &FormFieldEntity,
991 target: &FormFieldEntity,
992 rule: &ValidationRule,
993 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
994) {
995 if dependency.id != FieldDependencyId::for_rule_and_source(&rule.id, &source.id) {
996 push_integrity(
997 diagnostics,
998 ValidationDependencyPlanIntegrityKind::PlanIdentityDrift,
999 "Field dependency identity does not derive from Rule and source Field",
1000 );
1001 }
1002 if dependency.form != plan.form
1003 || dependency.form != source.owner_form
1004 || dependency.form != target.owner_form
1005 {
1006 push_integrity(
1007 diagnostics,
1008 ValidationDependencyPlanIntegrityKind::PlanFormMismatch,
1009 "Field dependency source, target, and plan do not share one Form",
1010 );
1011 }
1012 if dependency.form != rule.owner_form {
1013 push_integrity(
1014 diagnostics,
1015 ValidationDependencyPlanIntegrityKind::RuleFormMismatch,
1016 "Field dependency Rule does not belong to its Form",
1017 );
1018 }
1019 if dependency.component != source.owner_component
1020 || dependency.component != target.owner_component
1021 || dependency.component != rule.owner_component
1022 {
1023 push_integrity(
1024 diagnostics,
1025 ValidationDependencyPlanIntegrityKind::ComponentMismatch,
1026 "Field dependency does not share one Component owner",
1027 );
1028 }
1029 if source.id == target.id {
1030 push_integrity(
1031 diagnostics,
1032 ValidationDependencyPlanIntegrityKind::SelfDependency,
1033 "Field dependency cannot read its target Field",
1034 );
1035 }
1036 if dependency.target_field != rule.target_field {
1037 push_integrity(
1038 diagnostics,
1039 ValidationDependencyPlanIntegrityKind::RuleTargetMismatch,
1040 "Field dependency target does not match its Rule target",
1041 );
1042 }
1043 if rule.dependency.as_ref() != Some(&dependency.source_field) {
1044 push_integrity(
1045 diagnostics,
1046 ValidationDependencyPlanIntegrityKind::MissingI6Dependency,
1047 "Field dependency does not match its I6 Rule dependency",
1048 );
1049 }
1050 if dependency.execution_boundary != ExecutionBoundary::Client
1051 || rule.boundary != ExecutionBoundary::Client
1052 {
1053 push_integrity(
1054 diagnostics,
1055 ValidationDependencyPlanIntegrityKind::UnsupportedBoundary,
1056 "I7 plans accept only Client-boundary Rules",
1057 );
1058 }
1059}
1060
1061fn validate_dependency_backing(
1062 dependency: &FieldValidationDependency,
1063 source: &FormFieldEntity,
1064 rule: &ValidationRule,
1065 forms: &BTreeMap<FormId, FormEntity>,
1066 form_ownership: &FormOwnershipGraph,
1067 validation_graph: &ValidationGraph,
1068 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
1069) {
1070 if validation_graph
1071 .cycles
1072 .iter()
1073 .any(|cycle| cycle.candidates.contains(&rule.candidate_id))
1074 {
1075 push_integrity(
1076 diagnostics,
1077 ValidationDependencyPlanIntegrityKind::CycleRulePromoted,
1078 "cycle-excluded I6 Rule entered I7 planning",
1079 );
1080 }
1081 if !validation_graph
1082 .nodes
1083 .contains_key(&ValidationGraphNodeKey::ValidationRule(rule.id.clone()))
1084 || matching_i6_edges(validation_graph, rule, source).len() != 1
1085 {
1086 push_integrity(
1087 diagnostics,
1088 ValidationDependencyPlanIntegrityKind::RuleNotInValidationGraph,
1089 "I7 dependency is not backed by an exact I6 Validation Graph edge",
1090 );
1091 }
1092 let owns_field = |field: &FieldId| {
1093 form_ownership.ownership_edges.iter().any(|edge| {
1094 edge.owner == FormOwnershipNodeKey::Form(dependency.form.clone())
1095 && edge.child == FormOwnershipNodeKey::FormField(field.clone())
1096 })
1097 };
1098 if !owns_field(&dependency.source_field) || !owns_field(&dependency.target_field) {
1099 push_integrity(
1100 diagnostics,
1101 ValidationDependencyPlanIntegrityKind::FieldFormMismatch,
1102 "I7 dependency Fields are not canonical I5 children of its Form",
1103 );
1104 }
1105 if !forms.contains_key(&dependency.form) {
1106 push_integrity(
1107 diagnostics,
1108 ValidationDependencyPlanIntegrityKind::UnknownForm,
1109 "I7 dependency references an unknown Form",
1110 );
1111 }
1112}
1113
1114fn validate_dependency_provenance(
1115 dependency: &FieldValidationDependency,
1116 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
1117) {
1118 if !has_provenance(&dependency.provenance)
1119 || !has_provenance(&dependency.source_field_provenance)
1120 || !has_provenance(&dependency.target_field_provenance)
1121 {
1122 push_integrity(
1123 diagnostics,
1124 ValidationDependencyPlanIntegrityKind::MissingProvenance,
1125 "I7 dependency lacks canonical source provenance",
1126 );
1127 }
1128 if dependency.id.as_str().contains("form-instance")
1129 || dependency.plan.as_str().contains("form-instance")
1130 || dependency.form.as_str().contains("form-instance")
1131 {
1132 push_integrity(
1133 diagnostics,
1134 ValidationDependencyPlanIntegrityKind::InstanceIdentityLeak,
1135 "I7 declaration planning contains an instance-qualified identity",
1136 );
1137 }
1138}
1139
1140fn validate_entry_reciprocity(
1141 products: &ValidationDependencyPlans,
1142 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
1143) {
1144 for plan in products.plans.values() {
1145 for source in &plan.source_entries {
1146 let expected = products
1147 .dependencies
1148 .values()
1149 .filter(|dependency| {
1150 dependency.plan == plan.id && dependency.source_field == source.source_field
1151 })
1152 .collect::<Vec<_>>();
1153 let expected_ids = expected
1154 .iter()
1155 .map(|dependency| dependency.id.clone())
1156 .collect::<BTreeSet<_>>();
1157 let actual_ids = source.dependencies.iter().cloned().collect::<BTreeSet<_>>();
1158 if expected_ids != actual_ids {
1159 push_integrity(
1160 diagnostics,
1161 ValidationDependencyPlanIntegrityKind::InvalidationIndexMismatch,
1162 "source entry does not contain exactly its direct I7 dependencies",
1163 );
1164 }
1165 let expected_rules = expected
1166 .iter()
1167 .map(|dependency| dependency.dependent_rule.clone())
1168 .collect::<BTreeSet<_>>();
1169 let actual_rules = source
1170 .directly_invalidated_rules
1171 .iter()
1172 .cloned()
1173 .collect::<BTreeSet<_>>();
1174 if expected_rules != actual_rules {
1175 push_integrity(
1176 diagnostics,
1177 ValidationDependencyPlanIntegrityKind::TransitiveInvalidationLeak,
1178 "source entry contains non-direct invalidated Rules",
1179 );
1180 }
1181 }
1182 for target in &plan.target_entries {
1183 let expected = products
1184 .dependencies
1185 .values()
1186 .filter(|dependency| {
1187 dependency.plan == plan.id && dependency.target_field == target.target_field
1188 })
1189 .map(|dependency| dependency.id.clone())
1190 .collect::<BTreeSet<_>>();
1191 let actual = target.dependencies.iter().cloned().collect::<BTreeSet<_>>();
1192 if expected != actual {
1193 push_integrity(
1194 diagnostics,
1195 ValidationDependencyPlanIntegrityKind::TargetIndexMismatch,
1196 "target entry does not contain exactly its direct I7 dependencies",
1197 );
1198 }
1199 }
1200 }
1201 for dependency in products.dependencies.values() {
1202 let Some(plan) = products.plans.get(&dependency.plan) else {
1203 continue;
1204 };
1205 let source = plan
1206 .source_entries
1207 .iter()
1208 .find(|entry| entry.source_field == dependency.source_field);
1209 if !source.is_some_and(|entry| {
1210 entry.dependencies.contains(&dependency.id)
1211 && entry
1212 .directly_invalidated_rules
1213 .contains(&dependency.dependent_rule)
1214 && entry
1215 .directly_invalidated_target_fields
1216 .contains(&dependency.target_field)
1217 }) {
1218 push_integrity(
1219 diagnostics,
1220 ValidationDependencyPlanIntegrityKind::SourceIndexMismatch,
1221 "I7 source index omits its dependency projection",
1222 );
1223 }
1224 let target = plan
1225 .target_entries
1226 .iter()
1227 .find(|entry| entry.target_field == dependency.target_field);
1228 if !target.is_some_and(|entry| {
1229 entry.dependencies.contains(&dependency.id)
1230 && entry.cross_field_rules.contains(&dependency.dependent_rule)
1231 && entry.source_fields.contains(&dependency.source_field)
1232 }) {
1233 push_integrity(
1234 diagnostics,
1235 ValidationDependencyPlanIntegrityKind::TargetIndexMismatch,
1236 "I7 target index omits its dependency projection",
1237 );
1238 }
1239 }
1240}
1241
1242fn validate_product_ordering(
1243 products: &ValidationDependencyPlans,
1244 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
1245) {
1246 for plan in products.plans.values() {
1247 let orders = plan_field_orders(plan);
1248 let mut ordered_dependencies = plan.dependencies.clone();
1249 ordered_dependencies.sort_by(|left, right| {
1250 dependency_order(
1251 products
1252 .dependencies
1253 .get(left)
1254 .expect("plan dependency exists"),
1255 products
1256 .dependencies
1257 .get(right)
1258 .expect("plan dependency exists"),
1259 )
1260 });
1261 if plan.dependencies != ordered_dependencies {
1262 push_integrity(
1263 diagnostics,
1264 ValidationDependencyPlanIntegrityKind::NonCanonicalOrdering,
1265 "I7 plan dependencies are not canonically ordered",
1266 );
1267 }
1268 for source in &plan.source_entries {
1269 if has_duplicates(&source.directly_invalidated_rules) {
1270 push_integrity(
1271 diagnostics,
1272 ValidationDependencyPlanIntegrityKind::DuplicateScheduledRule,
1273 "source entry schedules one Rule more than once",
1274 );
1275 }
1276 if has_duplicates(&source.directly_invalidated_target_fields) {
1277 push_integrity(
1278 diagnostics,
1279 ValidationDependencyPlanIntegrityKind::DuplicateScheduledTarget,
1280 "source entry schedules one target Field more than once",
1281 );
1282 }
1283 let mut rules = source.directly_invalidated_rules.clone();
1284 rules.sort_by(|left, right| {
1285 let left = products
1286 .dependencies
1287 .values()
1288 .find(|dependency| {
1289 &dependency.dependent_rule == left
1290 && dependency.source_field == source.source_field
1291 })
1292 .expect("source Rule has dependency");
1293 let right = products
1294 .dependencies
1295 .values()
1296 .find(|dependency| {
1297 &dependency.dependent_rule == right
1298 && dependency.source_field == source.source_field
1299 })
1300 .expect("source Rule has dependency");
1301 rule_schedule_order(left, right)
1302 });
1303 if source.directly_invalidated_rules != rules {
1304 push_integrity(
1305 diagnostics,
1306 ValidationDependencyPlanIntegrityKind::InvalidationIndexMismatch,
1307 "source invalidation Rules are not in schedule order",
1308 );
1309 }
1310 }
1311 if plan
1312 .source_entries
1313 .iter()
1314 .map(|entry| entry.authored_field_order)
1315 .collect::<Vec<_>>()
1316 != plan
1317 .source_entries
1318 .iter()
1319 .map(|entry| orders[&entry.source_field])
1320 .collect::<Vec<_>>()
1321 {
1322 push_integrity(
1323 diagnostics,
1324 ValidationDependencyPlanIntegrityKind::NonCanonicalOrdering,
1325 "source entries do not preserve authored Field order",
1326 );
1327 }
1328 }
1329}
1330
1331fn canonicalize_plans(
1332 plans: &mut BTreeMap<ValidationPlanId, FormValidationDependencyPlan>,
1333 dependencies: &BTreeMap<FieldDependencyId, FieldValidationDependency>,
1334) {
1335 for plan in plans.values_mut() {
1336 plan.fields.sort_by_key(|field| {
1337 plan.source_entries
1338 .iter()
1339 .find(|entry| entry.source_field == *field)
1340 .map_or(usize::MAX, |entry| entry.authored_field_order)
1341 });
1342 plan.dependencies.sort_by(|left, right| {
1343 dependency_order(
1344 dependencies.get(left).expect("plan dependency exists"),
1345 dependencies.get(right).expect("plan dependency exists"),
1346 )
1347 });
1348 let orders = plan_field_orders(plan);
1349 for source in &mut plan.source_entries {
1350 source.dependencies.sort_by(|left, right| {
1351 dependency_order(
1352 dependencies.get(left).expect("source dependency exists"),
1353 dependencies.get(right).expect("source dependency exists"),
1354 )
1355 });
1356 source.directly_invalidated_rules.sort_by(|left, right| {
1357 let left = dependencies
1358 .values()
1359 .find(|dependency| {
1360 &dependency.dependent_rule == left
1361 && dependency.source_field == source.source_field
1362 })
1363 .expect("source Rule has dependency");
1364 let right = dependencies
1365 .values()
1366 .find(|dependency| {
1367 &dependency.dependent_rule == right
1368 && dependency.source_field == source.source_field
1369 })
1370 .expect("source Rule has dependency");
1371 rule_schedule_order(left, right)
1372 });
1373 source.directly_invalidated_rules.dedup();
1374 source
1375 .directly_invalidated_target_fields
1376 .sort_by_key(|field| {
1377 (
1378 orders.get(field).copied().unwrap_or(usize::MAX),
1379 field.clone(),
1380 )
1381 });
1382 source.directly_invalidated_target_fields.dedup();
1383 }
1384 for target in &mut plan.target_entries {
1385 target.dependencies.sort_by(|left, right| {
1386 dependency_order(
1387 dependencies.get(left).expect("target dependency exists"),
1388 dependencies.get(right).expect("target dependency exists"),
1389 )
1390 });
1391 target.cross_field_rules.sort_by(|left, right| {
1392 let left = dependencies
1393 .values()
1394 .find(|dependency| {
1395 &dependency.dependent_rule == left
1396 && dependency.target_field == target.target_field
1397 })
1398 .expect("target Rule has dependency");
1399 let right = dependencies
1400 .values()
1401 .find(|dependency| {
1402 &dependency.dependent_rule == right
1403 && dependency.target_field == target.target_field
1404 })
1405 .expect("target Rule has dependency");
1406 rule_schedule_order(left, right)
1407 });
1408 target.cross_field_rules.dedup();
1409 target.source_fields.sort_by_key(|field| {
1410 (
1411 orders.get(field).copied().unwrap_or(usize::MAX),
1412 field.clone(),
1413 )
1414 });
1415 target.source_fields.dedup();
1416 }
1417 plan.source_entries
1418 .sort_by_key(|entry| (entry.authored_field_order, entry.source_field.clone()));
1419 plan.target_entries
1420 .sort_by_key(|entry| (entry.authored_field_order, entry.target_field.clone()));
1421 }
1422}
1423
1424fn ordered_fields_for_form<'a>(
1425 fields: &'a BTreeMap<FieldId, FormFieldEntity>,
1426 form: &FormId,
1427) -> Vec<&'a FormFieldEntity> {
1428 let mut result = fields
1429 .values()
1430 .filter(|field| &field.owner_form == form)
1431 .collect::<Vec<_>>();
1432 result.sort_by(|left, right| field_order(left, right));
1433 result
1434}
1435
1436fn field_order(left: &FormFieldEntity, right: &FormFieldEntity) -> std::cmp::Ordering {
1437 (left.declaration_order, &left.id).cmp(&(right.declaration_order, &right.id))
1438}
1439
1440fn plan_field_orders(plan: &FormValidationDependencyPlan) -> BTreeMap<FieldId, usize> {
1441 plan.source_entries
1442 .iter()
1443 .map(|entry| (entry.source_field.clone(), entry.authored_field_order))
1444 .collect()
1445}
1446
1447fn dependency_order(
1448 left: &FieldValidationDependency,
1449 right: &FieldValidationDependency,
1450) -> std::cmp::Ordering {
1451 (
1452 left.target_field_order,
1453 left.rule_order,
1454 left.source_field_order,
1455 &left.id,
1456 )
1457 .cmp(&(
1458 right.target_field_order,
1459 right.rule_order,
1460 right.source_field_order,
1461 &right.id,
1462 ))
1463}
1464
1465fn dependency_sort_key(
1466 dependency: &FieldValidationDependency,
1467) -> (usize, usize, usize, FieldDependencyId) {
1468 (
1469 dependency.target_field_order,
1470 dependency.rule_order,
1471 dependency.source_field_order,
1472 dependency.id.clone(),
1473 )
1474}
1475
1476fn rule_schedule_order(
1477 left: &FieldValidationDependency,
1478 right: &FieldValidationDependency,
1479) -> std::cmp::Ordering {
1480 (
1481 left.target_field_order,
1482 left.rule_order,
1483 &left.dependent_rule,
1484 )
1485 .cmp(&(
1486 right.target_field_order,
1487 right.rule_order,
1488 &right.dependent_rule,
1489 ))
1490}
1491
1492fn rule_schedule_key(dependency: &FieldValidationDependency) -> (usize, usize, ValidationRuleId) {
1493 (
1494 dependency.target_field_order,
1495 dependency.rule_order,
1496 dependency.dependent_rule.clone(),
1497 )
1498}
1499
1500fn blocked_for_rule(
1501 rule: &ValidationRule,
1502 source: Option<FieldId>,
1503 reason: FieldDependencyBlockReason,
1504 edge: Option<&&crate::ValidationGraphEdge>,
1505) -> BlockedFieldValidationDependency {
1506 BlockedFieldValidationDependency {
1507 rule: Some(rule.id.clone()),
1508 source_field: source,
1509 target_field: Some(rule.target_field.clone()),
1510 form: Some(rule.owner_form.clone()),
1511 reason,
1512 provenance: edge
1513 .map(|edge| edge.provenance.clone())
1514 .or_else(|| Some(rule.provenance.clone())),
1515 }
1516}
1517
1518fn blocked_order(
1519 left: &BlockedFieldValidationDependency,
1520 right: &BlockedFieldValidationDependency,
1521) -> std::cmp::Ordering {
1522 (
1523 left.form.as_ref().map_or("", FormId::as_str),
1524 left.target_field.as_ref().map_or("", FieldId::as_str),
1525 left.rule.as_ref().map_or("", ValidationRuleId::as_str),
1526 left.source_field.as_ref().map_or("", FieldId::as_str),
1527 left.reason,
1528 left.provenance
1529 .as_ref()
1530 .map_or("", |provenance| provenance.path.to_str().unwrap_or("")),
1531 left.provenance
1532 .as_ref()
1533 .map_or(0, |provenance| provenance.span.start),
1534 )
1535 .cmp(&(
1536 right.form.as_ref().map_or("", FormId::as_str),
1537 right.target_field.as_ref().map_or("", FieldId::as_str),
1538 right.rule.as_ref().map_or("", ValidationRuleId::as_str),
1539 right.source_field.as_ref().map_or("", FieldId::as_str),
1540 right.reason,
1541 right
1542 .provenance
1543 .as_ref()
1544 .map_or("", |provenance| provenance.path.to_str().unwrap_or("")),
1545 right
1546 .provenance
1547 .as_ref()
1548 .map_or(0, |provenance| provenance.span.start),
1549 ))
1550}
1551
1552fn has_provenance(provenance: &SourceProvenance) -> bool {
1553 !provenance.path.as_os_str().is_empty() && provenance.span.start <= provenance.span.end
1554}
1555
1556fn has_duplicates<T: Ord + Clone>(values: &[T]) -> bool {
1557 values.iter().cloned().collect::<BTreeSet<_>>().len() != values.len()
1558}
1559
1560fn push_integrity(
1561 diagnostics: &mut Vec<ValidationDependencyPlanIntegrityDiagnostic>,
1562 kind: ValidationDependencyPlanIntegrityKind,
1563 message: &str,
1564) {
1565 diagnostics.push(ValidationDependencyPlanIntegrityDiagnostic {
1566 code: kind.code().to_string(),
1567 kind,
1568 message: message.to_string(),
1569 });
1570}
1571
1572#[cfg(test)]
1573mod tests {
1574 use super::{
1575 collect_validation_dependency_plans, validate_validation_dependency_plans,
1576 FieldDependencyBlockReason, ValidationDependencyPlanIntegrityKind,
1577 ValidationPlanningStatus,
1578 };
1579 use crate::{
1580 build_application_semantic_model, build_application_semantic_model_for_unit,
1581 build_semantic_graph, semantic_graph_json, validate_application_semantic_model,
1582 CompilationUnit, FormInstanceId, SemanticGraph, SEMANTIC_GRAPH_SCHEMA_VERSION,
1583 };
1584
1585 fn build(source: &str) -> crate::ApplicationSemanticModel {
1586 build_application_semantic_model(&presolve_parser::parse_file("src/Profile.tsx", source))
1587 }
1588
1589 #[test]
1590 fn plans_direct_cross_field_dependencies_without_unary_or_transitive_propagation() {
1591 let asm = build(
1592 r#"
1593@component("profile")
1594class Profile {
1595 @form() form!: Form;
1596 @field(this.form) c = "";
1597 @validate(required()) @validate(equals(this.c)) @field(this.form) b = "";
1598 @validate(equals(this.b)) @field(this.form) a = "";
1599 render() { return <div />; }
1600}
1601"#,
1602 );
1603 let form = asm.forms.keys().next().unwrap();
1604 let fields = &asm.form_fields;
1605 let c = fields
1606 .values()
1607 .find(|field| field.name == "c")
1608 .unwrap()
1609 .id
1610 .clone();
1611 let b = fields
1612 .values()
1613 .find(|field| field.name == "b")
1614 .unwrap()
1615 .id
1616 .clone();
1617 let a = fields
1618 .values()
1619 .find(|field| field.name == "a")
1620 .unwrap()
1621 .id
1622 .clone();
1623 let plan = asm
1624 .validation_dependency_plans
1625 .validation_plan(form)
1626 .unwrap();
1627 assert_eq!(plan.dependencies.len(), 2);
1628 assert!(plan
1629 .target_entries
1630 .iter()
1631 .find(|entry| entry.target_field == b)
1632 .is_some_and(|entry| entry.cross_field_rules.len() == 1));
1633 let c_schedule = asm
1634 .validation_dependency_plans
1635 .schedule_change_set(form, std::slice::from_ref(&c))
1636 .unwrap();
1637 assert_eq!(c_schedule.scheduled_rules.len(), 1);
1638 assert_eq!(c_schedule.scheduled_target_fields, vec![b.clone()]);
1639 let b_schedule = asm
1640 .validation_dependency_plans
1641 .schedule_change_set(form, std::slice::from_ref(&b))
1642 .unwrap();
1643 assert_eq!(b_schedule.scheduled_target_fields, vec![a]);
1644 assert_eq!(plan.initial_validation, ValidationPlanningStatus::Deferred);
1645 assert_eq!(
1646 plan.submission_validation,
1647 ValidationPlanningStatus::Deferred
1648 );
1649 }
1650
1651 #[test]
1652 fn schedules_change_sets_by_target_order_deduplicates_rules_and_keeps_evidence() {
1653 let asm = build(
1654 r#"
1655@component("profile")
1656class Profile {
1657 @form() form!: Form;
1658 @field(this.form) first = "";
1659 @field(this.form) second = "";
1660 @validate(equals(this.second)) @field(this.form) alpha = "";
1661 @validate(equals(this.first)) @field(this.form) beta = "";
1662 render() { return <div />; }
1663}
1664"#,
1665 );
1666 let form = asm.forms.keys().next().unwrap();
1667 let first = asm
1668 .form_fields
1669 .values()
1670 .find(|field| field.name == "first")
1671 .unwrap()
1672 .id
1673 .clone();
1674 let second = asm
1675 .form_fields
1676 .values()
1677 .find(|field| field.name == "second")
1678 .unwrap()
1679 .id
1680 .clone();
1681 let alpha = asm
1682 .form_fields
1683 .values()
1684 .find(|field| field.name == "alpha")
1685 .unwrap()
1686 .id
1687 .clone();
1688 let beta = asm
1689 .form_fields
1690 .values()
1691 .find(|field| field.name == "beta")
1692 .unwrap()
1693 .id
1694 .clone();
1695 let schedule = asm
1696 .validation_dependency_plans
1697 .schedule_change_set(form, &[second.clone(), first.clone(), second])
1698 .unwrap();
1699 assert_eq!(
1700 schedule.changed_fields,
1701 vec![
1702 first,
1703 asm.form_fields
1704 .values()
1705 .find(|field| field.name == "second")
1706 .unwrap()
1707 .id
1708 .clone()
1709 ]
1710 );
1711 assert_eq!(schedule.scheduled_target_fields, vec![alpha, beta]);
1712 assert_eq!(schedule.scheduled_rules.len(), 2);
1713 assert_eq!(schedule.triggering_dependencies.len(), 2);
1714 }
1715
1716 #[test]
1717 fn creates_empty_plans_and_rejects_cross_form_change_sets() {
1718 let asm = build(
1719 r#"
1720@component("profile")
1721class Profile {
1722 @form() empty!: Form;
1723 @form() active!: Form;
1724 @field(this.active) value = "";
1725 render() { return <div />; }
1726}
1727"#,
1728 );
1729 assert_eq!(asm.validation_dependency_plans.plans.len(), 2);
1730 let empty = asm
1731 .forms
1732 .values()
1733 .find(|form| form.name == "empty")
1734 .unwrap();
1735 assert!(asm
1736 .validation_dependency_plans
1737 .validation_plan(&empty.id)
1738 .is_some_and(|plan| plan.fields.is_empty() && plan.dependencies.is_empty()));
1739 let active = asm
1740 .forms
1741 .values()
1742 .find(|form| form.name == "active")
1743 .unwrap();
1744 let field = asm.form_fields.values().next().unwrap();
1745 assert!(asm
1746 .validation_dependency_plans
1747 .schedule_change_set(&empty.id, std::slice::from_ref(&field.id))
1748 .is_none());
1749 assert!(asm
1750 .validation_dependency_plans
1751 .schedule_change_set(&active.id, &[])
1752 .is_some());
1753 }
1754
1755 #[test]
1756 fn retains_blocked_records_and_detects_missing_projection_and_index_drift() {
1757 let asm = build(
1758 r#"
1759@component("profile")
1760class Profile {
1761 @form() form!: Form;
1762 @field(this.form) source = "";
1763 @validate(equals(this.source)) @field(this.form) target = "";
1764 render() { return <div />; }
1765}
1766"#,
1767 );
1768 let mut missing = asm.validation_dependency_plans.clone();
1769 let dependency = missing.dependencies.keys().next().unwrap().clone();
1770 missing.dependencies.remove(&dependency);
1771 missing
1772 .plans
1773 .values_mut()
1774 .next()
1775 .unwrap()
1776 .dependencies
1777 .clear();
1778 let validation = validate_validation_dependency_plans(
1779 &missing,
1780 &asm.forms,
1781 &asm.form_fields,
1782 &asm.validation_rules,
1783 &asm.form_ownership,
1784 &asm.validation_graph,
1785 );
1786 assert!(validation
1787 .diagnostics
1788 .iter()
1789 .any(|diagnostic| diagnostic.kind
1790 == ValidationDependencyPlanIntegrityKind::MissingDependencyProjection));
1791
1792 let mut graph = asm.validation_graph.clone();
1793 graph.edges.retain(|edge| {
1794 !matches!(
1795 edge.kind,
1796 crate::ValidationGraphEdgeKind::RuleDependsOnField
1797 )
1798 });
1799 let blocked = collect_validation_dependency_plans(
1800 &asm.forms,
1801 &asm.form_fields,
1802 &asm.validation_rules,
1803 &asm.form_ownership,
1804 &graph,
1805 );
1806 assert!(blocked
1807 .blocked
1808 .iter()
1809 .any(|blocked| blocked.reason == FieldDependencyBlockReason::IdentityMismatch));
1810
1811 let mut stale = asm.clone();
1812 stale.validation_dependency_plans.dependencies.clear();
1813 stale
1814 .validation_dependency_plans
1815 .plans
1816 .values_mut()
1817 .next()
1818 .unwrap()
1819 .dependencies
1820 .clear();
1821 assert!(validate_application_semantic_model(&stale)
1822 .iter()
1823 .any(|diagnostic| diagnostic.code == "PSASM1272"));
1824 }
1825
1826 #[test]
1827 fn remains_declaration_only_deterministic_and_hidden_from_public_schema() {
1828 let first = presolve_parser::parse_file(
1829 "src/A.tsx",
1830 r#"@component("a-x") class A { @form() form!: Form; @field(this.form) left = ""; @validate(equals(this.left)) @field(this.form) right = ""; render() { return <div />; } }"#,
1831 );
1832 let second = presolve_parser::parse_file(
1833 "src/B.tsx",
1834 r#"@component("b-x") class B { @form() form!: Form; render() { return <div />; } }"#,
1835 );
1836 let forward =
1837 build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
1838 first.clone(),
1839 second.clone(),
1840 ]));
1841 let reversed =
1842 build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
1843 second, first,
1844 ]));
1845 assert_eq!(
1846 forward.validation_dependency_plans,
1847 reversed.validation_dependency_plans
1848 );
1849 assert!(forward
1850 .validation_dependency_plans
1851 .dependencies
1852 .values()
1853 .all(|dependency| !dependency.id.as_str().contains("form-instance")));
1854 let form = forward.forms.values().next().unwrap();
1855 let instance = FormInstanceId::for_component_instance(
1856 &forward
1857 .component_instance_plan
1858 .instances
1859 .values()
1860 .next()
1861 .unwrap()
1862 .id,
1863 &form.id,
1864 );
1865 assert!(!forward
1866 .validation_dependency_plans
1867 .plans
1868 .keys()
1869 .any(|plan| plan.as_str() == instance.as_str()));
1870 assert_eq!(SEMANTIC_GRAPH_SCHEMA_VERSION, 6);
1871 let graph: SemanticGraph = build_semantic_graph(&forward);
1872 let json = semantic_graph_json(&graph);
1873 assert!(!json.contains("validation-plan"));
1874 assert!(!json.contains("field-dependency"));
1875 }
1876}