1use 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, ®ion);
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}