1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 build_computed_instance_slot_registry, build_resume_boundary_graph, build_resume_liveness_plan,
9 build_runtime_component_registry, build_state_instance_storage_registry,
10 lower_components_to_ir, ApplicationSemanticModel, ResumeBoundaryId, ResumeExistingSlot,
11 ResumeLivenessPlan, ResumeSchemaId, ResumeSlotId, SemanticType, SourceProvenance,
12};
13
14pub const RESUME_SCHEMA_REGISTRY_VERSION: u32 = 1;
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct ResumeObjectPropertyCodec {
18 pub name: String,
19 pub codec: ResumeValueCodec,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
23#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
24pub enum ResumeValueCodec {
25 NullCodec,
26 BooleanCodec,
27 NumberCodec,
28 StringCodec,
29 ArrayCodec(Box<ResumeValueCodec>),
30 ObjectCodec(Vec<ResumeObjectPropertyCodec>),
31 NullableCodec(Box<ResumeValueCodec>),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ResumeSlotSchema {
36 pub resume_slot_id: ResumeSlotId,
37 pub existing_slot: ResumeExistingSlot,
38 pub semantic_type: SemanticType,
39 pub codec: ResumeValueCodec,
40 pub provenance: SourceProvenance,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ResumeBoundarySchema {
45 pub id: ResumeSchemaId,
46 pub boundary: ResumeBoundaryId,
47 pub slots: Vec<ResumeSlotSchema>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
51pub enum ResumeSchemaBlockReason {
52 UpstreamLivenessBlock,
53 MissingCanonicalSlotType,
54 MalformedSemanticType,
55 UnsupportedValue,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ResumeSchemaBlock {
60 pub boundary: Option<ResumeBoundaryId>,
61 pub slot: ResumeExistingSlot,
62 pub reason: ResumeSchemaBlockReason,
63 pub semantic_type: Option<SemanticType>,
64 pub provenance: SourceProvenance,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ResumeSchemaRegistry {
69 pub version: u32,
70 pub schemas: Vec<ResumeBoundarySchema>,
71 pub blocks: Vec<ResumeSchemaBlock>,
72 pub schema_index: BTreeMap<ResumeBoundaryId, usize>,
73 pub slot_index: BTreeMap<ResumeExistingSlot, ResumeSchemaId>,
74}
75
76impl ResumeSchemaRegistry {
77 #[must_use]
78 pub fn schema(&self, boundary: &ResumeBoundaryId) -> Option<&ResumeBoundarySchema> {
79 self.schema_index
80 .get(boundary)
81 .and_then(|index| self.schemas.get(*index))
82 }
83
84 #[must_use]
85 pub fn schema_for_slot(&self, slot: &ResumeExistingSlot) -> Option<&ResumeBoundarySchema> {
86 self.slot_index.get(slot).and_then(|schema| {
87 self.schemas
88 .iter()
89 .find(|candidate| &candidate.id == schema)
90 })
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
95pub enum ResumeSchemaIntegrityCode {
96 MalformedSemanticType,
97 DuplicateProperty,
98 UnsupportedValue,
99 MissingSlot,
100 IdentityCollision,
101 OrderingOrIndexDrift,
102}
103
104impl ResumeSchemaIntegrityCode {
105 #[must_use]
106 pub const fn code(self) -> &'static str {
107 match self {
108 Self::MalformedSemanticType => "PSASM1349",
109 Self::DuplicateProperty => "PSASM1350",
110 Self::UnsupportedValue => "PSASM1351",
111 Self::MissingSlot => "PSASM1352",
112 Self::IdentityCollision => "PSASM1353",
113 Self::OrderingOrIndexDrift => "PSASM1354",
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct ResumeSchemaIntegrityDiagnostic {
120 pub code: ResumeSchemaIntegrityCode,
121 pub boundary: Option<ResumeBoundaryId>,
122 pub slot: Option<ResumeExistingSlot>,
123 pub message: String,
124}
125
126pub fn resume_value_codec(
131 semantic_type: &SemanticType,
132) -> Result<ResumeValueCodec, ResumeSchemaBlockReason> {
133 match semantic_type {
134 SemanticType::Null => Ok(ResumeValueCodec::NullCodec),
135 SemanticType::Boolean | SemanticType::BooleanLiteral(_) => {
136 Ok(ResumeValueCodec::BooleanCodec)
137 }
138 SemanticType::Number | SemanticType::NumberLiteral(_) => Ok(ResumeValueCodec::NumberCodec),
139 SemanticType::String | SemanticType::StringLiteral(_) => Ok(ResumeValueCodec::StringCodec),
140 SemanticType::Array(element) => resume_value_codec(element)
141 .map(Box::new)
142 .map(ResumeValueCodec::ArrayCodec),
143 SemanticType::Object(object) => object
144 .properties
145 .iter()
146 .map(|(name, property)| {
147 Ok(ResumeObjectPropertyCodec {
148 name: name.clone(),
149 codec: resume_value_codec(property)?,
150 })
151 })
152 .collect::<Result<Vec<_>, ResumeSchemaBlockReason>>()
153 .map(ResumeValueCodec::ObjectCodec),
154 SemanticType::Union(members) => nullable_codec(members),
155 SemanticType::Unknown
156 | SemanticType::Never
157 | SemanticType::Form
158 | SemanticType::SlotContent => Err(ResumeSchemaBlockReason::MalformedSemanticType),
159 SemanticType::Tuple(_) | SemanticType::Resource(_) => {
160 Err(ResumeSchemaBlockReason::UnsupportedValue)
161 }
162 }
163}
164
165fn nullable_codec(members: &[SemanticType]) -> Result<ResumeValueCodec, ResumeSchemaBlockReason> {
166 if members.is_empty() {
167 return Err(ResumeSchemaBlockReason::MalformedSemanticType);
168 }
169 let non_null = members
170 .iter()
171 .filter(|member| !matches!(member, SemanticType::Null))
172 .collect::<Vec<_>>();
173 let null_count = members.len() - non_null.len();
174 if null_count != 1 || non_null.len() != 1 {
175 return Err(ResumeSchemaBlockReason::UnsupportedValue);
176 }
177 resume_value_codec(non_null[0])
178 .map(Box::new)
179 .map(ResumeValueCodec::NullableCodec)
180}
181
182#[must_use]
183#[allow(clippy::too_many_lines)]
184pub fn build_resume_schema_registry(model: &ApplicationSemanticModel) -> ResumeSchemaRegistry {
185 let boundaries = build_resume_boundary_graph(model);
186 let liveness = build_resume_liveness_plan(model);
187 let slot_types = ResumeSlotTypeAuthority::new(model);
188 let mut schemas = boundaries
189 .boundaries
190 .iter()
191 .map(|boundary| ResumeBoundarySchema {
192 id: ResumeSchemaId::for_boundary(&boundary.id),
193 boundary: boundary.id.clone(),
194 slots: Vec::new(),
195 })
196 .collect::<Vec<_>>();
197 let schema_index = schemas
198 .iter()
199 .enumerate()
200 .map(|(index, schema)| (schema.boundary.clone(), index))
201 .collect::<BTreeMap<_, _>>();
202 let mut blocks = Vec::new();
203
204 for slot in capture_eligible_slots(&liveness) {
205 let boundary = slot.boundary_candidate.clone();
206 let Some(boundary_id) = boundary.as_ref() else {
207 blocks.push(ResumeSchemaBlock {
208 boundary,
209 slot: slot.existing_slot.clone(),
210 reason: ResumeSchemaBlockReason::MissingCanonicalSlotType,
211 semantic_type: None,
212 provenance: slot.provenance.clone(),
213 });
214 continue;
215 };
216 let Some(index) = schema_index.get(boundary_id).copied() else {
217 blocks.push(ResumeSchemaBlock {
218 boundary,
219 slot: slot.existing_slot.clone(),
220 reason: ResumeSchemaBlockReason::MissingCanonicalSlotType,
221 semantic_type: None,
222 provenance: slot.provenance.clone(),
223 });
224 continue;
225 };
226 let Some(semantic_type) = slot_types.semantic_type(&slot.existing_slot) else {
227 blocks.push(ResumeSchemaBlock {
228 boundary: Some(boundary_id.clone()),
229 slot: slot.existing_slot.clone(),
230 reason: ResumeSchemaBlockReason::MissingCanonicalSlotType,
231 semantic_type: None,
232 provenance: slot.provenance.clone(),
233 });
234 continue;
235 };
236 match resume_value_codec(&semantic_type) {
237 Ok(codec) => schemas[index].slots.push(ResumeSlotSchema {
238 resume_slot_id: slot.resume_slot_id.clone(),
239 existing_slot: slot.existing_slot.clone(),
240 semantic_type,
241 codec,
242 provenance: slot.provenance.clone(),
243 }),
244 Err(reason) => blocks.push(ResumeSchemaBlock {
245 boundary: Some(boundary_id.clone()),
246 slot: slot.existing_slot.clone(),
247 reason,
248 semantic_type: Some(semantic_type),
249 provenance: slot.provenance.clone(),
250 }),
251 }
252 }
253
254 for blocked in &liveness.blocked {
255 blocks.push(ResumeSchemaBlock {
256 boundary: blocked.slot.boundary_candidate.clone(),
257 slot: blocked.slot.existing_slot.clone(),
258 reason: ResumeSchemaBlockReason::UpstreamLivenessBlock,
259 semantic_type: slot_types.semantic_type(&blocked.slot.existing_slot),
260 provenance: blocked.slot.provenance.clone(),
261 });
262 }
263
264 for schema in &mut schemas {
265 schema.slots.sort_by(|left, right| {
266 (&left.resume_slot_id, &left.existing_slot)
267 .cmp(&(&right.resume_slot_id, &right.existing_slot))
268 });
269 }
270 blocks.sort_by(|left, right| {
271 (&left.boundary, &left.slot, left.reason).cmp(&(&right.boundary, &right.slot, right.reason))
272 });
273 let slot_index = schemas
274 .iter()
275 .flat_map(|schema| {
276 schema
277 .slots
278 .iter()
279 .map(|slot| (slot.existing_slot.clone(), schema.id.clone()))
280 })
281 .collect();
282
283 ResumeSchemaRegistry {
284 version: RESUME_SCHEMA_REGISTRY_VERSION,
285 schemas,
286 blocks,
287 schema_index,
288 slot_index,
289 }
290}
291
292fn capture_eligible_slots(liveness: &ResumeLivenessPlan) -> Vec<&crate::ResumeLivenessSlot> {
293 let mut slots = liveness
294 .retained
295 .iter()
296 .map(|record| &record.slot)
297 .chain(liveness.recomputable.iter().map(|record| &record.slot))
298 .collect::<Vec<_>>();
299 slots.sort_by(|left, right| left.existing_slot.cmp(&right.existing_slot));
300 slots
301}
302
303struct ResumeSlotTypeAuthority {
304 types: BTreeMap<ResumeExistingSlot, SemanticType>,
305}
306
307impl ResumeSlotTypeAuthority {
308 fn new(model: &ApplicationSemanticModel) -> Self {
309 let ir = lower_components_to_ir(model);
310 let mut types = BTreeMap::new();
311 for record in build_state_instance_storage_registry(model, &ir).records {
312 types.insert(
313 ResumeExistingSlot::State(record.slot_id),
314 record.semantic_type,
315 );
316 }
317 for record in build_computed_instance_slot_registry(model, &ir).records {
318 if let Some(semantic_type) = model.semantic_type_of(&record.computed_id) {
319 types.insert(
320 ResumeExistingSlot::ComputedCache(record.cache_slot_id),
321 semantic_type.clone(),
322 );
323 }
324 types.insert(
325 ResumeExistingSlot::ComputedDirty(record.dirty_slot_id),
326 SemanticType::Boolean,
327 );
328 }
329 let runtime_components =
330 build_runtime_component_registry(model, &model.component_ir_optimization);
331 for binding in runtime_components.instance_context_bindings {
332 if let Some(semantic_type) =
333 model.semantic_type_of(binding.selected_source.context.as_semantic_id())
334 {
335 types.insert(
336 ResumeExistingSlot::Context(binding.runtime_slot),
337 semantic_type.clone(),
338 );
339 }
340 }
341 for instance in model.optimized_form_ir.optimized.instances.values() {
342 for (field_id, slot) in &instance.storage.value {
343 if let Some(field) = model.form_fields.get(field_id) {
344 types.insert(
345 ResumeExistingSlot::FormFieldValue(slot.clone()),
346 field.semantic_type.clone(),
347 );
348 }
349 }
350 for slot in instance.storage.dirty.values() {
351 types.insert(
352 ResumeExistingSlot::FormFieldDirty(slot.clone()),
353 SemanticType::Boolean,
354 );
355 }
356 for slot in instance.storage.touched.values() {
357 types.insert(
358 ResumeExistingSlot::FormFieldTouched(slot.clone()),
359 SemanticType::Boolean,
360 );
361 }
362 for slot in instance.storage.validation.values() {
363 types.insert(
364 ResumeExistingSlot::FormFieldValidation(slot.clone()),
365 SemanticType::Array(Box::new(SemanticType::String)),
366 );
367 }
368 types.insert(
369 ResumeExistingSlot::FormValidationAggregate(instance.storage.aggregate.clone()),
370 SemanticType::Boolean,
371 );
372 types.insert(
373 ResumeExistingSlot::FormSubmission(instance.storage.submission.clone()),
374 SemanticType::String,
375 );
376 }
377 Self { types }
378 }
379
380 fn semantic_type(&self, slot: &ResumeExistingSlot) -> Option<SemanticType> {
381 self.types.get(slot).cloned()
382 }
383}
384
385pub fn validate_resume_schema_registry(
390 model: &ApplicationSemanticModel,
391 registry: &ResumeSchemaRegistry,
392) -> Result<(), Vec<ResumeSchemaIntegrityDiagnostic>> {
393 let mut diagnostics = Vec::new();
394 let boundaries = build_resume_boundary_graph(model);
395 let liveness = build_resume_liveness_plan(model);
396 let expected_boundary_order = boundaries
397 .boundaries
398 .iter()
399 .map(|boundary| boundary.id.clone())
400 .collect::<Vec<_>>();
401 let actual_boundary_order = registry
402 .schemas
403 .iter()
404 .map(|schema| schema.boundary.clone())
405 .collect::<Vec<_>>();
406 if expected_boundary_order != actual_boundary_order {
407 diagnostics.push(diagnostic(
408 ResumeSchemaIntegrityCode::OrderingOrIndexDrift,
409 None,
410 None,
411 "resume schemas do not preserve canonical parent-before-child boundary order",
412 ));
413 }
414
415 let mut schema_ids = BTreeSet::new();
416 let mut resume_slot_ids = BTreeSet::new();
417 let mut existing_slots = BTreeSet::new();
418 for schema in ®istry.schemas {
419 if schema.id != ResumeSchemaId::for_boundary(&schema.boundary)
420 || !schema_ids.insert(schema.id.clone())
421 {
422 diagnostics.push(diagnostic(
423 ResumeSchemaIntegrityCode::IdentityCollision,
424 Some(schema.boundary.clone()),
425 None,
426 "resume schema identity is not unique and boundary-derived",
427 ));
428 }
429 for slot in &schema.slots {
430 if slot.resume_slot_id != slot.existing_slot.resume_slot_id()
431 || !resume_slot_ids.insert(slot.resume_slot_id.clone())
432 || !existing_slots.insert(slot.existing_slot.clone())
433 {
434 diagnostics.push(diagnostic(
435 ResumeSchemaIntegrityCode::IdentityCollision,
436 Some(schema.boundary.clone()),
437 Some(slot.existing_slot.clone()),
438 "resume slot identity is not unique and storage-derived",
439 ));
440 }
441 validate_codec(
442 &slot.codec,
443 &schema.boundary,
444 &slot.existing_slot,
445 &mut diagnostics,
446 );
447 }
448 }
449
450 let expected_slots = capture_eligible_slots(&liveness)
451 .into_iter()
452 .map(|slot| slot.existing_slot.clone())
453 .collect::<BTreeSet<_>>();
454 let blocked_slots = registry
455 .blocks
456 .iter()
457 .map(|block| block.slot.clone())
458 .collect::<BTreeSet<_>>();
459 if !expected_slots
460 .iter()
461 .all(|slot| existing_slots.contains(slot) || blocked_slots.contains(slot))
462 {
463 diagnostics.push(diagnostic(
464 ResumeSchemaIntegrityCode::MissingSlot,
465 None,
466 None,
467 "capture-eligible liveness slot lacks a schema or explicit schema block",
468 ));
469 }
470 if registry.version != RESUME_SCHEMA_REGISTRY_VERSION
471 || registry != &build_resume_schema_registry(model)
472 {
473 diagnostics.push(diagnostic(
474 ResumeSchemaIntegrityCode::OrderingOrIndexDrift,
475 None,
476 None,
477 "resume schema registry drifted from canonical ordering or indexes",
478 ));
479 }
480
481 if diagnostics.is_empty() {
482 Ok(())
483 } else {
484 Err(diagnostics)
485 }
486}
487
488fn validate_codec(
489 codec: &ResumeValueCodec,
490 boundary: &ResumeBoundaryId,
491 slot: &ResumeExistingSlot,
492 diagnostics: &mut Vec<ResumeSchemaIntegrityDiagnostic>,
493) {
494 match codec {
495 ResumeValueCodec::ArrayCodec(element) | ResumeValueCodec::NullableCodec(element) => {
496 validate_codec(element, boundary, slot, diagnostics);
497 }
498 ResumeValueCodec::ObjectCodec(properties) => {
499 let names = properties
500 .iter()
501 .map(|property| property.name.as_str())
502 .collect::<Vec<_>>();
503 if names.windows(2).any(|pair| pair[0] == pair[1]) {
504 diagnostics.push(diagnostic(
505 ResumeSchemaIntegrityCode::DuplicateProperty,
506 Some(boundary.clone()),
507 Some(slot.clone()),
508 "resume object codec contains a duplicate property",
509 ));
510 }
511 if names.windows(2).any(|pair| pair[0] > pair[1]) {
512 diagnostics.push(diagnostic(
513 ResumeSchemaIntegrityCode::OrderingOrIndexDrift,
514 Some(boundary.clone()),
515 Some(slot.clone()),
516 "resume object codec properties are not in canonical order",
517 ));
518 }
519 for property in properties {
520 validate_codec(&property.codec, boundary, slot, diagnostics);
521 }
522 }
523 ResumeValueCodec::NullCodec
524 | ResumeValueCodec::BooleanCodec
525 | ResumeValueCodec::NumberCodec
526 | ResumeValueCodec::StringCodec => {}
527 }
528}
529
530fn diagnostic(
531 code: ResumeSchemaIntegrityCode,
532 boundary: Option<ResumeBoundaryId>,
533 slot: Option<ResumeExistingSlot>,
534 message: &str,
535) -> ResumeSchemaIntegrityDiagnostic {
536 ResumeSchemaIntegrityDiagnostic {
537 code,
538 boundary,
539 slot,
540 message: message.to_string(),
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use crate::ObjectType;
548
549 fn model(source: &str) -> ApplicationSemanticModel {
550 crate::build_application_semantic_model(&presolve_parser::parse_file(
551 "src/ResumeSchema.tsx",
552 source,
553 ))
554 }
555
556 #[test]
557 fn derives_closed_codecs_with_canonical_object_order_and_explicit_nullable() {
558 let object = SemanticType::Object(ObjectType {
559 properties: BTreeMap::from([
560 ("b".to_string(), SemanticType::String),
561 ("a".to_string(), SemanticType::Number),
562 ]),
563 });
564 assert_eq!(
565 resume_value_codec(&object),
566 Ok(ResumeValueCodec::ObjectCodec(vec![
567 ResumeObjectPropertyCodec {
568 name: "a".to_string(),
569 codec: ResumeValueCodec::NumberCodec,
570 },
571 ResumeObjectPropertyCodec {
572 name: "b".to_string(),
573 codec: ResumeValueCodec::StringCodec,
574 },
575 ]))
576 );
577 assert_eq!(
578 resume_value_codec(&SemanticType::Union(vec![
579 SemanticType::Null,
580 SemanticType::String,
581 ])),
582 Ok(ResumeValueCodec::NullableCodec(Box::new(
583 ResumeValueCodec::StringCodec
584 )))
585 );
586 assert_eq!(
587 resume_value_codec(&SemanticType::Union(vec![
588 SemanticType::String,
589 SemanticType::Number,
590 ])),
591 Err(ResumeSchemaBlockReason::UnsupportedValue)
592 );
593 }
594
595 #[test]
596 fn creates_one_schema_per_boundary_with_exact_slot_reciprocity() {
597 let model = model(
598 r#"@component("x-counter") class Counter { count = state(1); render() { return <button>{this.count}</button>; } }"#,
599 );
600 let graph = build_resume_boundary_graph(&model);
601 let liveness = build_resume_liveness_plan(&model);
602 let registry = build_resume_schema_registry(&model);
603 assert_eq!(registry.schemas.len(), graph.boundaries.len());
604 assert_eq!(
605 registry
606 .schemas
607 .iter()
608 .map(|schema| &schema.boundary)
609 .collect::<Vec<_>>(),
610 graph
611 .boundaries
612 .iter()
613 .map(|boundary| &boundary.id)
614 .collect::<Vec<_>>()
615 );
616 let expected = capture_eligible_slots(&liveness)
617 .into_iter()
618 .map(|slot| slot.existing_slot.clone())
619 .collect::<BTreeSet<_>>();
620 let actual = registry.slot_index.keys().cloned().collect::<BTreeSet<_>>();
621 assert_eq!(actual, expected);
622 assert!(registry.blocks.is_empty());
623 assert_eq!(validate_resume_schema_registry(&model, ®istry), Ok(()));
624 }
625
626 #[test]
627 fn freezes_form_runtime_slot_codecs_without_runtime_guessing() {
628 let model = model(
629 r#"@component("x-profile") class Profile {
630 @form() profile!: Form;
631 @field(this.profile) name = "";
632 render() { return <input field={this.name} />; }
633}"#,
634 );
635 let registry = build_resume_schema_registry(&model);
636 let codecs = registry
637 .schemas
638 .iter()
639 .flat_map(|schema| &schema.slots)
640 .map(|slot| (&slot.existing_slot, &slot.codec))
641 .collect::<BTreeMap<_, _>>();
642 assert!(codecs.iter().any(|(slot, codec)| matches!(
643 (slot, codec),
644 (
645 ResumeExistingSlot::FormFieldValidation(_),
646 ResumeValueCodec::ArrayCodec(element)
647 ) if **element == ResumeValueCodec::StringCodec
648 )));
649 assert!(codecs.iter().any(|(slot, codec)| matches!(
650 (slot, codec),
651 (
652 ResumeExistingSlot::FormValidationAggregate(_),
653 ResumeValueCodec::BooleanCodec
654 )
655 )));
656 assert!(codecs.iter().any(|(slot, codec)| matches!(
657 (slot, codec),
658 (
659 ResumeExistingSlot::FormSubmission(_),
660 ResumeValueCodec::StringCodec
661 )
662 )));
663 }
664
665 #[test]
666 fn integrity_rejects_identity_and_ordering_drift() {
667 let model = model(
668 r#"@component("x-counter") class Counter { count = state(1); render() { return <button>{this.count}</button>; } }"#,
669 );
670 let mut registry = build_resume_schema_registry(&model);
671 registry.schemas.reverse();
672 let diagnostics = validate_resume_schema_registry(&model, ®istry).expect_err("drift");
673 assert!(diagnostics.iter().any(|diagnostic| {
674 diagnostic.code == ResumeSchemaIntegrityCode::OrderingOrIndexDrift
675 }));
676 }
677
678 #[test]
679 fn schema_registry_is_deterministic_under_source_reversal() {
680 let first = presolve_parser::parse_file(
681 "src/A.tsx",
682 r#"@component("x-a") @route("/a") class A { value = state({ b: "two", a: 1 }); render() { return <button>{this.value}</button>; } }"#,
683 );
684 let second = presolve_parser::parse_file(
685 "src/B.tsx",
686 r#"@component("x-b") @route("/b") class B { enabled = state(true); render() { return <button>{this.enabled}</button>; } }"#,
687 );
688 let forward = crate::build_application_semantic_model_for_unit(
689 &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
690 );
691 let reverse = crate::build_application_semantic_model_for_unit(
692 &crate::CompilationUnit::from_parsed_files(vec![second, first]),
693 );
694 assert_eq!(
695 build_resume_schema_registry(&forward),
696 build_resume_schema_registry(&reverse)
697 );
698 }
699
700 #[test]
701 fn schema_order_keeps_nested_boundaries_parent_before_child() {
702 let model = model(
703 r#"@component("x-child") class Child { value = state(1); render() { return <span>{this.value}</span>; } }
704@component("x-parent") @route("/") class Parent { render() { return <Child />; } }"#,
705 );
706 let graph = build_resume_boundary_graph(&model);
707 let registry = build_resume_schema_registry(&model);
708 assert_eq!(
709 registry
710 .schemas
711 .iter()
712 .map(|schema| &schema.boundary)
713 .collect::<Vec<_>>(),
714 graph
715 .boundaries
716 .iter()
717 .map(|boundary| &boundary.id)
718 .collect::<Vec<_>>()
719 );
720 }
721
722 #[test]
723 fn reserves_the_complete_j6_integrity_range() {
724 assert_eq!(
725 [
726 ResumeSchemaIntegrityCode::MalformedSemanticType,
727 ResumeSchemaIntegrityCode::DuplicateProperty,
728 ResumeSchemaIntegrityCode::UnsupportedValue,
729 ResumeSchemaIntegrityCode::MissingSlot,
730 ResumeSchemaIntegrityCode::IdentityCollision,
731 ResumeSchemaIntegrityCode::OrderingOrIndexDrift,
732 ]
733 .map(ResumeSchemaIntegrityCode::code),
734 [
735 "PSASM1349",
736 "PSASM1350",
737 "PSASM1351",
738 "PSASM1352",
739 "PSASM1353",
740 "PSASM1354",
741 ]
742 );
743 }
744}