Skip to main content

presolve_compiler/
form_validation_plan.rs

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/// I7 deliberately retains no initial- or submission-validation policy.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ValidationPlanningStatus {
13    Deferred,
14}
15
16/// One direct I6 `ValidationRule -> source Field` read relation, projected into
17/// I7 invalidation planning. It is declaration-level and immutable.
18#[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/// Retained only for malformed or stale canonical I6/I5 products. Normal I6
75/// candidates remain solely in I6 candidate registries.
76#[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/// Exactly one declaration-level plan exists for every valid Form, including
179/// Forms with no Fields or no cross-field rules.
180#[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/// Complete I7 internal planning product. It is not a public schema, runtime
195/// artifact, or new ASM ownership authority.
196#[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    /// Schedules only directly read dependencies. An unknown Form or Field is
350    /// rejected by returning no schedule; this query never substitutes a Form.
351    #[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/// Builds I7 exclusively from I3/I5/I6 canonical products. No parser facts,
454/// JSX controls, runtime state, or declaration-name resolution is consulted.
455///
456/// # Panics
457///
458/// Panics only when an I3-valid Form lacks its canonical Component owner, or
459/// when a dependency accepted after I5/I6 reciprocity checks is absent from
460/// the plan it canonically names.
461#[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}