Skip to main content

presolve_compiler/
resume_boundary.rs

1//! J3 canonical resume boundary graph over existing Phase H/I and J2 products.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6    build_ordinary_template_instance_registry, build_resume_liveness_plan,
7    ApplicationSemanticModel, ComponentInstanceId, ComponentInstanceStatus, ComponentRootId,
8    ComponentStructuralRegionId, FormInstanceId, ResumeBoundaryId, ResumeBoundaryKind,
9    ResumeExistingSlot, ResumeLivenessBlockReason, SemanticId, SourceProvenance, SubmissionHostId,
10    SubmissionPlanId, TemplateInstanceTargetId,
11};
12
13pub const RESUME_BOUNDARY_GRAPH_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16pub enum ResumeBoundaryOwner {
17    ApplicationRoot(ComponentRootId),
18    ComponentInstance(ComponentInstanceId),
19    StructuralRegion {
20        component_instance: ComponentInstanceId,
21        region: ComponentStructuralRegionId,
22    },
23    FormInstance(FormInstanceId),
24    Interaction(ResumeBoundaryActivationIdentity),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
28pub enum ResumeBoundaryActivationIdentity {
29    OrdinaryEvent {
30        component_instance: ComponentInstanceId,
31        declaration_event: SemanticId,
32    },
33    FormSubmit {
34        component_instance: ComponentInstanceId,
35        submission_host: SubmissionHostId,
36        form_instance: FormInstanceId,
37    },
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ResumeBoundary {
42    pub id: ResumeBoundaryId,
43    pub kind: ResumeBoundaryKind,
44    pub owner: ResumeBoundaryOwner,
45    pub ownership_parent: Option<ResumeBoundaryId>,
46    pub owned_slots: Vec<ResumeExistingSlot>,
47    pub provenance: SourceProvenance,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
51pub struct ResumeBoundaryOwnershipEdge {
52    pub parent: ResumeBoundaryId,
53    pub child: ResumeBoundaryId,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum ResumeBoundaryActivationProgram {
58    OrdinaryEvent {
59        declaration_event: SemanticId,
60        target: TemplateInstanceTargetId,
61        event_type: String,
62        handler_method: SemanticId,
63        action_batch: SemanticId,
64        existing_program: SemanticId,
65    },
66    FormSubmit {
67        submission_host: SubmissionHostId,
68        target: TemplateInstanceTargetId,
69        form_instance: FormInstanceId,
70        submission_plan: SubmissionPlanId,
71        submit_action: SemanticId,
72        action_batch: SemanticId,
73        serialization_plan: crate::SerializationPlanId,
74        prevent_default: bool,
75    },
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ResumeBoundaryActivationReference {
80    pub interaction_boundary: ResumeBoundaryId,
81    pub owner_boundary: ResumeBoundaryId,
82    pub required_boundaries: Vec<ResumeBoundaryId>,
83    pub required_retained_slots: Vec<ResumeExistingSlot>,
84    pub program: ResumeBoundaryActivationProgram,
85    pub provenance: SourceProvenance,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
89pub enum ResumeBoundaryBlockSource {
90    ComponentInstance {
91        instance: ComponentInstanceId,
92        reason: crate::BlockedComponentInstanceReason,
93    },
94    LivenessSlot {
95        slot: ResumeExistingSlot,
96        reason: ResumeLivenessBlockReason,
97    },
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ResumeBoundaryBlock {
102    pub candidate_boundary: Option<ResumeBoundaryId>,
103    pub source: ResumeBoundaryBlockSource,
104    pub provenance: SourceProvenance,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ResumeBoundaryGraph {
109    pub version: u32,
110    pub boundaries: Vec<ResumeBoundary>,
111    pub ownership_edges: Vec<ResumeBoundaryOwnershipEdge>,
112    pub activation_references: Vec<ResumeBoundaryActivationReference>,
113    pub blocks: Vec<ResumeBoundaryBlock>,
114    pub boundary_index: BTreeMap<ResumeBoundaryId, usize>,
115    pub children_by_parent: BTreeMap<ResumeBoundaryId, Vec<ResumeBoundaryId>>,
116}
117
118impl ResumeBoundaryGraph {
119    #[must_use]
120    pub fn boundary(&self, id: &ResumeBoundaryId) -> Option<&ResumeBoundary> {
121        self.boundary_index
122            .get(id)
123            .and_then(|index| self.boundaries.get(*index))
124    }
125
126    #[must_use]
127    pub fn parent(&self, id: &ResumeBoundaryId) -> Option<&ResumeBoundaryId> {
128        self.boundary(id)
129            .and_then(|boundary| boundary.ownership_parent.as_ref())
130    }
131
132    #[must_use]
133    pub fn children(&self, id: &ResumeBoundaryId) -> &[ResumeBoundaryId] {
134        self.children_by_parent.get(id).map_or(&[], Vec::as_slice)
135    }
136
137    #[must_use]
138    pub fn activations_for_owner(
139        &self,
140        owner: &ResumeBoundaryId,
141    ) -> Vec<&ResumeBoundaryActivationReference> {
142        self.activation_references
143            .iter()
144            .filter(|reference| &reference.owner_boundary == owner)
145            .collect()
146    }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
150pub enum ResumeBoundaryIntegrityCode {
151    DuplicateBoundary,
152    InvalidOwner,
153    MissingOrMultipleParent,
154    Cycle,
155    Unreachable,
156    Reciprocity,
157    PhaseCorrespondence,
158    ProvenanceDrift,
159    OrderingOrIndexDrift,
160}
161
162impl ResumeBoundaryIntegrityCode {
163    #[must_use]
164    pub const fn code(self) -> &'static str {
165        match self {
166            Self::DuplicateBoundary => "PSASM1328",
167            Self::InvalidOwner => "PSASM1329",
168            Self::MissingOrMultipleParent => "PSASM1330",
169            Self::Cycle => "PSASM1331",
170            Self::Unreachable => "PSASM1332",
171            Self::Reciprocity => "PSASM1333",
172            Self::PhaseCorrespondence => "PSASM1334",
173            Self::ProvenanceDrift => "PSASM1335",
174            Self::OrderingOrIndexDrift => "PSASM1336",
175        }
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ResumeBoundaryIntegrityDiagnostic {
181    pub code: ResumeBoundaryIntegrityCode,
182    pub boundary: Option<ResumeBoundaryId>,
183    pub message: String,
184}
185
186#[derive(Debug, Clone)]
187struct PendingBoundary {
188    boundary: ResumeBoundary,
189    depth: usize,
190}
191
192#[must_use]
193#[allow(clippy::too_many_lines)]
194pub fn build_resume_boundary_graph(model: &ApplicationSemanticModel) -> ResumeBoundaryGraph {
195    let liveness = build_resume_liveness_plan(model);
196    let ordinary = build_ordinary_template_instance_registry(model);
197    let mut pending = Vec::new();
198    let mut ownership_edges = Vec::new();
199    let mut activation_references = Vec::new();
200    let mut blocks = Vec::new();
201
202    for root in model.component_instance_plan.roots.values() {
203        pending.push(PendingBoundary {
204            boundary: ResumeBoundary {
205                id: ResumeBoundaryId::application_root(&root.id),
206                kind: ResumeBoundaryKind::ApplicationRoot,
207                owner: ResumeBoundaryOwner::ApplicationRoot(root.id.clone()),
208                ownership_parent: None,
209                owned_slots: liveness_slots(
210                    &liveness,
211                    &ResumeBoundaryId::application_root(&root.id),
212                ),
213                provenance: root.provenance.clone(),
214            },
215            depth: 0,
216        });
217    }
218
219    for instance in model.component_instance_plan.instances.values() {
220        if !matches!(
221            instance.status,
222            ComponentInstanceStatus::Planned | ComponentInstanceStatus::StructuralTemplate
223        ) {
224            continue;
225        }
226        let id = ResumeBoundaryId::component_instance(&instance.id);
227        let parent = if instance.status == ComponentInstanceStatus::StructuralTemplate {
228            instance
229                .parent_instance
230                .as_ref()
231                .zip(instance.structural_region.as_ref())
232                .map_or_else(
233                    || ResumeBoundaryId::application_root(&instance.owner_root),
234                    |(parent, region)| ResumeBoundaryId::structural_region(parent, region),
235                )
236        } else {
237            instance.parent_instance.as_ref().map_or_else(
238                || ResumeBoundaryId::application_root(&instance.owner_root),
239                ResumeBoundaryId::component_instance,
240            )
241        };
242        pending.push(PendingBoundary {
243            boundary: ResumeBoundary {
244                id: id.clone(),
245                kind: ResumeBoundaryKind::ComponentInstance,
246                owner: ResumeBoundaryOwner::ComponentInstance(instance.id.clone()),
247                ownership_parent: Some(parent.clone()),
248                owned_slots: liveness_slots(&liveness, &id),
249                provenance: instance.provenance.clone(),
250            },
251            depth: instance.depth * 2 + 1,
252        });
253        ownership_edges.push(ResumeBoundaryOwnershipEdge { parent, child: id });
254    }
255
256    let mut structural_regions = BTreeMap::new();
257    for instance in model.component_instance_plan.instances.values() {
258        let (Some(parent), Some(region)) = (
259            instance.parent_instance.as_ref(),
260            instance.structural_region.as_ref(),
261        ) else {
262            continue;
263        };
264        structural_regions
265            .entry((parent.clone(), region.clone()))
266            .or_insert_with(|| {
267                (
268                    instance.depth.saturating_sub(1),
269                    instance.provenance.clone(),
270                )
271            });
272    }
273    for ((owner, region), (owner_depth, provenance)) in structural_regions {
274        let id = ResumeBoundaryId::structural_region(&owner, &region);
275        let parent = ResumeBoundaryId::component_instance(&owner);
276        pending.push(PendingBoundary {
277            boundary: ResumeBoundary {
278                id: id.clone(),
279                kind: ResumeBoundaryKind::StructuralRegion,
280                owner: ResumeBoundaryOwner::StructuralRegion {
281                    component_instance: owner,
282                    region,
283                },
284                ownership_parent: Some(parent.clone()),
285                owned_slots: liveness_slots(&liveness, &id),
286                provenance,
287            },
288            depth: owner_depth * 2 + 2,
289        });
290        ownership_edges.push(ResumeBoundaryOwnershipEdge { parent, child: id });
291    }
292
293    for instance in model.optimized_form_ir.optimized.instances.values() {
294        let id = ResumeBoundaryId::form_instance(&instance.id);
295        let parent = ResumeBoundaryId::component_instance(&instance.component_instance);
296        let depth = model
297            .component_instance_plan
298            .instances
299            .get(&instance.component_instance)
300            .map_or(0, |component| component.depth);
301        pending.push(PendingBoundary {
302            boundary: ResumeBoundary {
303                id: id.clone(),
304                kind: ResumeBoundaryKind::FormInstance,
305                owner: ResumeBoundaryOwner::FormInstance(instance.id.clone()),
306                ownership_parent: Some(parent.clone()),
307                owned_slots: liveness_slots(&liveness, &id),
308                provenance: model.forms[&instance.form].provenance.clone(),
309            },
310            depth: depth * 2 + 2,
311        });
312        ownership_edges.push(ResumeBoundaryOwnershipEdge { parent, child: id });
313    }
314
315    for event in &ordinary.events {
316        let Some(action_batch) = event.action_batch_id.clone() else {
317            continue;
318        };
319        let id = ResumeBoundaryId::interaction(
320            &event.component_instance_id,
321            &event.declaration_event_id,
322        );
323        let owner_boundary = ResumeBoundaryId::component_instance(&event.component_instance_id);
324        let required_boundaries = vec![owner_boundary.clone()];
325        let required_retained_slots =
326            retained_slots_for_boundaries(&liveness, &required_boundaries);
327        pending.push(PendingBoundary {
328            boundary: ResumeBoundary {
329                id: id.clone(),
330                kind: ResumeBoundaryKind::Interaction,
331                owner: ResumeBoundaryOwner::Interaction(
332                    ResumeBoundaryActivationIdentity::OrdinaryEvent {
333                        component_instance: event.component_instance_id.clone(),
334                        declaration_event: event.declaration_event_id.clone(),
335                    },
336                ),
337                ownership_parent: None,
338                owned_slots: Vec::new(),
339                provenance: event.provenance.clone(),
340            },
341            depth: usize::MAX,
342        });
343        activation_references.push(ResumeBoundaryActivationReference {
344            interaction_boundary: id,
345            owner_boundary,
346            required_boundaries,
347            required_retained_slots,
348            program: ResumeBoundaryActivationProgram::OrdinaryEvent {
349                declaration_event: event.declaration_event_id.clone(),
350                target: event.target_id.clone(),
351                event_type: event.event_type.clone(),
352                handler_method: event.handler_method_id.clone(),
353                action_batch,
354                existing_program: event.existing_event_program_identity.clone(),
355            },
356            provenance: event.provenance.clone(),
357        });
358    }
359
360    for form in model.optimized_form_ir.optimized.instances.values() {
361        for host in model
362            .submission_hosts
363            .values()
364            .filter(|host| host.form == form.form)
365        {
366            let id =
367                ResumeBoundaryId::interaction(&form.component_instance, host.id.as_semantic_id());
368            let owner_boundary = ResumeBoundaryId::component_instance(&form.component_instance);
369            let form_boundary = ResumeBoundaryId::form_instance(&form.id);
370            let required_boundaries = vec![owner_boundary.clone(), form_boundary];
371            let required_retained_slots =
372                retained_slots_for_boundaries(&liveness, &required_boundaries);
373            let target = TemplateInstanceTargetId::for_component_instance_template_entity(
374                form.component_instance.clone(),
375                host.owner_template_element.clone(),
376            );
377            pending.push(PendingBoundary {
378                boundary: ResumeBoundary {
379                    id: id.clone(),
380                    kind: ResumeBoundaryKind::Interaction,
381                    owner: ResumeBoundaryOwner::Interaction(
382                        ResumeBoundaryActivationIdentity::FormSubmit {
383                            component_instance: form.component_instance.clone(),
384                            submission_host: host.id.clone(),
385                            form_instance: form.id.clone(),
386                        },
387                    ),
388                    ownership_parent: None,
389                    owned_slots: Vec::new(),
390                    provenance: host.provenance.clone(),
391                },
392                depth: usize::MAX,
393            });
394            activation_references.push(ResumeBoundaryActivationReference {
395                interaction_boundary: id,
396                owner_boundary,
397                required_boundaries,
398                required_retained_slots,
399                program: ResumeBoundaryActivationProgram::FormSubmit {
400                    submission_host: host.id.clone(),
401                    target,
402                    form_instance: form.id.clone(),
403                    submission_plan: host.submission_plan.clone(),
404                    submit_action: host.submit_action.clone(),
405                    action_batch: host.action_batch.clone(),
406                    serialization_plan: host.serialization_plan.clone(),
407                    prevent_default: host.prevent_default,
408                },
409                provenance: host.provenance.clone(),
410            });
411        }
412    }
413
414    for blocked in model.component_instance_plan.blocked.values() {
415        blocks.push(ResumeBoundaryBlock {
416            candidate_boundary: Some(ResumeBoundaryId::component_instance(&blocked.id)),
417            source: ResumeBoundaryBlockSource::ComponentInstance {
418                instance: blocked.id.clone(),
419                reason: blocked.reason,
420            },
421            provenance: blocked.provenance.clone(),
422        });
423    }
424    for blocked in &liveness.blocked {
425        blocks.push(ResumeBoundaryBlock {
426            candidate_boundary: blocked.slot.boundary_candidate.clone(),
427            source: ResumeBoundaryBlockSource::LivenessSlot {
428                slot: blocked.slot.existing_slot.clone(),
429                reason: blocked.reason,
430            },
431            provenance: blocked.slot.provenance.clone(),
432        });
433    }
434
435    pending.sort_by(|left, right| {
436        (left.depth, &left.boundary.id).cmp(&(right.depth, &right.boundary.id))
437    });
438    let boundaries = pending
439        .into_iter()
440        .map(|pending| pending.boundary)
441        .collect::<Vec<_>>();
442    ownership_edges.sort();
443    ownership_edges.dedup();
444    activation_references
445        .sort_by(|left, right| left.interaction_boundary.cmp(&right.interaction_boundary));
446    blocks.sort_by(|left, right| {
447        (&left.candidate_boundary, &left.source).cmp(&(&right.candidate_boundary, &right.source))
448    });
449    let boundary_index = boundaries
450        .iter()
451        .enumerate()
452        .map(|(index, boundary)| (boundary.id.clone(), index))
453        .collect();
454    let mut children_by_parent = BTreeMap::<ResumeBoundaryId, Vec<ResumeBoundaryId>>::new();
455    for edge in &ownership_edges {
456        children_by_parent
457            .entry(edge.parent.clone())
458            .or_default()
459            .push(edge.child.clone());
460    }
461    for children in children_by_parent.values_mut() {
462        children.sort();
463        children.dedup();
464    }
465    ResumeBoundaryGraph {
466        version: RESUME_BOUNDARY_GRAPH_VERSION,
467        boundaries,
468        ownership_edges,
469        activation_references,
470        blocks,
471        boundary_index,
472        children_by_parent,
473    }
474}
475
476fn liveness_slots(
477    liveness: &crate::ResumeLivenessPlan,
478    boundary: &ResumeBoundaryId,
479) -> Vec<ResumeExistingSlot> {
480    let mut slots = liveness
481        .slots_for_boundary(boundary)
482        .into_iter()
483        .map(|slot| slot.existing_slot.clone())
484        .collect::<Vec<_>>();
485    slots.sort();
486    slots.dedup();
487    slots
488}
489
490fn retained_slots_for_boundaries(
491    liveness: &crate::ResumeLivenessPlan,
492    boundaries: &[ResumeBoundaryId],
493) -> Vec<ResumeExistingSlot> {
494    let boundaries = boundaries.iter().collect::<BTreeSet<_>>();
495    let mut slots = liveness
496        .retained
497        .iter()
498        .filter(|record| {
499            record
500                .slot
501                .boundary_candidate
502                .as_ref()
503                .is_some_and(|boundary| boundaries.contains(boundary))
504        })
505        .map(|record| record.slot.existing_slot.clone())
506        .collect::<Vec<_>>();
507    slots.sort();
508    slots.dedup();
509    slots
510}
511
512#[must_use]
513#[allow(clippy::too_many_lines)]
514pub fn validate_resume_boundary_graph(
515    model: &ApplicationSemanticModel,
516    graph: &ResumeBoundaryGraph,
517) -> Vec<ResumeBoundaryIntegrityDiagnostic> {
518    let canonical = build_resume_boundary_graph(model);
519    let mut diagnostics = Vec::new();
520    let mut ids = BTreeSet::new();
521    for (index, boundary) in graph.boundaries.iter().enumerate() {
522        if !ids.insert(boundary.id.clone()) {
523            diagnostics.push(integrity(
524                ResumeBoundaryIntegrityCode::DuplicateBoundary,
525                Some(boundary.id.clone()),
526                "resume boundary identity was emitted more than once",
527            ));
528        }
529        if graph.boundary_index.get(&boundary.id) != Some(&index) {
530            diagnostics.push(integrity(
531                ResumeBoundaryIntegrityCode::OrderingOrIndexDrift,
532                Some(boundary.id.clone()),
533                "resume boundary index disagrees with parent-before-child order",
534            ));
535        }
536        match boundary.kind {
537            ResumeBoundaryKind::ApplicationRoot => {
538                if boundary.ownership_parent.is_some() {
539                    diagnostics.push(integrity(
540                        ResumeBoundaryIntegrityCode::MissingOrMultipleParent,
541                        Some(boundary.id.clone()),
542                        "application root boundary cannot have an ownership parent",
543                    ));
544                }
545            }
546            ResumeBoundaryKind::Interaction => {
547                if boundary.ownership_parent.is_some() {
548                    diagnostics.push(integrity(
549                        ResumeBoundaryIntegrityCode::InvalidOwner,
550                        Some(boundary.id.clone()),
551                        "interaction boundary references an owner and cannot own children",
552                    ));
553                }
554            }
555            ResumeBoundaryKind::ComponentInstance
556            | ResumeBoundaryKind::StructuralRegion
557            | ResumeBoundaryKind::FormInstance => {
558                if boundary.ownership_parent.is_none() {
559                    diagnostics.push(integrity(
560                        ResumeBoundaryIntegrityCode::MissingOrMultipleParent,
561                        Some(boundary.id.clone()),
562                        "non-root ownership boundary must have exactly one parent",
563                    ));
564                }
565            }
566        }
567        if let Some(expected) = canonical.boundary(&boundary.id) {
568            if boundary.owner != expected.owner || boundary.kind != expected.kind {
569                diagnostics.push(integrity(
570                    ResumeBoundaryIntegrityCode::InvalidOwner,
571                    Some(boundary.id.clone()),
572                    "resume boundary owner disagrees with canonical Phase H/I ownership",
573                ));
574            }
575            if boundary.provenance != expected.provenance {
576                diagnostics.push(integrity(
577                    ResumeBoundaryIntegrityCode::ProvenanceDrift,
578                    Some(boundary.id.clone()),
579                    "resume boundary provenance drifted from its canonical owner",
580                ));
581            }
582        } else {
583            diagnostics.push(integrity(
584                ResumeBoundaryIntegrityCode::PhaseCorrespondence,
585                Some(boundary.id.clone()),
586                "resume boundary has no corresponding Phase H/I product",
587            ));
588        }
589    }
590
591    let edges = graph
592        .ownership_edges
593        .iter()
594        .cloned()
595        .collect::<BTreeSet<_>>();
596    for boundary in &graph.boundaries {
597        if let Some(parent) = &boundary.ownership_parent {
598            let edge = ResumeBoundaryOwnershipEdge {
599                parent: parent.clone(),
600                child: boundary.id.clone(),
601            };
602            if !edges.contains(&edge)
603                || !graph.children(parent).contains(&boundary.id)
604                || graph.boundary(parent).is_none()
605            {
606                diagnostics.push(integrity(
607                    ResumeBoundaryIntegrityCode::Reciprocity,
608                    Some(boundary.id.clone()),
609                    "boundary parent, ownership edge, and child index are not reciprocal",
610                ));
611            }
612        }
613    }
614    for edge in &graph.ownership_edges {
615        if graph
616            .boundary(&edge.child)
617            .is_none_or(|child| child.ownership_parent.as_ref() != Some(&edge.parent))
618            || !graph.children(&edge.parent).contains(&edge.child)
619        {
620            diagnostics.push(integrity(
621                ResumeBoundaryIntegrityCode::Reciprocity,
622                Some(edge.child.clone()),
623                "ownership edge does not reciprocate the boundary parent relation",
624            ));
625        }
626    }
627
628    for boundary in &graph.boundaries {
629        if matches!(
630            boundary.kind,
631            ResumeBoundaryKind::ApplicationRoot | ResumeBoundaryKind::Interaction
632        ) {
633            continue;
634        }
635        let mut seen = BTreeSet::new();
636        let mut current = &boundary.id;
637        let mut reached_root = false;
638        while let Some(parent) = graph.parent(current) {
639            if !seen.insert(current.clone()) {
640                diagnostics.push(integrity(
641                    ResumeBoundaryIntegrityCode::Cycle,
642                    Some(boundary.id.clone()),
643                    "resume boundary ownership graph contains a cycle",
644                ));
645                break;
646            }
647            let Some(parent_boundary) = graph.boundary(parent) else {
648                break;
649            };
650            if parent_boundary.kind == ResumeBoundaryKind::ApplicationRoot {
651                reached_root = true;
652                break;
653            }
654            current = parent;
655        }
656        if !reached_root {
657            diagnostics.push(integrity(
658                ResumeBoundaryIntegrityCode::Unreachable,
659                Some(boundary.id.clone()),
660                "resume boundary is not reachable from an application root",
661            ));
662        }
663    }
664
665    for reference in &graph.activation_references {
666        if graph
667            .boundary(&reference.interaction_boundary)
668            .is_none_or(|boundary| boundary.kind != ResumeBoundaryKind::Interaction)
669            || graph.boundary(&reference.owner_boundary).is_none()
670            || reference
671                .required_boundaries
672                .iter()
673                .any(|boundary| graph.boundary(boundary).is_none())
674        {
675            diagnostics.push(integrity(
676                ResumeBoundaryIntegrityCode::InvalidOwner,
677                Some(reference.interaction_boundary.clone()),
678                "interaction activation references an unknown or non-interaction boundary",
679            ));
680        }
681    }
682
683    if graph.version != RESUME_BOUNDARY_GRAPH_VERSION
684        || graph
685            .boundaries
686            .iter()
687            .map(|boundary| &boundary.id)
688            .collect::<Vec<_>>()
689            != canonical
690                .boundaries
691                .iter()
692                .map(|boundary| &boundary.id)
693                .collect::<Vec<_>>()
694        || graph.blocks != canonical.blocks
695    {
696        diagnostics.push(integrity(
697            ResumeBoundaryIntegrityCode::PhaseCorrespondence,
698            None,
699            "resume boundary membership drifted from exact Phase H/I and J2 products",
700        ));
701    }
702    if graph != &canonical {
703        diagnostics.push(integrity(
704            ResumeBoundaryIntegrityCode::OrderingOrIndexDrift,
705            None,
706            "resume boundary graph drifted from canonical order, edges, references, or indexes",
707        ));
708    }
709    diagnostics.sort_by(|left, right| {
710        (left.code, &left.boundary, left.message.as_str()).cmp(&(
711            right.code,
712            &right.boundary,
713            right.message.as_str(),
714        ))
715    });
716    diagnostics.dedup();
717    diagnostics
718}
719
720fn integrity(
721    code: ResumeBoundaryIntegrityCode,
722    boundary: Option<ResumeBoundaryId>,
723    message: &str,
724) -> ResumeBoundaryIntegrityDiagnostic {
725    ResumeBoundaryIntegrityDiagnostic {
726        code,
727        boundary,
728        message: message.to_string(),
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    fn representative_model() -> ApplicationSemanticModel {
737        crate::build_application_semantic_model(&presolve_parser::parse_file(
738            "src/Boundary.tsx",
739            r#"
740@component("x-boundary-child") class Child extends Component {
741  count = state(1);
742  @action() increment() { this.count++; }
743  render() { return <button onClick={() => this.increment()}>{this.count}</button>; }
744}
745@component("x-boundary-profile") class Profile {
746  @form() @serialize("json") profile!: Form;
747  @field(this.profile) name = "";
748  @action() @submit(this.profile) save(): void {}
749  render() { return <form form={this.profile}><input field={this.name}/></form>; }
750}
751@component("x-boundary-page") @route("/") class Page extends Component {
752  show = state(true);
753  render() {
754    return <>
755      {this.show ? <Child /> : <span />}
756      <Profile />
757    </>;
758  }
759}"#,
760        ))
761    }
762
763    #[test]
764    fn builds_parent_before_child_boundaries_and_activation_references() {
765        let model = representative_model();
766        let graph = build_resume_boundary_graph(&model);
767        let diagnostics = validate_resume_boundary_graph(&model, &graph);
768        assert!(diagnostics.is_empty(), "{diagnostics:#?}");
769        assert_eq!(
770            graph
771                .boundaries
772                .iter()
773                .filter(|boundary| boundary.kind == ResumeBoundaryKind::ApplicationRoot)
774                .count(),
775            1
776        );
777        assert_eq!(
778            graph
779                .boundaries
780                .iter()
781                .filter(|boundary| boundary.kind == ResumeBoundaryKind::StructuralRegion)
782                .count(),
783            1
784        );
785        assert_eq!(
786            graph
787                .boundaries
788                .iter()
789                .filter(|boundary| boundary.kind == ResumeBoundaryKind::FormInstance)
790                .count(),
791            1
792        );
793        assert_eq!(graph.activation_references.len(), 2);
794        assert!(graph.activation_references.iter().all(|reference| {
795            graph.parent(&reference.interaction_boundary).is_none()
796                && graph.boundary(&reference.owner_boundary).is_some()
797        }));
798        assert!(graph.activation_references.iter().any(|reference| {
799            matches!(
800                reference.program,
801                ResumeBoundaryActivationProgram::FormSubmit { .. }
802            ) && !reference.required_retained_slots.is_empty()
803        }));
804        for edge in &graph.ownership_edges {
805            assert!(graph.boundary_index[&edge.parent] < graph.boundary_index[&edge.child]);
806        }
807    }
808
809    #[test]
810    fn preserves_blocked_component_and_liveness_products() {
811        let mut model = crate::build_application_semantic_model(&presolve_parser::parse_file(
812            "src/BlockedBoundary.tsx",
813            r#"
814@component("x-blocked-boundary") @route("/") class Page extends Component {
815  value = state(null);
816  render() { return <Missing />; }
817}"#,
818        ));
819        let state = model.components[0].state_fields[0].id.clone();
820        model
821            .semantic_types
822            .assignments
823            .get_mut(&state)
824            .expect("state assignment")
825            .semantic_type = crate::SemanticType::Unknown;
826        let graph = build_resume_boundary_graph(&model);
827        assert!(graph.blocks.iter().any(|block| matches!(
828            block.source,
829            ResumeBoundaryBlockSource::ComponentInstance { .. }
830        )));
831        assert!(
832            graph.blocks.iter().any(|block| matches!(
833                block.source,
834                ResumeBoundaryBlockSource::LivenessSlot { .. }
835            )),
836            "liveness={:#?}",
837            build_resume_liveness_plan(&model)
838        );
839        assert!(validate_resume_boundary_graph(&model, &graph).is_empty());
840    }
841
842    #[test]
843    fn boundary_graph_is_deterministic_and_rejects_noncanonical_parentage() {
844        let first = presolve_parser::parse_file(
845            "src/A.tsx",
846            r#"@component("x-a") @route("/a") class A extends Component { render() { return <div />; } }"#,
847        );
848        let second = presolve_parser::parse_file(
849            "src/B.tsx",
850            r#"@component("x-b") @route("/b") class B extends Component { render() { return <div />; } }"#,
851        );
852        let forward = crate::build_application_semantic_model_for_unit(
853            &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
854        );
855        let reverse = crate::build_application_semantic_model_for_unit(
856            &crate::CompilationUnit::from_parsed_files(vec![second, first]),
857        );
858        assert_eq!(
859            build_resume_boundary_graph(&forward),
860            build_resume_boundary_graph(&reverse)
861        );
862        let mut invalid = build_resume_boundary_graph(&forward);
863        let child = invalid
864            .boundaries
865            .iter_mut()
866            .find(|boundary| boundary.kind == ResumeBoundaryKind::ComponentInstance)
867            .expect("component boundary");
868        child.ownership_parent = None;
869        let diagnostics = validate_resume_boundary_graph(&forward, &invalid);
870        assert!(diagnostics.iter().any(|diagnostic| {
871            diagnostic.code == ResumeBoundaryIntegrityCode::MissingOrMultipleParent
872        }));
873        assert!(diagnostics
874            .iter()
875            .any(|diagnostic| { diagnostic.code == ResumeBoundaryIntegrityCode::Reciprocity }));
876    }
877
878    #[test]
879    fn reserves_the_complete_j3_integrity_range() {
880        assert_eq!(
881            [
882                ResumeBoundaryIntegrityCode::DuplicateBoundary,
883                ResumeBoundaryIntegrityCode::InvalidOwner,
884                ResumeBoundaryIntegrityCode::MissingOrMultipleParent,
885                ResumeBoundaryIntegrityCode::Cycle,
886                ResumeBoundaryIntegrityCode::Unreachable,
887                ResumeBoundaryIntegrityCode::Reciprocity,
888                ResumeBoundaryIntegrityCode::PhaseCorrespondence,
889                ResumeBoundaryIntegrityCode::ProvenanceDrift,
890                ResumeBoundaryIntegrityCode::OrderingOrIndexDrift,
891            ]
892            .map(ResumeBoundaryIntegrityCode::code),
893            [
894                "PSASM1328",
895                "PSASM1329",
896                "PSASM1330",
897                "PSASM1331",
898                "PSASM1332",
899                "PSASM1333",
900                "PSASM1334",
901                "PSASM1335",
902                "PSASM1336",
903            ]
904        );
905    }
906}