Skip to main content

presolve_compiler/
application_semantic_model.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::Path;
3
4use presolve_parser::ParsedFile;
5
6use crate::compilation_unit::CompilationUnit;
7use crate::component_composition::{analyze_component_composition, ComponentCompositionAnalysis};
8use crate::component_graph::{
9    build_component_graph_for_module, build_v2_component_graph_for_module, render_event_handlers,
10    ActionEndpoint, ComponentAction, ComponentDiagnostic, ComponentDiagnosticSeverity,
11    ComponentMethod, ComponentNode, ComputedExpression, ComputedExpressionKind,
12    MethodLocalVariable, RenderEventHandler, StateField,
13};
14use crate::component_initialization::{plan_component_initialization, ComponentInitializationPlan};
15use crate::component_instance::{
16    plan_component_instances, plan_component_instances_with_virtual_invocations,
17    BlockedComponentInstancePlan, ComponentInstance, ComponentInstancePlan,
18};
19use crate::component_instance_scope::{
20    build_component_instance_scope_graph, ComponentInstanceScopeGraph,
21};
22use crate::component_invocation::{collect_component_invocations, ComponentInvocationEntity};
23use crate::component_ir::{lower_component_ir, ComponentIrReport};
24use crate::component_ir_optimization::{optimize_component_ir, OptimizedComponentIrReport};
25use crate::component_scope::ComponentScopeGraph;
26use crate::composition_typing::{collect_composition_type_products, CompositionTypeProducts};
27use crate::computed_value::{collect_computed_values, ComputedDiagnosticCode, ComputedValue};
28use crate::consumer::{collect_consumer_entities, ConsumerEntity, ContextResolutionState};
29use crate::context::{collect_context_entities, ContextEntity};
30use crate::context_declaration_candidate::{
31    collect_context_declaration_candidates, ContextDeclarationCandidateRegistry,
32};
33use crate::context_dependency::{collect_context_dependency_graph, ContextDependencyGraph};
34use crate::context_evaluation::{collect_context_evaluation_plan, ContextEvaluationPlan};
35use crate::context_lifetime::{collect_context_lifetime_analysis, ContextLifetimeAnalysis};
36use crate::context_ownership::{collect_context_ownership_graph, ContextOwnershipGraph};
37use crate::context_resolution::{
38    collect_context_resolutions, ContextResolution, ContextResolutionResult,
39};
40use crate::context_typing::{
41    collect_context_type_products, ConsumerTypeRecord, ContextBindingCompatibility,
42    ContextBindingTypeRecord, ContextTypeRecord, ProviderTypeRecord,
43};
44use crate::expression_graph::{ExpressionGraph, ExpressionNode, ExpressionNodeKind};
45use crate::form::{collect_form_entities, FormEntity};
46use crate::form_binding::{
47    collect_form_field_binding_products, FormFieldBinding, FormFieldBindingCandidate,
48};
49use crate::form_field::{collect_form_field_products, FormFieldEntity};
50use crate::form_ir::{lower_form_ir, FormIrReport};
51use crate::form_ir_optimization::{optimize_form_ir, OptimizedFormIrReport};
52use crate::form_ownership::{collect_form_ownership_graph, FormOwnershipGraph};
53use crate::form_reset::{collect_reset_products, ResetProducts};
54use crate::form_serialization::{collect_serialization_products, SerializationProducts};
55use crate::form_submission::{collect_submission_products, SubmissionProducts};
56use crate::form_submission_host::{
57    collect_submission_host_products, SubmissionHost, SubmissionHostCandidate,
58};
59use crate::form_tracking::{collect_form_tracking_products, FormTrackingProducts};
60use crate::form_validation::{
61    collect_validation_graph, collect_validation_products, ValidationGraph, ValidationRule,
62    ValidationRuleCandidate,
63};
64use crate::form_validation_plan::{collect_validation_dependency_plans, ValidationDependencyPlans};
65use crate::instance_context::{collect_instance_context_registry, InstanceContextRegistry};
66use crate::intermediate_representation::{
67    IrComputedEvaluationPlan, IrReactiveCycleAnalysis, IrReactiveGraph,
68    IrReactiveTransitiveAnalysis,
69};
70use crate::provider::{collect_provider_entities, DuplicateProviderDeclaration, ProviderEntity};
71use crate::runtime_form_registry::{build_runtime_form_registry, RuntimeFormRegistry};
72use crate::semantic_id::{
73    ComponentInvocationId, ConsumerId, ContextId, FieldBindingId, FieldId, FormId, ProviderId,
74    ResourceActivationId, ResourceId, SemanticId, SemanticOwner, SlotBindingId,
75    SlotContentFragmentId, SlotId, SlotOutletId, ValidationRuleId,
76};
77use crate::semantic_provenance::SourceProvenance;
78use crate::semantic_reference::{SemanticReference, SemanticReferenceKind};
79use crate::semantic_type::{EffectStatementTypeRecord, SemanticTypeModel};
80use crate::slot::{collect_slot_entities, SlotEntity};
81use crate::slot_binding::{
82    collect_slot_bindings, collect_slot_bindings_with_virtual_invocations, SlotBinding,
83    SlotBindingRegistry,
84};
85use crate::slot_content::{collect_slot_composition, SlotContentFragment, SlotOutlet};
86use crate::template_graph::{build_template_graph, TemplateNode};
87use crate::template_semantics::{
88    build_template_semantic_entities, TemplateSemanticEntity, TemplateSemanticKind,
89    TemplateSemanticScope,
90};
91use crate::{
92    analyze_effect_reactivity, collect_effects, derive_effect_trigger_plan, lower_effect_bodies,
93    plan_effect_execution, validate_effects, Effect, EffectBody, EffectExecutionPlan,
94    EffectReactiveAnalysis, EffectStatement, EffectTriggerPlan,
95};
96
97/// Stable failure returned while assembling the compiler-owned semantic model
98/// for a conventional `app/routes` application.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct FileRouteApplicationModelErrorV1 {
101    pub code: &'static str,
102    pub message: String,
103}
104
105/// Application-level semantic data assembled from the compiler's existing graphs.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ApplicationSemanticModel {
108    pub expression_graph: ExpressionGraph,
109    pub semantic_types: SemanticTypeModel,
110    pub components: Vec<ComponentNode>,
111    pub contexts: BTreeMap<ContextId, ContextEntity>,
112    pub providers: BTreeMap<ProviderId, ProviderEntity>,
113    pub consumers: BTreeMap<ConsumerId, ConsumerEntity>,
114    pub forms: BTreeMap<FormId, FormEntity>,
115    /// Integrity-checked package endpoint selections for Resource declaration
116    /// and activation lowering.
117    pub resource_endpoint_resolutions: Vec<crate::ResourceEndpointResolution>,
118    /// Integrity-checked opaque terminal package selections. These are
119    /// resolution facts only until a later artifact/runtime lowering product
120    /// consumes them.
121    pub opaque_action_resolutions: Vec<crate::OpaqueActionResolution>,
122    /// Validated Resource declaration products projected into the canonical
123    /// runtime activation artifact when a host supplies exact module locations.
124    pub resource_declarations: BTreeMap<ResourceId, crate::ResourceDeclaration>,
125    pub resource_activations: BTreeMap<ResourceActivationId, crate::ResourceActivation>,
126    pub form_field_declaration_candidates: Vec<crate::FormFieldDeclarationCandidate>,
127    pub form_fields: BTreeMap<FieldId, FormFieldEntity>,
128    pub form_field_binding_candidates: Vec<FormFieldBindingCandidate>,
129    pub form_field_bindings: BTreeMap<FieldBindingId, FormFieldBinding>,
130    pub form_ownership: FormOwnershipGraph,
131    pub validation_rule_candidates: Vec<ValidationRuleCandidate>,
132    pub validation_rules: BTreeMap<ValidationRuleId, ValidationRule>,
133    pub validation_graph: ValidationGraph,
134    pub validation_dependency_plans: ValidationDependencyPlans,
135    pub form_tracking: FormTrackingProducts,
136    pub submissions: SubmissionProducts,
137    pub submission_host_candidates: Vec<SubmissionHostCandidate>,
138    pub submission_hosts: BTreeMap<crate::SubmissionHostId, SubmissionHost>,
139    pub serialization: SerializationProducts,
140    pub reset: ResetProducts,
141    pub form_ir: FormIrReport,
142    pub optimized_form_ir: OptimizedFormIrReport,
143    pub runtime_forms: RuntimeFormRegistry,
144    pub slots: BTreeMap<SlotId, SlotEntity>,
145    pub component_invocations: BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
146    pub component_instance_plan: ComponentInstancePlan,
147    pub component_instance_scope: ComponentInstanceScopeGraph,
148    pub component_composition: ComponentCompositionAnalysis,
149    pub component_initialization: ComponentInitializationPlan,
150    pub component_ir: ComponentIrReport,
151    pub component_ir_optimization: OptimizedComponentIrReport,
152    pub instance_context: InstanceContextRegistry,
153    pub slot_content_fragments: BTreeMap<SlotContentFragmentId, SlotContentFragment>,
154    pub slot_outlets: BTreeMap<SlotOutletId, SlotOutlet>,
155    pub slot_bindings: SlotBindingRegistry,
156    pub composition_types: CompositionTypeProducts,
157    pub context_declaration_candidates: ContextDeclarationCandidateRegistry,
158    pub context_ownership: ContextOwnershipGraph,
159    pub context_dependency: ContextDependencyGraph,
160    pub context_lifetime: ContextLifetimeAnalysis,
161    pub context_evaluation: ContextEvaluationPlan,
162    pub component_scope: ComponentScopeGraph,
163    pub context_resolutions: BTreeMap<ConsumerId, ContextResolution>,
164    pub context_types: BTreeMap<ContextId, ContextTypeRecord>,
165    pub provider_types: BTreeMap<ProviderId, ProviderTypeRecord>,
166    pub consumer_types: BTreeMap<ConsumerId, ConsumerTypeRecord>,
167    pub context_binding_types: BTreeMap<ConsumerId, ContextBindingTypeRecord>,
168    pub duplicate_provider_declarations: Vec<DuplicateProviderDeclaration>,
169    pub computed_values: BTreeMap<SemanticId, ComputedValue>,
170    pub effects: BTreeMap<SemanticId, Effect>,
171    pub effect_reactive_analysis: BTreeMap<SemanticId, EffectReactiveAnalysis>,
172    pub effect_trigger_plan: EffectTriggerPlan,
173    pub effect_execution_plan: EffectExecutionPlan,
174    pub effect_bodies: BTreeMap<SemanticId, EffectBody>,
175    pub effect_statements: BTreeMap<SemanticId, EffectStatement>,
176    pub reactive_graph: IrReactiveGraph,
177    pub reactive_transitive_analysis: IrReactiveTransitiveAnalysis,
178    pub reactive_cycle_analysis: IrReactiveCycleAnalysis,
179    pub computed_evaluation_plan: IrComputedEvaluationPlan,
180    pub templates: Vec<TemplateNode>,
181    pub template_entities: Vec<TemplateSemanticEntity>,
182    pub diagnostics: Vec<ComponentDiagnostic>,
183    pub ownership: BTreeMap<SemanticId, SemanticOwner>,
184    pub references: Vec<SemanticReference>,
185    pub provenance: BTreeMap<SemanticId, SourceProvenance>,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum SemanticEntity<'a> {
190    Component(&'a ComponentNode),
191    StateField(&'a StateField),
192    Method(&'a ComponentMethod),
193    ActionEndpoint(&'a ActionEndpoint),
194    Context(&'a ContextEntity),
195    Provider(&'a ProviderEntity),
196    Consumer(&'a ConsumerEntity),
197    Form(&'a FormEntity),
198    FormField(&'a FormFieldEntity),
199    FormFieldBinding(&'a FormFieldBinding),
200    ValidationRule(&'a ValidationRule),
201    Slot(&'a SlotEntity),
202    ComponentInvocation(&'a ComponentInvocationEntity),
203    ComponentInstance(&'a ComponentInstance),
204    BlockedComponentInstance(&'a BlockedComponentInstancePlan),
205    SlotContentFragment(&'a SlotContentFragment),
206    SlotOutlet(&'a SlotOutlet),
207    Computed(&'a ComputedValue),
208    Effect(&'a Effect),
209    Parameter(&'a crate::MethodParameter),
210    LocalVariable(&'a MethodLocalVariable),
211    Action(&'a ComponentAction),
212    EventHandler(&'a RenderEventHandler),
213    Template(&'a TemplateNode),
214    TemplateEntity(&'a TemplateSemanticEntity),
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum SemanticEntityKind {
219    Component,
220    StateField,
221    Method,
222    ActionEndpoint,
223    Context,
224    Provider,
225    Consumer,
226    Form,
227    FormField,
228    FormFieldBinding,
229    ValidationRule,
230    Slot,
231    ComponentInvocation,
232    ComponentInstance,
233    BlockedComponentInstance,
234    SlotContentFragment,
235    SlotOutlet,
236    Computed,
237    Effect,
238    Parameter,
239    LocalVariable,
240    Action,
241    EventHandler,
242    Template,
243    TemplateEntity,
244}
245
246impl SemanticEntity<'_> {
247    #[must_use]
248    pub fn kind(self) -> SemanticEntityKind {
249        match self {
250            Self::Component(_) => SemanticEntityKind::Component,
251            Self::StateField(_) => SemanticEntityKind::StateField,
252            Self::Method(_) => SemanticEntityKind::Method,
253            Self::ActionEndpoint(_) => SemanticEntityKind::ActionEndpoint,
254            Self::Context(_) => SemanticEntityKind::Context,
255            Self::Provider(_) => SemanticEntityKind::Provider,
256            Self::Consumer(_) => SemanticEntityKind::Consumer,
257            Self::Form(_) => SemanticEntityKind::Form,
258            Self::FormField(_) => SemanticEntityKind::FormField,
259            Self::FormFieldBinding(_) => SemanticEntityKind::FormFieldBinding,
260            Self::ValidationRule(_) => SemanticEntityKind::ValidationRule,
261            Self::Slot(_) => SemanticEntityKind::Slot,
262            Self::ComponentInvocation(_) => SemanticEntityKind::ComponentInvocation,
263            Self::ComponentInstance(_) => SemanticEntityKind::ComponentInstance,
264            Self::BlockedComponentInstance(_) => SemanticEntityKind::BlockedComponentInstance,
265            Self::SlotContentFragment(_) => SemanticEntityKind::SlotContentFragment,
266            Self::SlotOutlet(_) => SemanticEntityKind::SlotOutlet,
267            Self::Computed(_) => SemanticEntityKind::Computed,
268            Self::Effect(_) => SemanticEntityKind::Effect,
269            Self::Parameter(_) => SemanticEntityKind::Parameter,
270            Self::LocalVariable(_) => SemanticEntityKind::LocalVariable,
271            Self::Action(_) => SemanticEntityKind::Action,
272            Self::EventHandler(_) => SemanticEntityKind::EventHandler,
273            Self::Template(_) => SemanticEntityKind::Template,
274            Self::TemplateEntity(_) => SemanticEntityKind::TemplateEntity,
275        }
276    }
277}
278
279impl ApplicationSemanticModel {
280    #[allow(clippy::too_many_lines)]
281    #[must_use]
282    pub fn entity(&self, id: &SemanticId) -> Option<SemanticEntity<'_>> {
283        for component in &self.components {
284            if component.id == *id {
285                return Some(SemanticEntity::Component(component));
286            }
287            if let Some(field) = component.state_fields.iter().find(|field| field.id == *id) {
288                return Some(SemanticEntity::StateField(field));
289            }
290            if let Some(method) = component.methods.iter().find(|method| method.id == *id) {
291                return Some(SemanticEntity::Method(method));
292            }
293            if let Some(endpoint) = component
294                .action_endpoints
295                .iter()
296                .find(|endpoint| endpoint.id == *id)
297            {
298                return Some(SemanticEntity::ActionEndpoint(endpoint));
299            }
300            if let Some(parameter) = component
301                .methods
302                .iter()
303                .flat_map(|method| method.parameters.iter())
304                .find(|parameter| parameter.id == *id)
305            {
306                return Some(SemanticEntity::Parameter(parameter));
307            }
308            if let Some(local) = component
309                .methods
310                .iter()
311                .flat_map(|method| method.local_variables.iter())
312                .find(|local| local.id == *id)
313            {
314                return Some(SemanticEntity::LocalVariable(local));
315            }
316            if let Some(action) = component.actions.iter().find(|action| action.id == *id) {
317                return Some(SemanticEntity::Action(action));
318            }
319            if let Some(render) = &component.render {
320                if let Some(handler) = render_event_handlers(render)
321                    .into_iter()
322                    .find(|handler| handler.id == *id)
323                {
324                    return Some(SemanticEntity::EventHandler(handler));
325                }
326            }
327        }
328
329        if let Some(computed) = self.computed_values.get(id) {
330            return Some(SemanticEntity::Computed(computed));
331        }
332        if let Some(context) = self
333            .contexts
334            .values()
335            .find(|context| context.id.as_semantic_id() == id)
336        {
337            return Some(SemanticEntity::Context(context));
338        }
339        if let Some(provider) = self
340            .providers
341            .values()
342            .find(|provider| provider.id.as_semantic_id() == id)
343        {
344            return Some(SemanticEntity::Provider(provider));
345        }
346        if let Some(consumer) = self
347            .consumers
348            .values()
349            .find(|consumer| consumer.id.as_semantic_id() == id)
350        {
351            return Some(SemanticEntity::Consumer(consumer));
352        }
353        if let Some(form) = self
354            .forms
355            .values()
356            .find(|form| form.id.as_semantic_id() == id)
357        {
358            return Some(SemanticEntity::Form(form));
359        }
360        if let Some(field) = self
361            .form_fields
362            .values()
363            .find(|field| field.id.as_semantic_id() == id)
364        {
365            return Some(SemanticEntity::FormField(field));
366        }
367        if let Some(binding) = self
368            .form_field_bindings
369            .values()
370            .find(|binding| binding.id.as_semantic_id() == id)
371        {
372            return Some(SemanticEntity::FormFieldBinding(binding));
373        }
374        if let Some(rule) = self
375            .validation_rules
376            .values()
377            .find(|rule| rule.id.as_semantic_id() == id)
378        {
379            return Some(SemanticEntity::ValidationRule(rule));
380        }
381        if let Some(slot) = self
382            .slots
383            .values()
384            .find(|slot| slot.id.as_semantic_id() == id)
385        {
386            return Some(SemanticEntity::Slot(slot));
387        }
388        if let Some(invocation) = self
389            .component_invocations
390            .values()
391            .find(|invocation| invocation.id.as_semantic_id() == id)
392        {
393            return Some(SemanticEntity::ComponentInvocation(invocation));
394        }
395        if let Some(instance) = self
396            .component_instance_plan
397            .instances
398            .values()
399            .find(|instance| instance.id.as_semantic_id() == id)
400        {
401            return Some(SemanticEntity::ComponentInstance(instance));
402        }
403        if let Some(blocked) = self
404            .component_instance_plan
405            .blocked
406            .values()
407            .find(|blocked| blocked.id.as_semantic_id() == id)
408        {
409            return Some(SemanticEntity::BlockedComponentInstance(blocked));
410        }
411        if let Some(fragment) = self
412            .slot_content_fragments
413            .values()
414            .find(|fragment| fragment.id.as_semantic_id() == id)
415        {
416            return Some(SemanticEntity::SlotContentFragment(fragment));
417        }
418        if let Some(outlet) = self
419            .slot_outlets
420            .values()
421            .find(|outlet| outlet.id.as_semantic_id() == id)
422        {
423            return Some(SemanticEntity::SlotOutlet(outlet));
424        }
425        if let Some(effect) = self.effects.get(id) {
426            return Some(SemanticEntity::Effect(effect));
427        }
428
429        if let Some(template) = self.templates.iter().find(|template| template.id == *id) {
430            return Some(SemanticEntity::Template(template));
431        }
432
433        self.template_entities
434            .iter()
435            .find(|entity| entity.id == *id)
436            .map(SemanticEntity::TemplateEntity)
437    }
438
439    #[must_use]
440    pub fn component(&self, id: &SemanticId) -> Option<&ComponentNode> {
441        self.components.iter().find(|component| component.id == *id)
442    }
443
444    #[must_use]
445    pub fn template(&self, id: &SemanticId) -> Option<&TemplateNode> {
446        self.templates.iter().find(|template| template.id == *id)
447    }
448
449    #[must_use]
450    pub fn computed_value(&self, id: &SemanticId) -> Option<&ComputedValue> {
451        self.computed_values.get(id)
452    }
453
454    #[must_use]
455    pub fn contexts(&self) -> Vec<&ContextEntity> {
456        self.contexts.values().collect()
457    }
458
459    #[must_use]
460    pub fn context(&self, id: &ContextId) -> Option<&ContextEntity> {
461        self.contexts.get(id)
462    }
463
464    #[must_use]
465    pub fn forms(&self) -> Vec<&FormEntity> {
466        self.forms.values().collect()
467    }
468
469    #[must_use]
470    pub fn form(&self, id: &FormId) -> Option<&FormEntity> {
471        self.forms.get(id)
472    }
473
474    #[must_use]
475    pub fn form_fields(&self) -> Vec<&FormFieldEntity> {
476        let mut fields = self.form_fields.values().collect::<Vec<_>>();
477        fields.sort_by(|left, right| {
478            (&left.owner_form, left.declaration_order, &left.id).cmp(&(
479                &right.owner_form,
480                right.declaration_order,
481                &right.id,
482            ))
483        });
484        fields
485    }
486
487    #[must_use]
488    pub fn form_field(&self, id: &FieldId) -> Option<&FormFieldEntity> {
489        self.form_fields.get(id)
490    }
491
492    #[must_use]
493    pub fn form_field_declaration_candidates(&self) -> &[crate::FormFieldDeclarationCandidate] {
494        &self.form_field_declaration_candidates
495    }
496
497    #[must_use]
498    pub fn form_field_binding_candidates(&self) -> &[FormFieldBindingCandidate] {
499        &self.form_field_binding_candidates
500    }
501
502    #[must_use]
503    pub fn form_field_bindings(&self) -> Vec<&FormFieldBinding> {
504        let mut bindings = self.form_field_bindings.values().collect::<Vec<_>>();
505        bindings.sort_by(|left, right| {
506            (&left.owner_template, left.authored_order, &left.id).cmp(&(
507                &right.owner_template,
508                right.authored_order,
509                &right.id,
510            ))
511        });
512        bindings
513    }
514
515    #[must_use]
516    pub fn form_field_binding(&self, id: &FieldBindingId) -> Option<&FormFieldBinding> {
517        self.form_field_bindings.get(id)
518    }
519
520    #[must_use]
521    pub const fn form_ownership(&self) -> &FormOwnershipGraph {
522        &self.form_ownership
523    }
524
525    #[must_use]
526    pub fn validation_rule_candidates(&self) -> &[ValidationRuleCandidate] {
527        &self.validation_rule_candidates
528    }
529
530    #[must_use]
531    pub fn validation_candidates_of(&self, field: &FieldId) -> Vec<&ValidationRuleCandidate> {
532        self.validation_rule_candidates
533            .iter()
534            .filter(|candidate| candidate.target_field.as_ref() == Some(field))
535            .collect()
536    }
537
538    #[must_use]
539    pub fn validation_rule(&self, id: &ValidationRuleId) -> Option<&ValidationRule> {
540        self.validation_rules.get(id)
541    }
542
543    #[must_use]
544    pub fn validation_rules(&self) -> Vec<&ValidationRule> {
545        let mut rules = self.validation_rules.values().collect::<Vec<_>>();
546        rules.sort_by(|left, right| {
547            (
548                &left.owner_form,
549                left.field_authored_order,
550                left.rule_authored_order,
551                &left.id,
552            )
553                .cmp(&(
554                    &right.owner_form,
555                    right.field_authored_order,
556                    right.rule_authored_order,
557                    &right.id,
558                ))
559        });
560        rules
561    }
562
563    #[must_use]
564    pub const fn validation_graph(&self) -> &ValidationGraph {
565        &self.validation_graph
566    }
567
568    #[must_use]
569    pub const fn validation_dependency_plans(&self) -> &ValidationDependencyPlans {
570        &self.validation_dependency_plans
571    }
572
573    #[must_use]
574    pub const fn form_tracking(&self) -> &FormTrackingProducts {
575        &self.form_tracking
576    }
577
578    #[must_use]
579    pub const fn submissions(&self) -> &SubmissionProducts {
580        &self.submissions
581    }
582
583    #[must_use]
584    pub const fn serialization(&self) -> &SerializationProducts {
585        &self.serialization
586    }
587
588    #[must_use]
589    pub const fn reset(&self) -> &ResetProducts {
590        &self.reset
591    }
592
593    #[must_use]
594    pub fn form_declaration_candidates(&self) -> Vec<&crate::FormDeclarationCandidate> {
595        let mut candidates = self
596            .components
597            .iter()
598            .flat_map(|component| component.form_declaration_candidates.iter())
599            .collect::<Vec<_>>();
600        candidates.sort_by(|left, right| {
601            (
602                left.provenance.path.as_path(),
603                left.provenance.span.start,
604                left.provenance.span.end,
605                left.id.as_str(),
606            )
607                .cmp(&(
608                    right.provenance.path.as_path(),
609                    right.provenance.span.start,
610                    right.provenance.span.end,
611                    right.id.as_str(),
612                ))
613        });
614        candidates
615    }
616
617    #[must_use]
618    pub const fn context_declaration_candidates(&self) -> &ContextDeclarationCandidateRegistry {
619        &self.context_declaration_candidates
620    }
621
622    #[must_use]
623    pub fn contexts_owned_by(&self, component: &SemanticId) -> Vec<&ContextId> {
624        self.contexts
625            .iter()
626            .filter_map(|(id, context)| {
627                (context.owner.entity_id() == Some(component)).then_some(id)
628            })
629            .collect()
630    }
631
632    #[must_use]
633    pub fn context_for_authored_field(&self, field: &SemanticId) -> Option<&ContextId> {
634        self.contexts
635            .iter()
636            .find_map(|(id, context)| (&context.authored_field == field).then_some(id))
637    }
638
639    #[must_use]
640    pub fn providers(&self) -> Vec<&ProviderEntity> {
641        self.providers.values().collect()
642    }
643
644    #[must_use]
645    pub fn provider(&self, id: &ProviderId) -> Option<&ProviderEntity> {
646        self.providers.get(id)
647    }
648
649    #[must_use]
650    pub fn providers_owned_by(&self, component: &SemanticId) -> Vec<&ProviderId> {
651        self.providers
652            .iter()
653            .filter_map(|(id, provider)| {
654                (provider.owner.entity_id() == Some(component)).then_some(id)
655            })
656            .collect()
657    }
658
659    #[must_use]
660    pub fn providers_for_context(&self, context: &ContextId) -> Vec<&ProviderId> {
661        self.providers
662            .iter()
663            .filter_map(|(id, provider)| (&provider.context == context).then_some(id))
664            .collect()
665    }
666
667    #[must_use]
668    pub fn provider_for_authored_field(&self, field: &SemanticId) -> Option<&ProviderId> {
669        self.providers
670            .iter()
671            .find_map(|(id, provider)| (&provider.authored_field == field).then_some(id))
672    }
673
674    #[must_use]
675    pub fn consumers(&self) -> Vec<&ConsumerEntity> {
676        self.consumers.values().collect()
677    }
678
679    #[must_use]
680    pub fn consumer(&self, id: &ConsumerId) -> Option<&ConsumerEntity> {
681        self.consumers.get(id)
682    }
683
684    pub(crate) fn consumer_for_semantic_id(&self, id: &SemanticId) -> Option<&ConsumerEntity> {
685        self.consumers
686            .values()
687            .find(|consumer| consumer.id.as_semantic_id() == id)
688    }
689
690    #[must_use]
691    pub fn consumers_owned_by(&self, component: &SemanticId) -> Vec<&ConsumerId> {
692        self.consumers
693            .iter()
694            .filter_map(|(id, consumer)| {
695                (consumer.owner.entity_id() == Some(component)).then_some(id)
696            })
697            .collect()
698    }
699
700    #[must_use]
701    pub fn consumers_for_context(&self, context: &ContextId) -> Vec<&ConsumerId> {
702        self.consumers
703            .iter()
704            .filter_map(|(id, consumer)| (consumer.context() == Some(context)).then_some(id))
705            .collect()
706    }
707
708    #[must_use]
709    pub fn consumer_for_authored_field(&self, field: &SemanticId) -> Option<&ConsumerId> {
710        self.consumers
711            .iter()
712            .find_map(|(id, consumer)| (&consumer.authored_field == field).then_some(id))
713    }
714
715    #[must_use]
716    pub fn slots(&self) -> Vec<&SlotEntity> {
717        self.slots.values().collect()
718    }
719
720    #[must_use]
721    pub fn slot(&self, id: &SlotId) -> Option<&SlotEntity> {
722        self.slots.get(id)
723    }
724
725    #[must_use]
726    pub fn slots_owned_by(&self, component: &SemanticId) -> Vec<&SlotId> {
727        self.slots
728            .iter()
729            .filter_map(|(id, slot)| (&slot.owner == component).then_some(id))
730            .collect()
731    }
732
733    #[must_use]
734    pub fn slot_for_authored_field(&self, field: &SemanticId) -> Option<&SlotId> {
735        self.slots
736            .iter()
737            .find_map(|(id, slot)| (&slot.authored_field == field).then_some(id))
738    }
739
740    #[must_use]
741    pub fn component_invocations(&self) -> Vec<&ComponentInvocationEntity> {
742        self.component_invocations.values().collect()
743    }
744
745    #[must_use]
746    pub fn component_invocation(
747        &self,
748        id: &ComponentInvocationId,
749    ) -> Option<&ComponentInvocationEntity> {
750        self.component_invocations.get(id)
751    }
752
753    #[must_use]
754    pub fn component_invocations_owned_by(
755        &self,
756        component: &SemanticId,
757    ) -> Vec<&ComponentInvocationId> {
758        self.component_invocations
759            .iter()
760            .filter_map(|(id, invocation)| (&invocation.owner_component == component).then_some(id))
761            .collect()
762    }
763
764    #[must_use]
765    pub fn component_instance(
766        &self,
767        id: &crate::ComponentInstanceId,
768    ) -> Option<&ComponentInstance> {
769        self.component_instance_plan.instances.get(id)
770    }
771
772    #[must_use]
773    pub fn component_build_roots(&self) -> Vec<&crate::ComponentBuildRoot> {
774        self.component_instance_plan.roots.values().collect()
775    }
776
777    #[must_use]
778    pub fn component_instances(&self) -> Vec<&ComponentInstance> {
779        self.component_instance_plan.instances.values().collect()
780    }
781
782    #[must_use]
783    pub fn blocked_component_instances(&self) -> Vec<&BlockedComponentInstancePlan> {
784        self.component_instance_plan.blocked.values().collect()
785    }
786
787    #[must_use]
788    pub fn blocked_component_instance(
789        &self,
790        id: &crate::ComponentInstanceId,
791    ) -> Option<&BlockedComponentInstancePlan> {
792        self.component_instance_plan.blocked.get(id)
793    }
794
795    #[must_use]
796    pub fn component_instances_for_definition(
797        &self,
798        component: &SemanticId,
799    ) -> Vec<&ComponentInstance> {
800        self.component_instance_plan
801            .instances
802            .values()
803            .filter(|instance| &instance.component == component)
804            .collect()
805    }
806
807    #[must_use]
808    pub const fn component_instance_scope_graph(&self) -> &ComponentInstanceScopeGraph {
809        &self.component_instance_scope
810    }
811
812    #[must_use]
813    pub const fn component_composition_analysis(&self) -> &ComponentCompositionAnalysis {
814        &self.component_composition
815    }
816
817    #[must_use]
818    pub const fn component_initialization_plan(&self) -> &ComponentInitializationPlan {
819        &self.component_initialization
820    }
821
822    #[must_use]
823    pub const fn component_ir_report(&self) -> &ComponentIrReport {
824        &self.component_ir
825    }
826
827    #[must_use]
828    pub const fn optimized_component_ir_report(&self) -> &OptimizedComponentIrReport {
829        &self.component_ir_optimization
830    }
831
832    #[must_use]
833    pub const fn instance_context_registry(&self) -> &InstanceContextRegistry {
834        &self.instance_context
835    }
836
837    #[must_use]
838    pub fn slot_content_fragments(&self) -> Vec<&SlotContentFragment> {
839        self.slot_content_fragments.values().collect()
840    }
841
842    #[must_use]
843    pub const fn slot_binding_registry(&self) -> &SlotBindingRegistry {
844        &self.slot_bindings
845    }
846
847    #[must_use]
848    pub const fn composition_type_products(&self) -> &CompositionTypeProducts {
849        &self.composition_types
850    }
851
852    #[must_use]
853    pub fn slot_bindings(&self) -> Vec<&SlotBinding> {
854        self.slot_bindings.bindings.values().collect()
855    }
856
857    #[must_use]
858    pub fn slot_binding(&self, id: &SlotBindingId) -> Option<&SlotBinding> {
859        self.slot_bindings.binding(id)
860    }
861
862    #[must_use]
863    pub fn slot_bindings_for_callee(
864        &self,
865        callee: &crate::ComponentInstanceId,
866    ) -> Vec<&SlotBinding> {
867        self.slot_bindings.for_callee(callee)
868    }
869
870    #[must_use]
871    pub fn slot_content_fragment(
872        &self,
873        id: &SlotContentFragmentId,
874    ) -> Option<&SlotContentFragment> {
875        self.slot_content_fragments.get(id)
876    }
877
878    #[must_use]
879    pub fn slot_content_fragments_for(
880        &self,
881        invocation: &ComponentInvocationId,
882    ) -> Vec<&SlotContentFragment> {
883        self.slot_content_fragments
884            .values()
885            .filter(|fragment| &fragment.invocation == invocation)
886            .collect()
887    }
888
889    #[must_use]
890    pub fn slot_outlets(&self) -> Vec<&SlotOutlet> {
891        self.slot_outlets.values().collect()
892    }
893
894    #[must_use]
895    pub fn slot_outlet(&self, id: &SlotOutletId) -> Option<&SlotOutlet> {
896        self.slot_outlets.get(id)
897    }
898
899    #[must_use]
900    pub fn slot_outlets_owned_by(&self, component: &SemanticId) -> Vec<&SlotOutletId> {
901        self.slot_outlets
902            .iter()
903            .filter_map(|(id, outlet)| (&outlet.owner_component == component).then_some(id))
904            .collect()
905    }
906
907    #[must_use]
908    pub fn context_resolution(&self, consumer: &ConsumerId) -> Option<&ContextResolution> {
909        self.context_resolutions.get(consumer)
910    }
911
912    #[must_use]
913    pub const fn context_ownership_graph(&self) -> &ContextOwnershipGraph {
914        &self.context_ownership
915    }
916
917    #[must_use]
918    pub const fn context_dependency_graph(&self) -> &ContextDependencyGraph {
919        &self.context_dependency
920    }
921
922    #[must_use]
923    pub const fn context_lifetime_analysis(&self) -> &ContextLifetimeAnalysis {
924        &self.context_lifetime
925    }
926
927    #[must_use]
928    pub const fn context_evaluation_plan(&self) -> &ContextEvaluationPlan {
929        &self.context_evaluation
930    }
931
932    #[must_use]
933    pub fn resolved_provider(&self, consumer: &ConsumerId) -> Option<&ProviderId> {
934        let ContextResolutionResult::Provider { provider, .. } =
935            &self.context_resolution(consumer)?.result
936        else {
937            return None;
938        };
939        Some(provider)
940    }
941
942    #[must_use]
943    pub fn consumers_resolved_to(&self, provider: &ProviderId) -> Vec<&ConsumerId> {
944        self.context_resolutions
945            .iter()
946            .filter_map(|(consumer, resolution)| {
947                matches!(
948                    &resolution.result,
949                    ContextResolutionResult::Provider {
950                        provider: resolved, ..
951                    } if resolved == provider
952                )
953                .then_some(consumer)
954            })
955            .collect()
956    }
957
958    #[must_use]
959    pub fn consumers_using_default(&self, context: &ContextId) -> Vec<&ConsumerId> {
960        self.context_resolutions
961            .iter()
962            .filter_map(|(consumer, resolution)| {
963                matches!(
964                    &resolution.result,
965                    ContextResolutionResult::ContextDefault {
966                        context: resolved, ..
967                    } if resolved == context
968                )
969                .then_some(consumer)
970            })
971            .collect()
972    }
973
974    #[must_use]
975    pub fn unresolved_context_consumers(&self) -> Vec<&ConsumerId> {
976        self.context_resolutions
977            .iter()
978            .filter_map(|(consumer, resolution)| {
979                matches!(resolution.result, ContextResolutionResult::Unresolved).then_some(consumer)
980            })
981            .collect()
982    }
983
984    #[must_use]
985    pub fn context_type(&self, context: &ContextId) -> Option<&ContextTypeRecord> {
986        self.context_types.get(context)
987    }
988
989    #[must_use]
990    pub fn provider_type(&self, provider: &ProviderId) -> Option<&ProviderTypeRecord> {
991        self.provider_types.get(provider)
992    }
993
994    #[must_use]
995    pub fn consumer_type(&self, consumer: &ConsumerId) -> Option<&ConsumerTypeRecord> {
996        self.consumer_types.get(consumer)
997    }
998
999    #[must_use]
1000    pub fn context_binding_type(&self, consumer: &ConsumerId) -> Option<&ContextBindingTypeRecord> {
1001        self.context_binding_types.get(consumer)
1002    }
1003
1004    #[must_use]
1005    pub fn runtime_eligible_context_binding(&self, consumer: &ConsumerId) -> bool {
1006        self.context_binding_type(consumer)
1007            .is_some_and(|binding| binding.overall == ContextBindingCompatibility::Compatible)
1008    }
1009
1010    #[must_use]
1011    pub fn effect(&self, id: &SemanticId) -> Option<&Effect> {
1012        self.effects.get(id)
1013    }
1014
1015    #[must_use]
1016    pub fn effect_body(&self, effect: &SemanticId) -> Option<&EffectBody> {
1017        self.effect_bodies.get(effect)
1018    }
1019
1020    #[must_use]
1021    pub fn effect_reactive_analysis(&self, effect: &SemanticId) -> Option<&EffectReactiveAnalysis> {
1022        self.effect_reactive_analysis.get(effect)
1023    }
1024
1025    #[must_use]
1026    pub const fn effect_trigger_plan(&self) -> &EffectTriggerPlan {
1027        &self.effect_trigger_plan
1028    }
1029
1030    #[must_use]
1031    pub const fn effect_execution_plan(&self) -> &EffectExecutionPlan {
1032        &self.effect_execution_plan
1033    }
1034
1035    /// Returns F4 typing facts for one canonical effect statement.
1036    #[must_use]
1037    pub fn effect_statement_type(
1038        &self,
1039        statement: &SemanticId,
1040    ) -> Option<&EffectStatementTypeRecord> {
1041        self.semantic_types.effect_statements.get(statement)
1042    }
1043
1044    #[must_use]
1045    pub fn effect_statement(&self, id: &SemanticId) -> Option<&EffectStatement> {
1046        self.effect_statements.get(id)
1047    }
1048
1049    #[must_use]
1050    pub fn template_entity(&self, id: &SemanticId) -> Option<&TemplateSemanticEntity> {
1051        self.template_entities
1052            .iter()
1053            .find(|entity| entity.id == *id)
1054    }
1055
1056    #[must_use]
1057    pub fn template_entities_for(&self, template: &SemanticId) -> Vec<&TemplateSemanticEntity> {
1058        self.children_of(template)
1059            .into_iter()
1060            .filter_map(|id| self.template_entity(id))
1061            .collect()
1062    }
1063
1064    #[must_use]
1065    pub fn owner(&self, id: &SemanticId) -> Option<&SemanticOwner> {
1066        self.ownership.get(id)
1067    }
1068
1069    #[must_use]
1070    pub fn parent_of(&self, id: &SemanticId) -> Option<&SemanticId> {
1071        self.owner(id).and_then(SemanticOwner::entity_id)
1072    }
1073
1074    #[must_use]
1075    pub fn ancestors_of(&self, id: &SemanticId) -> Vec<&SemanticId> {
1076        let mut ancestors = Vec::new();
1077        let mut seen = BTreeSet::from([id.clone()]);
1078        let mut current = id;
1079
1080        while let Some(parent) = self.parent_of(current) {
1081            if !seen.insert(parent.clone()) {
1082                break;
1083            }
1084            ancestors.push(parent);
1085            current = parent;
1086        }
1087
1088        ancestors
1089    }
1090
1091    #[must_use]
1092    pub fn provenance(&self, id: &SemanticId) -> Option<&SourceProvenance> {
1093        self.provenance.get(id)
1094    }
1095
1096    #[must_use]
1097    pub fn expression(&self, id: &SemanticId) -> Option<&ExpressionNode> {
1098        self.expression_graph.node(id)
1099    }
1100
1101    #[must_use]
1102    pub fn expression_root(&self, owner: &SemanticId) -> Option<&SemanticId> {
1103        self.expression_graph.root_for(owner)
1104    }
1105
1106    #[must_use]
1107    pub fn expressions_for(&self, owner: &SemanticId) -> Vec<&ExpressionNode> {
1108        self.expression_graph.nodes_for(owner)
1109    }
1110
1111    #[must_use]
1112    pub fn expression_dependencies(&self, id: &SemanticId) -> Vec<&SemanticId> {
1113        self.expression_graph.dependencies_of(id)
1114    }
1115
1116    #[must_use]
1117    pub fn expression_dependents(&self, id: &SemanticId) -> Vec<&ExpressionNode> {
1118        self.expression_graph.dependents_of(id)
1119    }
1120
1121    #[must_use]
1122    pub fn expression_owner(&self, id: &SemanticId) -> Option<&SemanticId> {
1123        self.expression_graph.owner_of(id)
1124    }
1125
1126    #[must_use]
1127    pub fn expression_provenance(&self, id: &SemanticId) -> Option<&SourceProvenance> {
1128        self.expression_graph.provenance_of(id)
1129    }
1130
1131    #[must_use]
1132    pub fn semantic_type_of(&self, id: &SemanticId) -> Option<&crate::SemanticType> {
1133        self.semantic_types
1134            .assignments
1135            .get(id)
1136            .map(|assignment| &assignment.semantic_type)
1137    }
1138
1139    #[must_use]
1140    pub fn expression_type(&self, id: &SemanticId) -> Option<&crate::SemanticType> {
1141        self.expression_graph
1142            .node(id)
1143            .and_then(|_| self.semantic_type_of(id))
1144    }
1145
1146    #[must_use]
1147    pub fn type_declarations(
1148        &self,
1149        semantic_type: &crate::SemanticType,
1150    ) -> Vec<&crate::SemanticTypeAssignment> {
1151        self.semantic_types
1152            .assignments
1153            .values()
1154            .filter(|assignment| {
1155                assignment.status == crate::SemanticTypeStatus::Declared
1156                    && assignment.semantic_type == *semantic_type
1157            })
1158            .collect()
1159    }
1160
1161    #[must_use]
1162    pub fn type_usages(
1163        &self,
1164        semantic_type: &crate::SemanticType,
1165    ) -> Vec<&crate::SemanticTypeAssignment> {
1166        self.semantic_types
1167            .assignments
1168            .values()
1169            .filter(|assignment| assignment.semantic_type == *semantic_type)
1170            .collect()
1171    }
1172
1173    #[must_use]
1174    pub fn serialization_compatibility_of(
1175        &self,
1176        id: &SemanticId,
1177    ) -> Option<crate::SerializationCompatibility> {
1178        self.semantic_type_of(id)
1179            .map(crate::serialization_compatibility)
1180    }
1181
1182    #[must_use]
1183    pub fn is_type_assignable(&self, source: &SemanticId, target: &SemanticId) -> Option<bool> {
1184        Some(crate::is_assignable(
1185            self.semantic_type_of(source)?,
1186            self.semantic_type_of(target)?,
1187        ))
1188    }
1189
1190    #[must_use]
1191    pub fn expressions_in_file(&self, path: &Path) -> Vec<&ExpressionNode> {
1192        self.expression_graph.nodes_in_file(path)
1193    }
1194
1195    #[must_use]
1196    pub fn expressions_at(&self, path: &Path, offset: usize) -> Vec<&ExpressionNode> {
1197        self.expression_graph.nodes_at(path, offset)
1198    }
1199
1200    #[must_use]
1201    pub fn application_roots(&self) -> Vec<&SemanticId> {
1202        self.ownership
1203            .iter()
1204            .filter_map(|(id, owner)| matches!(owner, SemanticOwner::Application).then_some(id))
1205            .collect()
1206    }
1207
1208    #[must_use]
1209    pub fn children_of(&self, owner: &SemanticId) -> Vec<&SemanticId> {
1210        self.ownership
1211            .iter()
1212            .filter_map(|(id, entity_owner)| {
1213                (entity_owner.entity_id() == Some(owner)).then_some(id)
1214            })
1215            .collect()
1216    }
1217
1218    #[must_use]
1219    pub fn descendants_of(&self, owner: &SemanticId) -> Vec<&SemanticId> {
1220        let mut descendants = Vec::new();
1221        self.collect_descendants(owner, &mut descendants);
1222        descendants
1223    }
1224
1225    #[must_use]
1226    pub fn entities_of_kind(&self, kind: SemanticEntityKind) -> Vec<&SemanticId> {
1227        self.ownership
1228            .keys()
1229            .filter(|id| self.entity(id).is_some_and(|entity| entity.kind() == kind))
1230            .collect()
1231    }
1232
1233    #[must_use]
1234    pub fn entities_in_file(&self, path: &Path) -> Vec<&SemanticId> {
1235        self.provenance
1236            .iter()
1237            .filter_map(|(id, provenance)| (provenance.path == path).then_some(id))
1238            .collect()
1239    }
1240
1241    #[must_use]
1242    pub fn entities_at(&self, path: &Path, offset: usize) -> Vec<&SemanticId> {
1243        self.provenance
1244            .iter()
1245            .filter_map(|(id, provenance)| {
1246                (provenance.path == path
1247                    && provenance.span.start <= offset
1248                    && offset < provenance.span.end)
1249                    .then_some(id)
1250            })
1251            .collect()
1252    }
1253
1254    #[must_use]
1255    pub fn references_of_kind(&self, kind: SemanticReferenceKind) -> Vec<&SemanticReference> {
1256        let mut references = self
1257            .references
1258            .iter()
1259            .filter(|reference| reference.kind == kind)
1260            .collect::<Vec<_>>();
1261        references.sort_by(|left, right| {
1262            (left.source.as_str(), left.target.as_str())
1263                .cmp(&(right.source.as_str(), right.target.as_str()))
1264        });
1265        references
1266    }
1267
1268    #[must_use]
1269    pub fn references_in_file(&self, path: &Path) -> Vec<&SemanticReference> {
1270        let mut references = self
1271            .references
1272            .iter()
1273            .filter(|reference| reference.provenance.path == path)
1274            .collect::<Vec<_>>();
1275        references.sort_by(|left, right| {
1276            (left.source.as_str(), left.target.as_str())
1277                .cmp(&(right.source.as_str(), right.target.as_str()))
1278        });
1279        references
1280    }
1281
1282    #[must_use]
1283    pub fn references_at(&self, path: &Path, offset: usize) -> Vec<&SemanticReference> {
1284        let mut references = self
1285            .references
1286            .iter()
1287            .filter(|reference| {
1288                reference.provenance.path == path
1289                    && reference.provenance.span.start <= offset
1290                    && offset < reference.provenance.span.end
1291            })
1292            .collect::<Vec<_>>();
1293        references.sort_by(|left, right| {
1294            (left.source.as_str(), left.target.as_str())
1295                .cmp(&(right.source.as_str(), right.target.as_str()))
1296        });
1297        references
1298    }
1299
1300    fn collect_descendants<'a>(
1301        &'a self,
1302        owner: &SemanticId,
1303        descendants: &mut Vec<&'a SemanticId>,
1304    ) {
1305        for child in self.children_of(owner) {
1306            descendants.push(child);
1307            self.collect_descendants(child, descendants);
1308        }
1309    }
1310
1311    #[must_use]
1312    pub fn references_from(&self, id: &SemanticId) -> Vec<&SemanticReference> {
1313        self.references
1314            .iter()
1315            .filter(move |reference| reference.source == *id)
1316            .collect()
1317    }
1318
1319    #[must_use]
1320    pub fn references_to(&self, id: &SemanticId) -> Vec<&SemanticReference> {
1321        self.references
1322            .iter()
1323            .filter(move |reference| reference.target == *id)
1324            .collect()
1325    }
1326}
1327
1328#[must_use]
1329pub fn build_application_semantic_model(parsed: &ParsedFile) -> ApplicationSemanticModel {
1330    build_application_semantic_model_from_files(std::slice::from_ref(parsed))
1331}
1332
1333/// Assemble canonical ASM from an existing component graph while preserving its identity mode.
1334#[allow(clippy::too_many_lines)]
1335#[must_use]
1336pub fn build_application_semantic_model_from_component_graph(
1337    component_graph: &crate::component_graph::ComponentGraph,
1338) -> ApplicationSemanticModel {
1339    let templates = build_template_graph(component_graph).templates;
1340    let template_entities = build_template_semantic_entities(&templates);
1341    let component_invocations = collect_component_invocations(
1342        &component_graph.components,
1343        &templates,
1344        &template_entities,
1345        &[],
1346        None,
1347        &component_graph.provenance,
1348    );
1349    let component_instance_plan = plan_component_instances(
1350        &component_graph.components,
1351        &component_invocations,
1352        &template_entities,
1353        &component_graph.provenance,
1354    );
1355    let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
1356    let component_composition = analyze_component_composition(
1357        &component_graph
1358            .components
1359            .iter()
1360            .map(|component| component.id.clone())
1361            .collect(),
1362        &component_invocations,
1363        &component_instance_plan,
1364    );
1365    let (computed_values, computed_diagnostics) = classify_computed_values(
1366        &component_graph.components,
1367        collect_computed_values(&component_graph.components, &component_graph.provenance),
1368        &component_graph.provenance,
1369    );
1370    let effects = collect_effects(&component_graph.components, &component_graph.provenance);
1371    let expression_graph =
1372        ExpressionGraph::from_components(&component_graph.components, &component_graph.provenance);
1373    let contexts = collect_context_entities(&component_graph.components, &expression_graph);
1374    let forms = collect_form_entities(&component_graph.components);
1375    let resource_endpoint_resolutions =
1376        collect_resource_endpoint_resolutions(&component_graph.components, None);
1377    let opaque_action_resolutions =
1378        collect_opaque_action_resolutions(&component_graph.components, None);
1379    let base_semantic_types = SemanticTypeModel::from_components(
1380        &component_graph.components,
1381        &component_graph.provenance,
1382    );
1383    let resource_declarations = collect_resource_declarations(
1384        &component_graph.components,
1385        &resource_endpoint_resolutions,
1386        &base_semantic_types,
1387        None,
1388    );
1389    let resource_activations =
1390        collect_resource_activations(&resource_declarations, &component_instance_plan);
1391    let form_field_products = collect_form_field_products(
1392        &component_graph.components,
1393        &forms,
1394        &base_semantic_types,
1395        None,
1396    );
1397    let form_field_declaration_candidates = form_field_products.candidates;
1398    let form_fields = form_field_products.fields;
1399    let form_field_binding_products = collect_form_field_binding_products(
1400        &component_graph.components,
1401        &templates,
1402        &forms,
1403        &form_fields,
1404        &form_field_declaration_candidates,
1405    );
1406    let form_field_binding_candidates = form_field_binding_products.candidates;
1407    let form_field_bindings = form_field_binding_products.bindings;
1408    let slots = collect_slot_entities(&component_graph.components);
1409    let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
1410    let slot_bindings = collect_slot_bindings(
1411        &component_instance_plan,
1412        &component_invocations,
1413        &slots,
1414        &slot_composition.fragments,
1415        &slot_composition.outlets,
1416    );
1417    let (providers, duplicate_provider_declarations) = collect_provider_entities(
1418        &component_graph.components,
1419        &contexts,
1420        &expression_graph,
1421        None,
1422    );
1423    let consumers = collect_consumer_entities(&component_graph.components, &contexts, None);
1424    let instance_context = collect_instance_context_registry(
1425        &component_instance_scope,
1426        &contexts,
1427        &providers,
1428        &consumers,
1429    );
1430    let context_declaration_candidates = collect_context_declaration_candidates(
1431        &component_graph.components,
1432        &contexts,
1433        &providers,
1434        &consumers,
1435    );
1436    let component_scope = ComponentScopeGraph::reflexive(&component_graph.components);
1437    let context_resolutions = collect_context_resolutions(
1438        &consumers,
1439        &contexts,
1440        &providers,
1441        &expression_graph,
1442        &component_scope,
1443    );
1444    let mut provenance = component_graph.provenance.clone();
1445    extend_template_entity_provenance(&mut provenance, &template_entities);
1446    extend_derived_entity_provenance(
1447        &mut provenance,
1448        &contexts,
1449        &forms,
1450        &form_fields,
1451        &form_field_bindings,
1452        &providers,
1453        &consumers,
1454        &slots,
1455        &component_invocations,
1456        &slot_composition.fragments,
1457        &slot_composition.outlets,
1458        &component_instance_plan,
1459        &computed_values,
1460        &effects,
1461    );
1462    let mut ownership = collect_ownership(
1463        &component_graph.components,
1464        &contexts,
1465        &forms,
1466        &form_fields,
1467        &form_field_bindings,
1468        &providers,
1469        &consumers,
1470        &slots,
1471        &component_invocations,
1472        &slot_composition.fragments,
1473        &slot_composition.outlets,
1474        &component_instance_plan,
1475        &computed_values,
1476        &effects,
1477        &templates,
1478        &template_entities,
1479    );
1480    let context_ownership = collect_context_ownership_graph(
1481        &component_graph.components,
1482        &contexts,
1483        &providers,
1484        &consumers,
1485        &expression_graph,
1486        &provenance,
1487    );
1488    let (effect_bodies, effect_statements) =
1489        lower_effect_bodies(&component_graph.components, &effects, &expression_graph);
1490    let mut references = component_graph.references.clone();
1491    references.extend(build_computed_references(
1492        &component_graph.components,
1493        &computed_values,
1494        &resource_declarations,
1495        &expression_graph,
1496    ));
1497    references.extend(build_effect_references(
1498        &component_graph.components,
1499        &effects,
1500        &computed_values,
1501        &expression_graph,
1502    ));
1503    references.extend(build_provider_references(&providers));
1504    references.extend(build_consumer_references(&consumers));
1505    references.extend(build_context_resolution_references(&context_resolutions));
1506    extend_template_references(
1507        &mut references,
1508        &component_graph.components,
1509        &computed_values,
1510        &template_entities,
1511        &ownership,
1512    );
1513    references.extend(build_form_field_binding_references(&form_field_bindings));
1514    let form_ownership = collect_form_ownership_graph(
1515        &component_instance_plan.roots,
1516        &component_graph.components,
1517        &forms,
1518        &form_fields,
1519        &form_field_bindings,
1520        &template_entities,
1521        &ownership,
1522        &references,
1523        &provenance,
1524    );
1525    let validation_products =
1526        collect_validation_products(&component_graph.components, &forms, &form_fields);
1527    let validation_rule_candidates = validation_products.candidates;
1528    let validation_rules = validation_products.rules;
1529    extend_validation_products(
1530        &mut provenance,
1531        &mut ownership,
1532        &mut references,
1533        &validation_rules,
1534    );
1535    let validation_graph = collect_validation_graph(
1536        &component_instance_plan.roots,
1537        &form_ownership,
1538        &forms,
1539        &form_fields,
1540        &validation_rules,
1541        &validation_rule_candidates,
1542        &validation_products.cycles,
1543        &ownership,
1544        &references,
1545    );
1546    let validation_dependency_plans = collect_validation_dependency_plans(
1547        &forms,
1548        &form_fields,
1549        &validation_rules,
1550        &form_ownership,
1551        &validation_graph,
1552    );
1553    let form_tracking =
1554        collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
1555    let semantic_types = finalize_semantic_types(
1556        base_semantic_types.with_resource_types(&resource_declarations),
1557        &component_graph.components,
1558        &contexts,
1559        &forms,
1560        &form_fields,
1561        &providers,
1562        &consumers,
1563        &slots,
1564        &computed_values,
1565        &effects,
1566        &effect_statements,
1567        &expression_graph,
1568        &references,
1569        &template_entities,
1570    );
1571    let context_type_products = collect_context_type_products(
1572        &contexts,
1573        &providers,
1574        &consumers,
1575        &context_resolutions,
1576        &expression_graph,
1577        &semantic_types,
1578    );
1579    let context_dependency = collect_context_dependency_graph(
1580        &component_graph.components,
1581        &contexts,
1582        &providers,
1583        &consumers,
1584        &context_resolutions,
1585        &context_type_products.contexts,
1586        &context_type_products.providers,
1587        &context_type_products.bindings,
1588        &computed_values,
1589        &expression_graph,
1590    );
1591    let context_lifetime = collect_context_lifetime_analysis(
1592        &component_graph.components,
1593        &contexts,
1594        &providers,
1595        &consumers,
1596        &computed_values,
1597        &context_ownership,
1598        &component_scope,
1599        &context_resolutions,
1600        &context_dependency,
1601        &provenance,
1602    );
1603    let effects = validate_effects(
1604        &component_graph.components,
1605        effects,
1606        &effect_statements,
1607        &semantic_types,
1608    );
1609    let (
1610        reactive_graph,
1611        reactive_transitive_analysis,
1612        reactive_cycle_analysis,
1613        computed_evaluation_plan,
1614    ) = build_computed_reactive_products(
1615        &component_graph.components,
1616        &computed_values,
1617        &effects,
1618        &resource_declarations,
1619        &references,
1620        &provenance,
1621    );
1622    let context_evaluation = collect_context_evaluation_plan(
1623        &contexts,
1624        &providers,
1625        &context_resolutions,
1626        &context_type_products.contexts,
1627        &context_type_products.providers,
1628        &context_type_products.bindings,
1629        &context_lifetime,
1630        &context_dependency,
1631        &computed_evaluation_plan,
1632        &component_scope,
1633    );
1634    let effect_reactive_analysis = analyze_effect_reactivity(
1635        &component_graph.components,
1636        &computed_values,
1637        &effects,
1638        &reactive_transitive_analysis,
1639    );
1640    let effect_trigger_plan = derive_effect_trigger_plan(
1641        &component_graph.components,
1642        &effects,
1643        &effect_reactive_analysis,
1644        &provenance,
1645    );
1646    let submissions = collect_submission_products(
1647        &component_graph.components,
1648        &forms,
1649        &form_fields,
1650        &validation_rules,
1651        &effect_trigger_plan,
1652    );
1653    let serialization = collect_serialization_products(
1654        &component_graph.components,
1655        &forms,
1656        &form_fields,
1657        &submissions.plans,
1658    );
1659    let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
1660    let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
1661    let optimized_form_ir = optimize_form_ir(&form_ir);
1662    let submission_host_products = collect_submission_host_products(
1663        &templates,
1664        &component_graph.components,
1665        &forms,
1666        &form_field_bindings,
1667        &submissions.plans,
1668        &serialization.plans,
1669        &optimized_form_ir.optimized,
1670    );
1671    let runtime_forms = build_runtime_form_registry(
1672        &forms,
1673        &form_fields,
1674        &form_field_bindings,
1675        &validation_rules,
1676        &optimized_form_ir.optimized,
1677        &submissions,
1678        &serialization,
1679        &reset,
1680        &submission_host_products.hosts,
1681    );
1682    let effect_execution_plan = plan_effect_execution(
1683        &computed_values,
1684        &effects,
1685        &effect_reactive_analysis,
1686        &effect_trigger_plan,
1687        &reactive_transitive_analysis,
1688        &computed_evaluation_plan,
1689    );
1690    let mut diagnostics = component_graph.diagnostics.clone();
1691    diagnostics.extend(computed_diagnostics);
1692    extend_computed_diagnostics(
1693        &mut diagnostics,
1694        &component_graph.components,
1695        &computed_values,
1696        &resource_declarations,
1697        &expression_graph,
1698        &semantic_types,
1699        &reactive_cycle_analysis,
1700        &provenance,
1701    );
1702    let component_ids = component_graph
1703        .components
1704        .iter()
1705        .map(|component| component.id.clone())
1706        .collect::<BTreeSet<_>>();
1707    let composition_types = collect_composition_type_products(
1708        &component_ids,
1709        &component_invocations,
1710        &slot_bindings,
1711        &slots,
1712        &slot_composition.fragments,
1713        &slot_composition.outlets,
1714        &instance_context,
1715        &context_type_products.bindings,
1716        &context_type_products.contexts,
1717        &context_type_products.providers,
1718        &context_lifetime,
1719        &ownership,
1720        &references,
1721    );
1722    let component_initialization = plan_component_initialization(
1723        &component_instance_plan,
1724        &slot_bindings,
1725        &composition_types,
1726        &instance_context,
1727    );
1728    let mut model = ApplicationSemanticModel {
1729        expression_graph,
1730        semantic_types,
1731        components: components_with_resolved_form_field_candidates(
1732            &component_graph.components,
1733            &form_field_declaration_candidates,
1734        ),
1735        contexts,
1736        providers,
1737        consumers,
1738        forms,
1739        resource_endpoint_resolutions,
1740        opaque_action_resolutions,
1741        resource_declarations,
1742        resource_activations,
1743        form_field_declaration_candidates,
1744        form_fields,
1745        form_field_binding_candidates,
1746        form_field_bindings,
1747        form_ownership,
1748        validation_rule_candidates,
1749        validation_rules,
1750        validation_graph,
1751        validation_dependency_plans,
1752        form_tracking,
1753        submissions,
1754        submission_host_candidates: submission_host_products.candidates,
1755        submission_hosts: submission_host_products.hosts,
1756        serialization,
1757        reset,
1758        form_ir,
1759        optimized_form_ir,
1760        runtime_forms,
1761        slots,
1762        component_invocations,
1763        component_instance_plan,
1764        component_instance_scope,
1765        component_composition,
1766        component_initialization,
1767        component_ir: ComponentIrReport::default(),
1768        component_ir_optimization: OptimizedComponentIrReport::default(),
1769        instance_context,
1770        slot_content_fragments: slot_composition.fragments,
1771        slot_outlets: slot_composition.outlets,
1772        slot_bindings,
1773        composition_types,
1774        context_declaration_candidates,
1775        context_ownership,
1776        context_dependency,
1777        context_lifetime,
1778        context_evaluation,
1779        component_scope,
1780        context_resolutions,
1781        context_types: context_type_products.contexts,
1782        provider_types: context_type_products.providers,
1783        consumer_types: context_type_products.consumers,
1784        context_binding_types: context_type_products.bindings,
1785        duplicate_provider_declarations,
1786        computed_values,
1787        effects,
1788        effect_reactive_analysis,
1789        effect_trigger_plan,
1790        effect_execution_plan,
1791        effect_bodies,
1792        effect_statements,
1793        reactive_graph,
1794        reactive_transitive_analysis,
1795        reactive_cycle_analysis,
1796        computed_evaluation_plan,
1797        templates,
1798        template_entities,
1799        diagnostics,
1800        ownership,
1801        references,
1802        provenance,
1803    };
1804    model
1805        .diagnostics
1806        .extend(crate::collect_effect_diagnostics(&model));
1807    model
1808        .diagnostics
1809        .extend(crate::collect_context_diagnostics(&model));
1810    model
1811        .diagnostics
1812        .extend(crate::collect_form_diagnostics(&model));
1813    model.component_ir = lower_component_ir(&model);
1814    model.component_ir_optimization = optimize_component_ir(&model.component_ir);
1815    model
1816        .diagnostics
1817        .extend(crate::collect_component_diagnostics(&model));
1818    model
1819}
1820
1821#[must_use]
1822pub fn build_application_semantic_model_for_unit(
1823    unit: &CompilationUnit,
1824) -> ApplicationSemanticModel {
1825    build_application_semantic_model_for_unit_with_packages(
1826        unit,
1827        &crate::semantic_package::SemanticPackageResolutionTable::default(),
1828    )
1829}
1830
1831/// Build the canonical application model with caller-supplied package semantics.
1832#[must_use]
1833pub fn build_application_semantic_model_for_unit_with_packages(
1834    unit: &CompilationUnit,
1835    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1836) -> ApplicationSemanticModel {
1837    let symbols = crate::build_symbol_table(unit);
1838    let modules = crate::build_module_graph(unit);
1839    let bindings =
1840        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1841    build_application_semantic_model_from_files_with_bindings(unit.files(), Some(&bindings))
1842}
1843
1844/// Builds the canonical application model for a discovered file-route source
1845/// set. Conventional layouts are compiler-issued default-Slot composition
1846/// edges, so every downstream product observes the same instance topology.
1847///
1848/// # Errors
1849///
1850/// Returns stable route or layout composition diagnostics before publication
1851/// can materialize an incomplete page.
1852pub fn build_file_route_application_semantic_model_for_unit_with_packages(
1853    unit: &CompilationUnit,
1854    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1855) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1856    let symbols = crate::build_symbol_table(unit);
1857    let modules = crate::build_module_graph(unit);
1858    let bindings =
1859        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1860    build_application_semantic_model_from_files_with_bindings_mode(
1861        unit.files(),
1862        Some(&bindings),
1863        ApplicationAssemblyMode::FileRoutes,
1864    )
1865}
1866
1867/// File-route assembly with explicit canonical V2 authoring evidence. Entries
1868/// absent from `v2_authoring` remain on the named legacy compatibility path;
1869/// entries present in the map are projected through the V2 graph adapter.
1870pub fn build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
1871    unit: &CompilationUnit,
1872    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1873    v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
1874) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1875    let symbols = crate::build_symbol_table(unit);
1876    let modules = crate::build_module_graph(unit);
1877    let bindings =
1878        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1879    build_application_semantic_model_from_files_with_bindings_mode_and_v2(
1880        unit.files(),
1881        Some(&bindings),
1882        ApplicationAssemblyMode::FileRoutes,
1883        Some(v2_authoring),
1884    )
1885}
1886
1887/// Builds the canonical composed model for one already-resolved file-route
1888/// page. Publication uses this route-scoped entry so shared layouts never
1889/// project sibling pages into the same default Slot.
1890///
1891/// # Errors
1892///
1893/// Returns stable route/layout diagnostics, including an unknown selected
1894/// route component.
1895pub fn build_file_route_application_semantic_model_for_route_with_packages(
1896    unit: &CompilationUnit,
1897    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1898    route_component: &SemanticId,
1899) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1900    let symbols = crate::build_symbol_table(unit);
1901    let modules = crate::build_module_graph(unit);
1902    let bindings =
1903        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1904    build_application_semantic_model_from_files_with_bindings_mode(
1905        unit.files(),
1906        Some(&bindings),
1907        ApplicationAssemblyMode::FileRoute(route_component.clone()),
1908    )
1909}
1910
1911/// Route-scoped V2-aware counterpart to
1912/// [`build_file_route_application_semantic_model_for_route_with_packages`].
1913pub fn build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring(
1914    unit: &CompilationUnit,
1915    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1916    route_component: &SemanticId,
1917    v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
1918) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1919    let symbols = crate::build_symbol_table(unit);
1920    let modules = crate::build_module_graph(unit);
1921    let bindings =
1922        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1923    build_application_semantic_model_from_files_with_bindings_mode_and_v2(
1924        unit.files(),
1925        Some(&bindings),
1926        ApplicationAssemblyMode::FileRoute(route_component.clone()),
1927        Some(v2_authoring),
1928    )
1929}
1930
1931fn build_application_semantic_model_from_files(files: &[ParsedFile]) -> ApplicationSemanticModel {
1932    build_application_semantic_model_from_files_with_bindings(files, None)
1933}
1934
1935#[allow(clippy::too_many_lines)]
1936fn build_application_semantic_model_from_files_with_bindings(
1937    files: &[ParsedFile],
1938    bindings: Option<&crate::BindingTable>,
1939) -> ApplicationSemanticModel {
1940    build_application_semantic_model_from_files_with_bindings_mode(
1941        files,
1942        bindings,
1943        ApplicationAssemblyMode::Authored,
1944    )
1945    .expect("authored application assembly cannot derive file-route diagnostics")
1946}
1947
1948#[derive(Debug, Clone, PartialEq, Eq)]
1949enum ApplicationAssemblyMode {
1950    Authored,
1951    FileRoutes,
1952    FileRoute(SemanticId),
1953}
1954
1955#[allow(clippy::too_many_lines)]
1956fn build_application_semantic_model_from_files_with_bindings_mode(
1957    files: &[ParsedFile],
1958    bindings: Option<&crate::BindingTable>,
1959    mode: ApplicationAssemblyMode,
1960) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1961    build_application_semantic_model_from_files_with_bindings_mode_and_v2(
1962        files, bindings, mode, None,
1963    )
1964}
1965
1966#[allow(clippy::too_many_lines)]
1967fn build_application_semantic_model_from_files_with_bindings_mode_and_v2(
1968    files: &[ParsedFile],
1969    bindings: Option<&crate::BindingTable>,
1970    mode: ApplicationAssemblyMode,
1971    v2_authoring: Option<&BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>>,
1972) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1973    let (mut components, mut templates, mut diagnostics, mut references) =
1974        (Vec::new(), Vec::new(), Vec::new(), Vec::new());
1975    let (mut provenance, mut template_entities, mut type_aliases) =
1976        (BTreeMap::new(), Vec::new(), Vec::new());
1977    for parsed in files {
1978        let component_graph = v2_authoring
1979            .and_then(|models| models.get(&parsed.path))
1980            .map_or_else(
1981                || build_component_graph_for_module(parsed),
1982                |model| build_v2_component_graph_for_module(parsed, model),
1983            );
1984        let template_graph = build_template_graph(&component_graph);
1985        let file_template_entities = build_template_semantic_entities(&template_graph.templates);
1986
1987        components.extend(component_graph.components);
1988        templates.extend(template_graph.templates);
1989        diagnostics.extend(component_graph.diagnostics);
1990        references.extend(component_graph.references);
1991        provenance.extend(component_graph.provenance);
1992        extend_template_entity_provenance(&mut provenance, &file_template_entities);
1993        template_entities.extend(file_template_entities);
1994        type_aliases.extend(
1995            parsed
1996                .type_aliases
1997                .iter()
1998                .cloned()
1999                .map(|alias| (parsed.path.clone(), alias)),
2000        );
2001    }
2002
2003    resolve_builtin_pure_calls(&mut components);
2004
2005    if let Some(bindings) = bindings {
2006        diagnostics.extend(bindings.diagnostics.iter().map(|diagnostic| {
2007            ComponentDiagnostic::error(
2008                diagnostic.code.clone(),
2009                format!("{}: {}", diagnostic.module.display(), diagnostic.message),
2010            )
2011        }));
2012        resolve_semantic_package_pure_calls(&mut components, bindings);
2013    }
2014
2015    let resource_endpoint_resolutions =
2016        collect_resource_endpoint_resolutions(&components, bindings);
2017    diagnostics.extend(collect_resource_lowering_diagnostics(
2018        &resource_endpoint_resolutions,
2019    ));
2020    let opaque_action_resolutions = collect_opaque_action_resolutions(&components, bindings);
2021    diagnostics.extend(collect_opaque_action_lowering_diagnostics(
2022        &opaque_action_resolutions,
2023    ));
2024
2025    let mut component_invocations = collect_component_invocations(
2026        &components,
2027        &templates,
2028        &template_entities,
2029        files,
2030        bindings,
2031        &provenance,
2032    );
2033    let virtual_invocations = match mode {
2034        ApplicationAssemblyMode::Authored => BTreeMap::new(),
2035        ApplicationAssemblyMode::FileRoutes | ApplicationAssemblyMode::FileRoute(_) => {
2036            let mut graph = crate::build_validated_file_route_graph_from_components_v1(&components)
2037                .map_err(|error| FileRouteApplicationModelErrorV1 {
2038                    code: error.code,
2039                    message: error.message,
2040                })?;
2041            if let ApplicationAssemblyMode::FileRoute(selected) = &mode {
2042                graph.routes.retain(|route| route.component == *selected);
2043                if graph.routes.is_empty() {
2044                    return Err(FileRouteApplicationModelErrorV1 {
2045                        code: "PSROUTE1014_FILE_ROUTE_COMPONENT_UNRESOLVED",
2046                        message: format!("component `{selected}` is not a conventional file route"),
2047                    });
2048                }
2049            }
2050            let plan = crate::build_layout_composition_plan_from_components_v1(&components, &graph)
2051                .map_err(|error| FileRouteApplicationModelErrorV1 {
2052                    code: error.code,
2053                    message: error.message,
2054                })?;
2055            crate::layout_composition_virtual_invocations_from_provenance_v1(&plan, &provenance)
2056        }
2057    };
2058    let component_instance_plan = plan_component_instances_with_virtual_invocations(
2059        &components,
2060        &component_invocations,
2061        &virtual_invocations,
2062        &template_entities,
2063        &provenance,
2064    );
2065    component_invocations.extend(virtual_invocations.clone());
2066    let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
2067    let component_composition = analyze_component_composition(
2068        &components
2069            .iter()
2070            .map(|component| component.id.clone())
2071            .collect(),
2072        &component_invocations,
2073        &component_instance_plan,
2074    );
2075
2076    let (computed_values, computed_diagnostics) = classify_computed_values(
2077        &components,
2078        collect_computed_values(&components, &provenance),
2079        &provenance,
2080    );
2081    let effects = collect_effects(&components, &provenance);
2082    let expression_graph = ExpressionGraph::from_components(&components, &provenance);
2083    let contexts = collect_context_entities(&components, &expression_graph);
2084    let forms = collect_form_entities(&components);
2085    let base_semantic_types = SemanticTypeModel::from_components_with_aliases_and_bindings(
2086        &components,
2087        &provenance,
2088        &type_aliases,
2089        bindings,
2090    );
2091    let resource_declarations = collect_resource_declarations(
2092        &components,
2093        &resource_endpoint_resolutions,
2094        &base_semantic_types,
2095        bindings,
2096    );
2097    let resource_activations =
2098        collect_resource_activations(&resource_declarations, &component_instance_plan);
2099    let form_field_products =
2100        collect_form_field_products(&components, &forms, &base_semantic_types, bindings);
2101    let form_field_declaration_candidates = form_field_products.candidates;
2102    let form_fields = form_field_products.fields;
2103    let form_field_binding_products = collect_form_field_binding_products(
2104        &components,
2105        &templates,
2106        &forms,
2107        &form_fields,
2108        &form_field_declaration_candidates,
2109    );
2110    let form_field_binding_candidates = form_field_binding_products.candidates;
2111    let form_field_bindings = form_field_binding_products.bindings;
2112    let slots = collect_slot_entities(&components);
2113    let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
2114    let slot_bindings = collect_slot_bindings_with_virtual_invocations(
2115        &component_instance_plan,
2116        &component_invocations,
2117        &virtual_invocations,
2118        &slots,
2119        &slot_composition.fragments,
2120        &slot_composition.outlets,
2121    );
2122    let (providers, duplicate_provider_declarations) =
2123        collect_provider_entities(&components, &contexts, &expression_graph, bindings);
2124    let consumers = collect_consumer_entities(&components, &contexts, bindings);
2125    let instance_context = collect_instance_context_registry(
2126        &component_instance_scope,
2127        &contexts,
2128        &providers,
2129        &consumers,
2130    );
2131    let context_declaration_candidates =
2132        collect_context_declaration_candidates(&components, &contexts, &providers, &consumers);
2133    let component_scope = ComponentScopeGraph::reflexive(&components);
2134    let context_resolutions = collect_context_resolutions(
2135        &consumers,
2136        &contexts,
2137        &providers,
2138        &expression_graph,
2139        &component_scope,
2140    );
2141    diagnostics.extend(computed_diagnostics);
2142    extend_derived_entity_provenance(
2143        &mut provenance,
2144        &contexts,
2145        &forms,
2146        &form_fields,
2147        &form_field_bindings,
2148        &providers,
2149        &consumers,
2150        &slots,
2151        &component_invocations,
2152        &slot_composition.fragments,
2153        &slot_composition.outlets,
2154        &component_instance_plan,
2155        &computed_values,
2156        &effects,
2157    );
2158    let mut ownership = collect_ownership(
2159        &components,
2160        &contexts,
2161        &forms,
2162        &form_fields,
2163        &form_field_bindings,
2164        &providers,
2165        &consumers,
2166        &slots,
2167        &component_invocations,
2168        &slot_composition.fragments,
2169        &slot_composition.outlets,
2170        &component_instance_plan,
2171        &computed_values,
2172        &effects,
2173        &templates,
2174        &template_entities,
2175    );
2176    let context_ownership = collect_context_ownership_graph(
2177        &components,
2178        &contexts,
2179        &providers,
2180        &consumers,
2181        &expression_graph,
2182        &provenance,
2183    );
2184    let (effect_bodies, effect_statements) =
2185        lower_effect_bodies(&components, &effects, &expression_graph);
2186    references.extend(build_computed_references(
2187        &components,
2188        &computed_values,
2189        &resource_declarations,
2190        &expression_graph,
2191    ));
2192    references.extend(build_effect_references(
2193        &components,
2194        &effects,
2195        &computed_values,
2196        &expression_graph,
2197    ));
2198    references.extend(build_provider_references(&providers));
2199    references.extend(build_consumer_references(&consumers));
2200    references.extend(build_context_resolution_references(&context_resolutions));
2201    extend_template_references(
2202        &mut references,
2203        &components,
2204        &computed_values,
2205        &template_entities,
2206        &ownership,
2207    );
2208    references.extend(build_form_field_binding_references(&form_field_bindings));
2209    let form_ownership = collect_form_ownership_graph(
2210        &component_instance_plan.roots,
2211        &components,
2212        &forms,
2213        &form_fields,
2214        &form_field_bindings,
2215        &template_entities,
2216        &ownership,
2217        &references,
2218        &provenance,
2219    );
2220    let validation_products = collect_validation_products(&components, &forms, &form_fields);
2221    let validation_rule_candidates = validation_products.candidates;
2222    let validation_rules = validation_products.rules;
2223    extend_validation_products(
2224        &mut provenance,
2225        &mut ownership,
2226        &mut references,
2227        &validation_rules,
2228    );
2229    let validation_graph = collect_validation_graph(
2230        &component_instance_plan.roots,
2231        &form_ownership,
2232        &forms,
2233        &form_fields,
2234        &validation_rules,
2235        &validation_rule_candidates,
2236        &validation_products.cycles,
2237        &ownership,
2238        &references,
2239    );
2240    let validation_dependency_plans = collect_validation_dependency_plans(
2241        &forms,
2242        &form_fields,
2243        &validation_rules,
2244        &form_ownership,
2245        &validation_graph,
2246    );
2247    let form_tracking =
2248        collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
2249    let semantic_types = finalize_semantic_types(
2250        base_semantic_types.with_resource_types(&resource_declarations),
2251        &components,
2252        &contexts,
2253        &forms,
2254        &form_fields,
2255        &providers,
2256        &consumers,
2257        &slots,
2258        &computed_values,
2259        &effects,
2260        &effect_statements,
2261        &expression_graph,
2262        &references,
2263        &template_entities,
2264    );
2265    let context_type_products = collect_context_type_products(
2266        &contexts,
2267        &providers,
2268        &consumers,
2269        &context_resolutions,
2270        &expression_graph,
2271        &semantic_types,
2272    );
2273    let context_dependency = collect_context_dependency_graph(
2274        &components,
2275        &contexts,
2276        &providers,
2277        &consumers,
2278        &context_resolutions,
2279        &context_type_products.contexts,
2280        &context_type_products.providers,
2281        &context_type_products.bindings,
2282        &computed_values,
2283        &expression_graph,
2284    );
2285    let context_lifetime = collect_context_lifetime_analysis(
2286        &components,
2287        &contexts,
2288        &providers,
2289        &consumers,
2290        &computed_values,
2291        &context_ownership,
2292        &component_scope,
2293        &context_resolutions,
2294        &context_dependency,
2295        &provenance,
2296    );
2297    let effects = validate_effects(&components, effects, &effect_statements, &semantic_types);
2298    let (
2299        reactive_graph,
2300        reactive_transitive_analysis,
2301        reactive_cycle_analysis,
2302        computed_evaluation_plan,
2303    ) = build_computed_reactive_products(
2304        &components,
2305        &computed_values,
2306        &effects,
2307        &resource_declarations,
2308        &references,
2309        &provenance,
2310    );
2311    let context_evaluation = collect_context_evaluation_plan(
2312        &contexts,
2313        &providers,
2314        &context_resolutions,
2315        &context_type_products.contexts,
2316        &context_type_products.providers,
2317        &context_type_products.bindings,
2318        &context_lifetime,
2319        &context_dependency,
2320        &computed_evaluation_plan,
2321        &component_scope,
2322    );
2323    let effect_reactive_analysis = analyze_effect_reactivity(
2324        &components,
2325        &computed_values,
2326        &effects,
2327        &reactive_transitive_analysis,
2328    );
2329    let effect_trigger_plan = derive_effect_trigger_plan(
2330        &components,
2331        &effects,
2332        &effect_reactive_analysis,
2333        &provenance,
2334    );
2335    let submissions = collect_submission_products(
2336        &components,
2337        &forms,
2338        &form_fields,
2339        &validation_rules,
2340        &effect_trigger_plan,
2341    );
2342    let serialization =
2343        collect_serialization_products(&components, &forms, &form_fields, &submissions.plans);
2344    let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
2345    let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
2346    let optimized_form_ir = optimize_form_ir(&form_ir);
2347    let submission_host_products = collect_submission_host_products(
2348        &templates,
2349        &components,
2350        &forms,
2351        &form_field_bindings,
2352        &submissions.plans,
2353        &serialization.plans,
2354        &optimized_form_ir.optimized,
2355    );
2356    let runtime_forms = build_runtime_form_registry(
2357        &forms,
2358        &form_fields,
2359        &form_field_bindings,
2360        &validation_rules,
2361        &optimized_form_ir.optimized,
2362        &submissions,
2363        &serialization,
2364        &reset,
2365        &submission_host_products.hosts,
2366    );
2367    let effect_execution_plan = plan_effect_execution(
2368        &computed_values,
2369        &effects,
2370        &effect_reactive_analysis,
2371        &effect_trigger_plan,
2372        &reactive_transitive_analysis,
2373        &computed_evaluation_plan,
2374    );
2375    extend_computed_diagnostics(
2376        &mut diagnostics,
2377        &components,
2378        &computed_values,
2379        &resource_declarations,
2380        &expression_graph,
2381        &semantic_types,
2382        &reactive_cycle_analysis,
2383        &provenance,
2384    );
2385    let component_ids = components
2386        .iter()
2387        .map(|component| component.id.clone())
2388        .collect::<BTreeSet<_>>();
2389    let composition_types = collect_composition_type_products(
2390        &component_ids,
2391        &component_invocations,
2392        &slot_bindings,
2393        &slots,
2394        &slot_composition.fragments,
2395        &slot_composition.outlets,
2396        &instance_context,
2397        &context_type_products.bindings,
2398        &context_type_products.contexts,
2399        &context_type_products.providers,
2400        &context_lifetime,
2401        &ownership,
2402        &references,
2403    );
2404    let component_initialization = plan_component_initialization(
2405        &component_instance_plan,
2406        &slot_bindings,
2407        &composition_types,
2408        &instance_context,
2409    );
2410    let components = components_with_resolved_form_field_candidates(
2411        &components,
2412        &form_field_declaration_candidates,
2413    );
2414    let mut model = ApplicationSemanticModel {
2415        expression_graph,
2416        semantic_types,
2417        components,
2418        contexts,
2419        providers,
2420        consumers,
2421        forms,
2422        resource_endpoint_resolutions,
2423        opaque_action_resolutions,
2424        resource_declarations,
2425        resource_activations,
2426        form_field_declaration_candidates,
2427        form_fields,
2428        form_field_binding_candidates,
2429        form_field_bindings,
2430        form_ownership,
2431        validation_rule_candidates,
2432        validation_rules,
2433        validation_graph,
2434        validation_dependency_plans,
2435        form_tracking,
2436        submissions,
2437        submission_host_candidates: submission_host_products.candidates,
2438        submission_hosts: submission_host_products.hosts,
2439        serialization,
2440        reset,
2441        form_ir,
2442        optimized_form_ir,
2443        runtime_forms,
2444        slots,
2445        component_invocations,
2446        component_instance_plan,
2447        component_instance_scope,
2448        component_composition,
2449        component_initialization,
2450        component_ir: ComponentIrReport::default(),
2451        component_ir_optimization: OptimizedComponentIrReport::default(),
2452        instance_context,
2453        slot_content_fragments: slot_composition.fragments,
2454        slot_outlets: slot_composition.outlets,
2455        slot_bindings,
2456        composition_types,
2457        context_declaration_candidates,
2458        context_ownership,
2459        context_dependency,
2460        context_lifetime,
2461        context_evaluation,
2462        component_scope,
2463        context_resolutions,
2464        context_types: context_type_products.contexts,
2465        provider_types: context_type_products.providers,
2466        consumer_types: context_type_products.consumers,
2467        context_binding_types: context_type_products.bindings,
2468        duplicate_provider_declarations,
2469        computed_values,
2470        effects,
2471        effect_reactive_analysis,
2472        effect_trigger_plan,
2473        effect_execution_plan,
2474        effect_bodies,
2475        effect_statements,
2476        reactive_graph,
2477        reactive_transitive_analysis,
2478        reactive_cycle_analysis,
2479        computed_evaluation_plan,
2480        templates,
2481        template_entities,
2482        diagnostics,
2483        ownership,
2484        references,
2485        provenance,
2486    };
2487    model
2488        .diagnostics
2489        .extend(crate::collect_effect_diagnostics(&model));
2490    model
2491        .diagnostics
2492        .extend(crate::collect_context_diagnostics(&model));
2493    model
2494        .diagnostics
2495        .extend(crate::collect_form_diagnostics(&model));
2496    model.component_ir = lower_component_ir(&model);
2497    model.component_ir_optimization = optimize_component_ir(&model.component_ir);
2498    model
2499        .diagnostics
2500        .extend(crate::collect_component_diagnostics(&model));
2501    Ok(model)
2502}
2503
2504fn resolve_builtin_pure_calls(components: &mut [ComponentNode]) {
2505    for component in components {
2506        for method in component
2507            .methods
2508            .iter_mut()
2509            .filter(|method| method.is_computed())
2510        {
2511            if let Some(expression) = method.computed_expression.as_mut() {
2512                resolve_builtin_pure_call(expression);
2513            }
2514        }
2515    }
2516}
2517
2518fn resolve_builtin_pure_call(expression: &mut ComputedExpression) {
2519    match &mut expression.kind {
2520        ComputedExpressionKind::Call { callee, arguments } => {
2521            for argument in arguments.iter_mut() {
2522                resolve_builtin_pure_call(argument);
2523            }
2524            let operation = match (callee.as_str(), arguments.len()) {
2525                ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
2526                ("Math.floor", 1) => Some(crate::component_graph::BuiltinPureOperation::MathFloor),
2527                ("Math.ceil", 1) => Some(crate::component_graph::BuiltinPureOperation::MathCeil),
2528                ("Math.round", 1) => Some(crate::component_graph::BuiltinPureOperation::MathRound),
2529                ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
2530                ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
2531                _ => None,
2532            };
2533            if let Some(operation) = operation {
2534                expression.kind = ComputedExpressionKind::BuiltinPureCall {
2535                    operation,
2536                    arguments: std::mem::take(arguments),
2537                };
2538            }
2539        }
2540        ComputedExpressionKind::BuiltinPureCall { arguments, .. }
2541        | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
2542            for argument in arguments {
2543                resolve_builtin_pure_call(argument);
2544            }
2545        }
2546        ComputedExpressionKind::Template { expressions, .. } => {
2547            for expression in expressions {
2548                resolve_builtin_pure_call(expression);
2549            }
2550        }
2551        ComputedExpressionKind::MemberAccess { object, .. }
2552        | ComputedExpressionKind::Unary {
2553            operand: object, ..
2554        } => {
2555            resolve_builtin_pure_call(object);
2556        }
2557        ComputedExpressionKind::IndexAccess { object, index } => {
2558            resolve_builtin_pure_call(object);
2559            resolve_builtin_pure_call(index);
2560        }
2561        ComputedExpressionKind::Conditional {
2562            condition,
2563            when_true,
2564            when_false,
2565        } => {
2566            resolve_builtin_pure_call(condition);
2567            resolve_builtin_pure_call(when_true);
2568            resolve_builtin_pure_call(when_false);
2569        }
2570        ComputedExpressionKind::Arithmetic { left, right, .. }
2571        | ComputedExpressionKind::Comparison { left, right, .. }
2572        | ComputedExpressionKind::Logical { left, right, .. }
2573        | ComputedExpressionKind::NullishCoalescing { left, right } => {
2574            resolve_builtin_pure_call(left);
2575            resolve_builtin_pure_call(right);
2576        }
2577        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
2578    }
2579}
2580
2581fn components_with_resolved_form_field_candidates(
2582    components: &[ComponentNode],
2583    candidates: &[crate::FormFieldDeclarationCandidate],
2584) -> Vec<ComponentNode> {
2585    let candidates = candidates
2586        .iter()
2587        .map(|candidate| (candidate.id.clone(), candidate))
2588        .collect::<BTreeMap<_, _>>();
2589    let mut components = components.to_vec();
2590    for component in &mut components {
2591        for candidate in &mut component.form_field_declaration_candidates {
2592            if let Some(resolved) = candidates.get(&candidate.id) {
2593                *candidate = (*resolved).clone();
2594            }
2595        }
2596    }
2597    components
2598}
2599
2600fn extend_template_entity_provenance(
2601    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2602    template_entities: &[TemplateSemanticEntity],
2603) {
2604    provenance.extend(
2605        template_entities
2606            .iter()
2607            .map(|entity| (entity.id.clone(), entity.provenance.clone())),
2608    );
2609}
2610
2611fn extend_validation_products(
2612    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2613    ownership: &mut BTreeMap<SemanticId, SemanticOwner>,
2614    references: &mut Vec<SemanticReference>,
2615    rules: &BTreeMap<ValidationRuleId, ValidationRule>,
2616) {
2617    for rule in rules.values() {
2618        provenance.insert(rule.id.as_semantic_id().clone(), rule.provenance.clone());
2619        ownership.insert(
2620            rule.id.as_semantic_id().clone(),
2621            SemanticOwner::entity(rule.target_field.as_semantic_id().clone()),
2622        );
2623        if let Some(dependency) = &rule.dependency {
2624            references.push(SemanticReference {
2625                kind: SemanticReferenceKind::ValidationRuleField,
2626                source: rule.id.as_semantic_id().clone(),
2627                target: dependency.as_semantic_id().clone(),
2628                provenance: rule
2629                    .argument_provenance
2630                    .clone()
2631                    .unwrap_or_else(|| rule.decorator_provenance.clone()),
2632            });
2633        }
2634    }
2635    references.sort_by(|left, right| {
2636        (left.source.as_str(), left.kind, left.target.as_str()).cmp(&(
2637            right.source.as_str(),
2638            right.kind,
2639            right.target.as_str(),
2640        ))
2641    });
2642    references.dedup();
2643}
2644
2645#[allow(clippy::too_many_arguments)]
2646fn extend_derived_entity_provenance(
2647    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2648    contexts: &BTreeMap<ContextId, ContextEntity>,
2649    forms: &BTreeMap<FormId, FormEntity>,
2650    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2651    form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
2652    providers: &BTreeMap<ProviderId, ProviderEntity>,
2653    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2654    slots: &BTreeMap<SlotId, SlotEntity>,
2655    component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
2656    slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
2657    slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
2658    component_instance_plan: &ComponentInstancePlan,
2659    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2660    effects: &BTreeMap<SemanticId, Effect>,
2661) {
2662    provenance.extend(contexts.values().map(|context| {
2663        (
2664            context.id.as_semantic_id().clone(),
2665            context.provenance.clone(),
2666        )
2667    }));
2668    provenance.extend(
2669        forms
2670            .values()
2671            .map(|form| (form.id.as_semantic_id().clone(), form.provenance.clone())),
2672    );
2673    provenance.extend(
2674        form_fields
2675            .values()
2676            .map(|field| (field.id.as_semantic_id().clone(), field.provenance.clone())),
2677    );
2678    provenance.extend(form_field_bindings.values().map(|binding| {
2679        (
2680            binding.id.as_semantic_id().clone(),
2681            binding.provenance.clone(),
2682        )
2683    }));
2684    provenance.extend(providers.values().map(|provider| {
2685        (
2686            provider.id.as_semantic_id().clone(),
2687            provider.provenance.clone(),
2688        )
2689    }));
2690    provenance.extend(consumers.values().map(|consumer| {
2691        (
2692            consumer.id.as_semantic_id().clone(),
2693            consumer.provenance.clone(),
2694        )
2695    }));
2696    provenance.extend(
2697        slots
2698            .values()
2699            .map(|slot| (slot.id.as_semantic_id().clone(), slot.provenance.clone())),
2700    );
2701    provenance.extend(component_invocations.values().map(|invocation| {
2702        (
2703            invocation.id.as_semantic_id().clone(),
2704            invocation.provenance.clone(),
2705        )
2706    }));
2707    provenance.extend(slot_content_fragments.values().map(|fragment| {
2708        (
2709            fragment.id.as_semantic_id().clone(),
2710            fragment.provenance.clone(),
2711        )
2712    }));
2713    provenance.extend(slot_outlets.values().map(|outlet| {
2714        (
2715            outlet.id.as_semantic_id().clone(),
2716            outlet.provenance.clone(),
2717        )
2718    }));
2719    provenance.extend(component_instance_plan.instances.values().map(|instance| {
2720        (
2721            instance.id.as_semantic_id().clone(),
2722            instance.provenance.clone(),
2723        )
2724    }));
2725    provenance.extend(component_instance_plan.blocked.values().map(|blocked| {
2726        (
2727            blocked.id.as_semantic_id().clone(),
2728            blocked.provenance.clone(),
2729        )
2730    }));
2731    provenance.extend(
2732        computed_values
2733            .iter()
2734            .map(|(id, computed)| (id.clone(), computed.provenance.clone())),
2735    );
2736    provenance.extend(
2737        effects
2738            .iter()
2739            .map(|(id, effect)| (id.clone(), effect.provenance.clone())),
2740    );
2741}
2742
2743#[allow(clippy::too_many_arguments)]
2744fn finalize_semantic_types(
2745    semantic_types: SemanticTypeModel,
2746    components: &[ComponentNode],
2747    contexts: &BTreeMap<ContextId, ContextEntity>,
2748    forms: &BTreeMap<FormId, FormEntity>,
2749    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2750    providers: &BTreeMap<ProviderId, ProviderEntity>,
2751    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2752    slots: &BTreeMap<SlotId, SlotEntity>,
2753    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2754    effects: &BTreeMap<SemanticId, Effect>,
2755    effect_statements: &BTreeMap<SemanticId, EffectStatement>,
2756    expression_graph: &ExpressionGraph,
2757    references: &[SemanticReference],
2758    template_entities: &[TemplateSemanticEntity],
2759) -> SemanticTypeModel {
2760    semantic_types
2761        .with_slot_types(slots)
2762        .with_form_types(forms)
2763        .with_form_field_types(form_fields)
2764        .with_context_types(contexts)
2765        .with_provider_types(providers)
2766        .with_consumer_types(consumers)
2767        .with_expression_types(expression_graph, components, contexts, providers)
2768        .with_computed_value_types(components, computed_values, expression_graph, references)
2769        .with_context_source_expression_types(components, contexts, providers, expression_graph)
2770        .with_effect_statement_types(components, effects, effect_statements, expression_graph)
2771        .with_template_binding_types(template_entities, references)
2772        .normalized()
2773}
2774
2775#[allow(clippy::too_many_arguments)]
2776fn extend_computed_diagnostics(
2777    diagnostics: &mut Vec<ComponentDiagnostic>,
2778    components: &[ComponentNode],
2779    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2780    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
2781    expression_graph: &ExpressionGraph,
2782    semantic_types: &SemanticTypeModel,
2783    reactive_cycle_analysis: &IrReactiveCycleAnalysis,
2784    provenance: &BTreeMap<SemanticId, SourceProvenance>,
2785) {
2786    diagnostics.extend(collect_computed_cycle_diagnostics(
2787        reactive_cycle_analysis,
2788        computed_values,
2789    ));
2790    diagnostics.extend(collect_computed_semantic_diagnostics(
2791        components,
2792        computed_values,
2793        resource_declarations,
2794        expression_graph,
2795        semantic_types,
2796        provenance,
2797    ));
2798}
2799
2800#[allow(clippy::too_many_lines)]
2801fn classify_computed_values(
2802    components: &[ComponentNode],
2803    mut computed_values: BTreeMap<SemanticId, ComputedValue>,
2804    provenance: &BTreeMap<SemanticId, SourceProvenance>,
2805) -> (
2806    BTreeMap<SemanticId, ComputedValue>,
2807    Vec<ComponentDiagnostic>,
2808) {
2809    let mut diagnostics = Vec::new();
2810
2811    for computed in computed_values.values_mut() {
2812        let Some(component_id) = computed.owner.entity_id() else {
2813            continue;
2814        };
2815        let Some(component) = components
2816            .iter()
2817            .find(|component| component.id == *component_id)
2818        else {
2819            continue;
2820        };
2821        let Some(method) = component
2822            .methods
2823            .iter()
2824            .find(|method| method.id == computed.method)
2825        else {
2826            continue;
2827        };
2828        let mut violations = Vec::new();
2829
2830        if method.is_async {
2831            violations.push(crate::ComputedPurityViolation {
2832                kind: crate::ComputedPurityViolationKind::Async,
2833                provenance: computed.provenance.clone(),
2834            });
2835        }
2836        if method
2837            .decorators
2838            .iter()
2839            .any(|decorator| decorator == "action")
2840        {
2841            violations.push(crate::ComputedPurityViolation {
2842                kind: crate::ComputedPurityViolationKind::Action,
2843                provenance: computed.provenance.clone(),
2844            });
2845        }
2846        if method
2847            .decorators
2848            .iter()
2849            .any(|decorator| decorator == "effect")
2850        {
2851            violations.push(crate::ComputedPurityViolation {
2852                kind: crate::ComputedPurityViolationKind::Effect,
2853                provenance: computed.provenance.clone(),
2854            });
2855        }
2856        for action in component
2857            .actions
2858            .iter()
2859            .filter(|action| action.method == method.name)
2860        {
2861            violations.push(crate::ComputedPurityViolation {
2862                kind: crate::ComputedPurityViolationKind::StateMutation,
2863                provenance: provenance
2864                    .get(&action.id)
2865                    .expect("computed state update should have canonical provenance")
2866                    .clone(),
2867            });
2868        }
2869        for call in &method.calls {
2870            if method
2871                .computed_expression
2872                .as_ref()
2873                .is_some_and(|expression| {
2874                    contains_semantic_package_pure_call(expression, &call.callee)
2875                        || contains_builtin_pure_call(expression, &call.callee)
2876                })
2877            {
2878                continue;
2879            }
2880            let kind = computed_call_purity_kind(component, &call.callee);
2881            violations.push(crate::ComputedPurityViolation {
2882                kind,
2883                provenance: SourceProvenance::new(&computed.provenance.path, call.span),
2884            });
2885        }
2886
2887        violations.sort_by(|left, right| {
2888            (
2889                left.kind,
2890                left.provenance.path.as_path(),
2891                left.provenance.span.start,
2892                left.provenance.span.end,
2893            )
2894                .cmp(&(
2895                    right.kind,
2896                    right.provenance.path.as_path(),
2897                    right.provenance.span.start,
2898                    right.provenance.span.end,
2899                ))
2900        });
2901        violations
2902            .dedup_by(|left, right| left.kind == right.kind && left.provenance == right.provenance);
2903        computed.purity = if violations.is_empty() {
2904            crate::ComputedPurity::Pure
2905        } else {
2906            crate::ComputedPurity::Impure
2907        };
2908        computed.purity_violations = violations;
2909        diagnostics.extend(computed.purity_violations.iter().map(|violation| {
2910            ComponentDiagnostic {
2911                severity: ComponentDiagnosticSeverity::Error,
2912                effect_id: None,
2913                statement_id: None,
2914                context_declaration_candidate_id: None,
2915                context_id: None,
2916                provider_id: None,
2917                consumer_id: None,
2918                slot_id: None,
2919                invocation_id: None,
2920                component_instance_id: None,
2921                slot_binding_id: None,
2922                structural_region_id: None,
2923                component_id: None,
2924                provider_instance_id: None,
2925                consumer_instance_id: None,
2926                secondary_labels: Vec::new(),
2927                code: ComputedDiagnosticCode::PurityViolation.as_str().to_string(),
2928                message: format!(
2929                    "computed getter `{}` is impure: {}",
2930                    computed.name,
2931                    violation.kind.description()
2932                ),
2933                provenance: Some(violation.provenance.clone()),
2934            }
2935        }));
2936    }
2937
2938    (computed_values, diagnostics)
2939}
2940
2941fn resolve_semantic_package_pure_calls(
2942    components: &mut [ComponentNode],
2943    bindings: &crate::BindingTable,
2944) {
2945    for component in components {
2946        for method in component
2947            .methods
2948            .iter_mut()
2949            .filter(|method| method.is_computed())
2950        {
2951            let Some(expression) = method.computed_expression.as_mut() else {
2952                continue;
2953            };
2954            resolve_semantic_package_pure_call(expression, &component.module_path, bindings);
2955        }
2956    }
2957}
2958
2959fn collect_resource_endpoint_resolutions(
2960    components: &[ComponentNode],
2961    bindings: Option<&crate::BindingTable>,
2962) -> Vec<crate::ResourceEndpointResolution> {
2963    let mut resolutions = Vec::new();
2964    for component in components {
2965        for resource in &component.resource_declaration_candidates {
2966            let outcome = match resource.endpoint_designator.as_deref() {
2967                None => crate::ResourceEndpointResolutionOutcome::MissingDesignator,
2968                Some(designator) => {
2969                    let Some(bindings) = bindings else {
2970                        resolutions.push(crate::ResourceEndpointResolution {
2971                            owner_component: resource.owner_component.clone(),
2972                            field: resource.field.clone(),
2973                            endpoint_designator: resource.endpoint_designator.clone(),
2974                            outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
2975                                designator: designator.to_string(),
2976                            },
2977                            provenance: resource.provenance.clone(),
2978                        });
2979                        continue;
2980                    };
2981                    let Some(binding) = bindings.resolve_import(&component.module_path, designator)
2982                    else {
2983                        resolutions.push(crate::ResourceEndpointResolution {
2984                            owner_component: resource.owner_component.clone(),
2985                            field: resource.field.clone(),
2986                            endpoint_designator: resource.endpoint_designator.clone(),
2987                            outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
2988                                designator: designator.to_string(),
2989                            },
2990                            provenance: resource.provenance.clone(),
2991                        });
2992                        continue;
2993                    };
2994                    match &binding.target {
2995                        crate::ImportBindingTarget::SemanticPackage {
2996                            package,
2997                            version,
2998                            integrity,
2999                            export,
3000                            kind: crate::SemanticPackageKind::Resource,
3001                            type_signature,
3002                            runtime_module,
3003                            resume_policy,
3004                            resource_endpoint: Some(endpoint),
3005                            ..
3006                        } => crate::ResourceEndpointResolutionOutcome::Resolved(
3007                            crate::ResourceEndpointBinding {
3008                                local_name: binding.local_name.clone(),
3009                                package: package.clone(),
3010                                version: version.clone(),
3011                                integrity: integrity.clone(),
3012                                export: export.clone(),
3013                                type_signature: type_signature.clone(),
3014                                runtime_module: runtime_module.clone(),
3015                                resume_policy: resume_policy.clone(),
3016                                endpoint: endpoint.clone(),
3017                            },
3018                        ),
3019                        crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3020                            crate::ResourceEndpointResolutionOutcome::NonResourceBinding {
3021                                designator: designator.to_string(),
3022                                kind: kind.clone(),
3023                            }
3024                        }
3025                        _ => crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding {
3026                            designator: designator.to_string(),
3027                        },
3028                    }
3029                }
3030            };
3031            resolutions.push(crate::ResourceEndpointResolution {
3032                owner_component: resource.owner_component.clone(),
3033                field: resource.field.clone(),
3034                endpoint_designator: resource.endpoint_designator.clone(),
3035                outcome,
3036                provenance: resource.provenance.clone(),
3037            });
3038        }
3039    }
3040    resolutions
3041}
3042
3043fn collect_opaque_action_resolutions(
3044    components: &[ComponentNode],
3045    bindings: Option<&crate::BindingTable>,
3046) -> Vec<crate::OpaqueActionResolution> {
3047    let mut resolutions = Vec::new();
3048    for component in components {
3049        for fact in component
3050            .opaque_action_facts
3051            .iter()
3052            .filter(|fact| crate::component_graph::is_valid_opaque_action_fact(fact))
3053        {
3054            let (Some(owner_component), Some(method), Some(package_specifier), Some(export_name)) = (
3055                fact.owner_component.as_ref(),
3056                fact.method.as_ref(),
3057                fact.package.as_ref(),
3058                fact.export.as_ref(),
3059            ) else {
3060                continue;
3061            };
3062            let outcome = bindings
3063                .and_then(|bindings| bindings.module(&component.module_path))
3064                .and_then(|module| {
3065                    module.imports.values().find(|binding| {
3066                        binding.source_module == *package_specifier
3067                            && binding.imported_name == *export_name
3068                    })
3069                })
3070                .map_or_else(
3071                    || crate::OpaqueActionResolutionOutcome::UnboundImport {
3072                        package: package_specifier.clone(),
3073                        export: export_name.clone(),
3074                    },
3075                    |binding| match &binding.target {
3076                        crate::ImportBindingTarget::SemanticPackage {
3077                            package,
3078                            version,
3079                            integrity,
3080                            export,
3081                            kind: crate::SemanticPackageKind::Opaque,
3082                            type_signature,
3083                            runtime_module,
3084                            resume_policy,
3085                            opaque_terminal: Some(terminal),
3086                            ..
3087                        } => crate::OpaqueActionResolutionOutcome::Resolved(
3088                            crate::OpaqueTerminalBinding {
3089                                local_name: binding.local_name.clone(),
3090                                package: package.clone(),
3091                                version: version.clone(),
3092                                integrity: integrity.clone(),
3093                                export: export.clone(),
3094                                type_signature: type_signature.clone(),
3095                                runtime_module: runtime_module.clone(),
3096                                resume_policy: resume_policy.clone(),
3097                                terminal: terminal.clone(),
3098                            },
3099                        ),
3100                        crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3101                            crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3102                                package: package_specifier.clone(),
3103                                export: export_name.clone(),
3104                                kind: kind.clone(),
3105                            }
3106                        }
3107                        _ => crate::OpaqueActionResolutionOutcome::UnboundImport {
3108                            package: package_specifier.clone(),
3109                            export: export_name.clone(),
3110                        },
3111                    },
3112                );
3113            resolutions.push(crate::OpaqueActionResolution {
3114                activation: fact.id.clone(),
3115                owner_component: owner_component.clone(),
3116                method: method.clone(),
3117                method_name: fact.method_name.clone(),
3118                package_specifier: package_specifier.clone(),
3119                export_name: export_name.clone(),
3120                outcome,
3121                provenance: fact.provenance.clone(),
3122            });
3123        }
3124    }
3125    resolutions.sort_by(|left, right| left.activation.cmp(&right.activation));
3126    resolutions
3127}
3128
3129fn collect_opaque_action_lowering_diagnostics(
3130    resolutions: &[crate::OpaqueActionResolution],
3131) -> Vec<ComponentDiagnostic> {
3132    resolutions
3133        .iter()
3134        .filter_map(|resolution| {
3135            let message = match &resolution.outcome {
3136                crate::OpaqueActionResolutionOutcome::Resolved(_) => return None,
3137                crate::OpaqueActionResolutionOutcome::UnboundImport { package, export } => format!(
3138                    "opaque Action `{}` requires an imported opaque semantic-package export `{package}` / `{export}`",
3139                    resolution.method_name
3140                ),
3141                crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3142                    package,
3143                    export,
3144                    kind,
3145                } => format!(
3146                    "opaque Action `{}` selects `{package}` / `{export}` with package kind {kind:?}, not opaque",
3147                    resolution.method_name
3148                ),
3149            };
3150            let mut diagnostic = ComponentDiagnostic::error("PSC1131", message);
3151            diagnostic.provenance = Some(resolution.provenance.clone());
3152            Some(diagnostic)
3153        })
3154        .collect()
3155}
3156
3157fn collect_resource_declarations(
3158    components: &[ComponentNode],
3159    resolutions: &[crate::ResourceEndpointResolution],
3160    semantic_types: &SemanticTypeModel,
3161    bindings: Option<&crate::BindingTable>,
3162) -> BTreeMap<ResourceId, crate::ResourceDeclaration> {
3163    let mut declarations = BTreeMap::new();
3164    for component in components {
3165        for resource in &component.resource_declaration_candidates {
3166            let Some(resolution) = resolutions.iter().find(|resolution| {
3167                resolution.owner_component == resource.owner_component
3168                    && resolution.field == resource.field
3169            }) else {
3170                continue;
3171            };
3172            let crate::ResourceEndpointResolutionOutcome::Resolved(endpoint) = &resolution.outcome
3173            else {
3174                continue;
3175            };
3176            let Some(declared_type) = resource.declared_type.as_ref() else {
3177                continue;
3178            };
3179            let Some(resolved_type) = semantic_types.resolve_declared_type(declared_type, bindings)
3180            else {
3181                continue;
3182            };
3183            let crate::SemanticType::Resource(resource_type) = resolved_type.semantic_type else {
3184                continue;
3185            };
3186            let execution_boundary = match endpoint.endpoint.execution_boundary {
3187                crate::SemanticPackageResourceExecutionBoundary::Client => {
3188                    crate::ResourceExecutionBoundary::Client
3189                }
3190                crate::SemanticPackageResourceExecutionBoundary::Server => {
3191                    crate::ResourceExecutionBoundary::Server
3192                }
3193                crate::SemanticPackageResourceExecutionBoundary::Shared => {
3194                    crate::ResourceExecutionBoundary::Shared
3195                }
3196            };
3197            let key = format!("{}:{}", component.id.as_str(), resource.field);
3198            let declaration = crate::ResourceDeclaration::new(
3199                resource.owner_component.clone(),
3200                resource.field.clone(),
3201                key,
3202                *resource_type.data,
3203                *resource_type.error,
3204                execution_boundary,
3205                BTreeSet::new(),
3206                crate::ResourceRetryPolicy::ExplicitOnly,
3207                crate::ResourceInvalidationPolicy::ExplicitOnly,
3208                resource.provenance.clone(),
3209            );
3210            let Ok(declaration) = declaration else {
3211                continue;
3212            };
3213            declarations.insert(declaration.id.clone(), declaration);
3214        }
3215    }
3216    declarations
3217}
3218
3219fn collect_resource_lowering_diagnostics(
3220    resolutions: &[crate::ResourceEndpointResolution],
3221) -> Vec<ComponentDiagnostic> {
3222    resolutions
3223        .iter()
3224        .filter(|resolution| {
3225            !matches!(
3226                resolution.outcome,
3227                crate::ResourceEndpointResolutionOutcome::Resolved(_)
3228            )
3229        })
3230        .map(|resolution| {
3231            let message = match &resolution.outcome {
3232                crate::ResourceEndpointResolutionOutcome::MissingDesignator => format!(
3233                    "resource declaration `{}` requires one imported semantic-package resource endpoint designator",
3234                    resolution.field
3235                ),
3236                crate::ResourceEndpointResolutionOutcome::UnboundDesignator { designator } => format!(
3237                    "resource declaration `{}` designator `{designator}` does not resolve to an imported semantic-package endpoint",
3238                    resolution.field
3239                ),
3240                crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding { designator } => format!(
3241                    "resource declaration `{}` designator `{designator}` must bind a semantic-package resource export",
3242                    resolution.field
3243                ),
3244                crate::ResourceEndpointResolutionOutcome::NonResourceBinding { designator, kind } => format!(
3245                    "resource declaration `{}` designator `{designator}` resolves to package kind {kind:?}, not resource",
3246                    resolution.field
3247                ),
3248                crate::ResourceEndpointResolutionOutcome::Resolved(_) => unreachable!(
3249                    "resolved resource endpoints are valid N6-C13 lowering inputs"
3250                ),
3251            };
3252            let mut diagnostic = ComponentDiagnostic::error("PSC1128", message);
3253            diagnostic.provenance = Some(resolution.provenance.clone());
3254            diagnostic
3255        })
3256        .collect()
3257}
3258
3259fn collect_resource_activations(
3260    declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3261    instances: &ComponentInstancePlan,
3262) -> BTreeMap<ResourceActivationId, crate::ResourceActivation> {
3263    let mut activations = BTreeMap::new();
3264    for instance in instances.instances.values() {
3265        for declaration in declarations
3266            .values()
3267            .filter(|declaration| declaration.owner_component == instance.component)
3268        {
3269            let activation = declaration.activation_for(instance.id.clone());
3270            activations.insert(activation.id.clone(), activation);
3271        }
3272    }
3273    activations
3274}
3275
3276fn resolve_semantic_package_pure_call(
3277    expression: &mut ComputedExpression,
3278    module_path: &Path,
3279    bindings: &crate::BindingTable,
3280) {
3281    match &mut expression.kind {
3282        ComputedExpressionKind::Call { callee, arguments } => {
3283            for argument in arguments.iter_mut() {
3284                resolve_semantic_package_pure_call(argument, module_path, bindings);
3285            }
3286            let Some(binding) = bindings.resolve_import(module_path, callee) else {
3287                let operation = match (callee.as_str(), arguments.len()) {
3288                    ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
3289                    ("Math.floor", 1) => {
3290                        Some(crate::component_graph::BuiltinPureOperation::MathFloor)
3291                    }
3292                    ("Math.ceil", 1) => {
3293                        Some(crate::component_graph::BuiltinPureOperation::MathCeil)
3294                    }
3295                    ("Math.round", 1) => {
3296                        Some(crate::component_graph::BuiltinPureOperation::MathRound)
3297                    }
3298                    ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
3299                    ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
3300                    _ => None,
3301                };
3302                if let Some(operation) = operation {
3303                    expression.kind = ComputedExpressionKind::BuiltinPureCall {
3304                        operation,
3305                        arguments: std::mem::take(arguments),
3306                    };
3307                }
3308                return;
3309            };
3310            let crate::ImportBindingTarget::SemanticPackage {
3311                package,
3312                version,
3313                integrity,
3314                export,
3315                kind: crate::semantic_package::SemanticPackageKind::Pure,
3316                runtime_module,
3317                resume_policy,
3318                pure_operation: Some(operation),
3319                ..
3320            } = &binding.target
3321            else {
3322                return;
3323            };
3324            expression.kind = ComputedExpressionKind::SemanticPackagePureCall {
3325                local_name: callee.clone(),
3326                package: package.clone(),
3327                version: version.clone(),
3328                integrity: integrity.clone(),
3329                export: export.clone(),
3330                runtime_module: runtime_module.clone(),
3331                resume_policy: resume_policy.clone(),
3332                operation: *operation,
3333                arguments: std::mem::take(arguments),
3334            };
3335        }
3336        ComputedExpressionKind::BuiltinPureCall { arguments, .. } => {
3337            for argument in arguments {
3338                resolve_semantic_package_pure_call(argument, module_path, bindings);
3339            }
3340        }
3341        ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
3342            for argument in arguments {
3343                resolve_semantic_package_pure_call(argument, module_path, bindings);
3344            }
3345        }
3346        ComputedExpressionKind::Template { expressions, .. } => {
3347            for expression in expressions {
3348                resolve_semantic_package_pure_call(expression, module_path, bindings);
3349            }
3350        }
3351        ComputedExpressionKind::MemberAccess { object, .. }
3352        | ComputedExpressionKind::Unary {
3353            operand: object, ..
3354        } => {
3355            resolve_semantic_package_pure_call(object, module_path, bindings);
3356        }
3357        ComputedExpressionKind::IndexAccess { object, index } => {
3358            resolve_semantic_package_pure_call(object, module_path, bindings);
3359            resolve_semantic_package_pure_call(index, module_path, bindings);
3360        }
3361        ComputedExpressionKind::Conditional {
3362            condition,
3363            when_true,
3364            when_false,
3365        } => {
3366            resolve_semantic_package_pure_call(condition, module_path, bindings);
3367            resolve_semantic_package_pure_call(when_true, module_path, bindings);
3368            resolve_semantic_package_pure_call(when_false, module_path, bindings);
3369        }
3370        ComputedExpressionKind::Arithmetic { left, right, .. }
3371        | ComputedExpressionKind::Comparison { left, right, .. }
3372        | ComputedExpressionKind::Logical { left, right, .. }
3373        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3374            resolve_semantic_package_pure_call(left, module_path, bindings);
3375            resolve_semantic_package_pure_call(right, module_path, bindings);
3376        }
3377        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
3378    }
3379}
3380
3381fn contains_semantic_package_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3382    match &expression.kind {
3383        ComputedExpressionKind::SemanticPackagePureCall {
3384            local_name,
3385            arguments,
3386            ..
3387        } => {
3388            local_name == callee
3389                || arguments
3390                    .iter()
3391                    .any(|argument| contains_semantic_package_pure_call(argument, callee))
3392        }
3393        ComputedExpressionKind::Call { arguments, .. } => arguments
3394            .iter()
3395            .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3396        ComputedExpressionKind::BuiltinPureCall { arguments, .. } => arguments
3397            .iter()
3398            .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3399        ComputedExpressionKind::Template { expressions, .. } => expressions
3400            .iter()
3401            .any(|expression| contains_semantic_package_pure_call(expression, callee)),
3402        ComputedExpressionKind::MemberAccess { object, .. }
3403        | ComputedExpressionKind::Unary {
3404            operand: object, ..
3405        } => contains_semantic_package_pure_call(object, callee),
3406        ComputedExpressionKind::IndexAccess { object, index } => {
3407            contains_semantic_package_pure_call(object, callee)
3408                || contains_semantic_package_pure_call(index, callee)
3409        }
3410        ComputedExpressionKind::Conditional {
3411            condition,
3412            when_true,
3413            when_false,
3414        } => {
3415            contains_semantic_package_pure_call(condition, callee)
3416                || contains_semantic_package_pure_call(when_true, callee)
3417                || contains_semantic_package_pure_call(when_false, callee)
3418        }
3419        ComputedExpressionKind::Arithmetic { left, right, .. }
3420        | ComputedExpressionKind::Comparison { left, right, .. }
3421        | ComputedExpressionKind::Logical { left, right, .. }
3422        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3423            contains_semantic_package_pure_call(left, callee)
3424                || contains_semantic_package_pure_call(right, callee)
3425        }
3426        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3427    }
3428}
3429
3430fn contains_builtin_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3431    match &expression.kind {
3432        ComputedExpressionKind::BuiltinPureCall {
3433            operation,
3434            arguments,
3435        } => {
3436            let operation_callee = match operation {
3437                crate::component_graph::BuiltinPureOperation::MathAbs => "Math.abs",
3438                crate::component_graph::BuiltinPureOperation::MathFloor => "Math.floor",
3439                crate::component_graph::BuiltinPureOperation::MathCeil => "Math.ceil",
3440                crate::component_graph::BuiltinPureOperation::MathRound => "Math.round",
3441                crate::component_graph::BuiltinPureOperation::MathMin => "Math.min",
3442                crate::component_graph::BuiltinPureOperation::MathMax => "Math.max",
3443            };
3444            operation_callee == callee
3445                || arguments
3446                    .iter()
3447                    .any(|argument| contains_builtin_pure_call(argument, callee))
3448        }
3449        ComputedExpressionKind::Call { arguments, .. }
3450        | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => arguments
3451            .iter()
3452            .any(|argument| contains_builtin_pure_call(argument, callee)),
3453        ComputedExpressionKind::Template { expressions, .. } => expressions
3454            .iter()
3455            .any(|expression| contains_builtin_pure_call(expression, callee)),
3456        ComputedExpressionKind::MemberAccess { object, .. }
3457        | ComputedExpressionKind::Unary {
3458            operand: object, ..
3459        } => contains_builtin_pure_call(object, callee),
3460        ComputedExpressionKind::IndexAccess { object, index } => {
3461            contains_builtin_pure_call(object, callee) || contains_builtin_pure_call(index, callee)
3462        }
3463        ComputedExpressionKind::Conditional {
3464            condition,
3465            when_true,
3466            when_false,
3467        } => {
3468            contains_builtin_pure_call(condition, callee)
3469                || contains_builtin_pure_call(when_true, callee)
3470                || contains_builtin_pure_call(when_false, callee)
3471        }
3472        ComputedExpressionKind::Arithmetic { left, right, .. }
3473        | ComputedExpressionKind::Comparison { left, right, .. }
3474        | ComputedExpressionKind::Logical { left, right, .. }
3475        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3476            contains_builtin_pure_call(left, callee) || contains_builtin_pure_call(right, callee)
3477        }
3478        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3479    }
3480}
3481
3482fn collect_computed_cycle_diagnostics(
3483    analysis: &IrReactiveCycleAnalysis,
3484    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3485) -> Vec<ComponentDiagnostic> {
3486    analysis
3487        .cycles
3488        .iter()
3489        .filter_map(|cycle| {
3490            let first = cycle.nodes.first()?;
3491            let computed = computed_values
3492                .values()
3493                .find(|computed| computed.id.as_str() == first)?;
3494            Some(ComponentDiagnostic {
3495                severity: ComponentDiagnosticSeverity::Error,
3496                effect_id: None,
3497                statement_id: None,
3498                context_declaration_candidate_id: None,
3499                context_id: None,
3500                provider_id: None,
3501                consumer_id: None,
3502                slot_id: None,
3503                invocation_id: None,
3504                component_instance_id: None,
3505                slot_binding_id: None,
3506                structural_region_id: None,
3507                component_id: None,
3508                provider_instance_id: None,
3509                consumer_instance_id: None,
3510                secondary_labels: Vec::new(),
3511                code: ComputedDiagnosticCode::DependencyCycle.as_str().to_string(),
3512                message: format!(
3513                    "computed dependency cycle detected among: {}",
3514                    cycle.nodes.join(", ")
3515                ),
3516                provenance: Some(computed.provenance.clone()),
3517            })
3518        })
3519        .collect()
3520}
3521
3522fn collect_computed_semantic_diagnostics(
3523    components: &[ComponentNode],
3524    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3525    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3526    expression_graph: &ExpressionGraph,
3527    semantic_types: &SemanticTypeModel,
3528    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3529) -> Vec<ComponentDiagnostic> {
3530    let mut diagnostics = collect_invalid_computed_declaration_diagnostics(components, provenance);
3531    diagnostics.extend(collect_computed_body_and_read_diagnostics(
3532        components,
3533        computed_values,
3534        resource_declarations,
3535        expression_graph,
3536    ));
3537    diagnostics.extend(collect_computed_type_diagnostics(
3538        computed_values,
3539        semantic_types,
3540    ));
3541    diagnostics.extend(collect_computed_conditional_diagnostics(
3542        components,
3543        expression_graph,
3544        semantic_types,
3545    ));
3546    sort_computed_diagnostics(&mut diagnostics);
3547    diagnostics
3548}
3549
3550fn collect_computed_conditional_diagnostics(
3551    components: &[ComponentNode],
3552    expression_graph: &ExpressionGraph,
3553    semantic_types: &SemanticTypeModel,
3554) -> Vec<ComponentDiagnostic> {
3555    expression_graph
3556        .nodes
3557        .values()
3558        .filter_map(|node| {
3559            let crate::ExpressionNodeKind::Conditional { condition, .. } = &node.kind else {
3560                return None;
3561            };
3562            let condition_type = semantic_types
3563                .assignments
3564                .get(condition)
3565                .map(|assignment| assignment.semantic_type.clone())
3566                .or_else(|| match &expression_graph.node(condition)?.kind {
3567                    crate::ExpressionNodeKind::ThisMember { name } => components
3568                        .iter()
3569                        .find(|component| {
3570                            component
3571                                .methods
3572                                .iter()
3573                                .any(|method| component.id.computed(&method.name) == node.owner)
3574                        })
3575                        .and_then(|component| {
3576                            component
3577                                .state_fields
3578                                .iter()
3579                                .find(|field| field.name == *name)
3580                        })
3581                        .and_then(|field| semantic_types.assignments.get(&field.id))
3582                        .map(|assignment| assignment.semantic_type.clone()),
3583                    _ => None,
3584                })
3585                .unwrap_or(crate::SemanticType::Unknown);
3586            if is_boolean_computed_condition(&condition_type) {
3587                None
3588            } else {
3589                Some(ComponentDiagnostic {
3590                    severity: ComponentDiagnosticSeverity::Error,
3591                    effect_id: None,
3592                    statement_id: None,
3593                    context_declaration_candidate_id: None,
3594                    context_id: None,
3595                    provider_id: None,
3596                    consumer_id: None,
3597                    slot_id: None,
3598                    invocation_id: None,
3599                    component_instance_id: None,
3600                    slot_binding_id: None,
3601                    structural_region_id: None,
3602                    component_id: None,
3603                    provider_instance_id: None,
3604                    consumer_instance_id: None,
3605                    secondary_labels: Vec::new(),
3606                    code: crate::TypeDiagnosticCode::InvalidCondition
3607                        .as_str()
3608                        .to_string(),
3609                    message: format!(
3610                        "computed conditional requires boolean, but has {}",
3611                        crate::semantic_type_text(&condition_type)
3612                    ),
3613                    provenance: Some(node.provenance.clone()),
3614                })
3615            }
3616        })
3617        .collect()
3618}
3619
3620fn is_boolean_computed_condition(semantic_type: &crate::SemanticType) -> bool {
3621    match semantic_type {
3622        crate::SemanticType::Boolean | crate::SemanticType::BooleanLiteral(_) => true,
3623        crate::SemanticType::Union(members) => members.iter().all(is_boolean_computed_condition),
3624        _ => false,
3625    }
3626}
3627
3628fn collect_invalid_computed_declaration_diagnostics(
3629    components: &[ComponentNode],
3630    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3631) -> Vec<ComponentDiagnostic> {
3632    components
3633        .iter()
3634        .flat_map(|component| &component.methods)
3635        .filter(|method| {
3636            method
3637                .decorators
3638                .iter()
3639                .any(|decorator| decorator == "computed")
3640                && !method.is_getter
3641        })
3642        .map(|method| ComponentDiagnostic {
3643            severity: ComponentDiagnosticSeverity::Error,
3644            effect_id: None,
3645            statement_id: None,
3646            context_declaration_candidate_id: None,
3647            context_id: None,
3648            provider_id: None,
3649            consumer_id: None,
3650            slot_id: None,
3651            invocation_id: None,
3652            component_instance_id: None,
3653            slot_binding_id: None,
3654            structural_region_id: None,
3655            component_id: None,
3656            provider_instance_id: None,
3657            consumer_instance_id: None,
3658            secondary_labels: Vec::new(),
3659            code: ComputedDiagnosticCode::InvalidDeclaration
3660                .as_str()
3661                .to_string(),
3662            message: format!(
3663                "@computed() declaration `{}` must decorate a getter",
3664                method.name
3665            ),
3666            provenance: Some(
3667                provenance
3668                    .get(&method.id)
3669                    .expect("component methods should have canonical provenance")
3670                    .clone(),
3671            ),
3672        })
3673        .collect()
3674}
3675
3676fn collect_computed_body_and_read_diagnostics(
3677    components: &[ComponentNode],
3678    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3679    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3680    expression_graph: &ExpressionGraph,
3681) -> Vec<ComponentDiagnostic> {
3682    let mut diagnostics = Vec::new();
3683
3684    for computed in computed_values.values() {
3685        let component_id = computed
3686            .owner
3687            .entity_id()
3688            .expect("computed values should have component owners");
3689        let component = components
3690            .iter()
3691            .find(|component| component.id == *component_id)
3692            .expect("computed values should have owning components");
3693        let method = component
3694            .methods
3695            .iter()
3696            .find(|method| method.id == computed.method)
3697            .expect("computed values should have authored methods");
3698
3699        if method.computed_expression.is_none() {
3700            diagnostics.push(ComponentDiagnostic {
3701                severity: ComponentDiagnosticSeverity::Error,
3702                effect_id: None,
3703                statement_id: None,
3704                context_declaration_candidate_id: None,
3705                context_id: None,
3706                provider_id: None,
3707                consumer_id: None,
3708                slot_id: None,
3709                invocation_id: None,
3710                component_instance_id: None,
3711                slot_binding_id: None,
3712                structural_region_id: None,
3713                component_id: None,
3714                provider_instance_id: None,
3715                consumer_instance_id: None,
3716                secondary_labels: Vec::new(),
3717                code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3718                message: format!(
3719                    "computed getter `{}` has an unsupported body",
3720                    computed.name
3721                ),
3722                provenance: Some(computed.provenance.clone()),
3723            });
3724        }
3725
3726        diagnostics.extend(
3727            expression_graph
3728                .nodes_for(&computed.id)
3729                .into_iter()
3730                .filter_map(|node| {
3731                    unresolved_computed_read_diagnostic(
3732                        component,
3733                        computed_values,
3734                        resource_declarations,
3735                        computed,
3736                        node,
3737                    )
3738                }),
3739        );
3740        diagnostics.extend(computed_resource_projection_diagnostics(
3741            component,
3742            computed,
3743            resource_declarations,
3744            expression_graph,
3745        ));
3746    }
3747
3748    diagnostics
3749}
3750
3751fn unresolved_computed_read_diagnostic(
3752    component: &ComponentNode,
3753    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3754    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3755    computed: &ComputedValue,
3756    node: &ExpressionNode,
3757) -> Option<ComponentDiagnostic> {
3758    let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3759        return None;
3760    };
3761    let resolved = component
3762        .state_fields
3763        .iter()
3764        .any(|field| field.name == *name)
3765        || computed_values.contains_key(&component.id.computed(name))
3766        || resource_declarations.contains_key(&ResourceId::for_owner(&component.id, name));
3767    (!resolved).then(|| ComponentDiagnostic {
3768        severity: ComponentDiagnosticSeverity::Error,
3769        effect_id: None,
3770        statement_id: None,
3771        context_declaration_candidate_id: None,
3772        context_id: None,
3773        provider_id: None,
3774        consumer_id: None,
3775        slot_id: None,
3776        invocation_id: None,
3777        component_instance_id: None,
3778        slot_binding_id: None,
3779        structural_region_id: None,
3780        component_id: None,
3781        provider_instance_id: None,
3782        consumer_instance_id: None,
3783        secondary_labels: Vec::new(),
3784        code: ComputedDiagnosticCode::UnresolvedRead.as_str().to_string(),
3785        message: format!(
3786            "computed getter `{}` reads unresolved member `this.{name}`",
3787            computed.name
3788        ),
3789        provenance: Some(node.provenance.clone()),
3790    })
3791}
3792
3793fn computed_resource_projection_diagnostics(
3794    component: &ComponentNode,
3795    computed: &ComputedValue,
3796    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3797    expression_graph: &ExpressionGraph,
3798) -> Vec<ComponentDiagnostic> {
3799    let nodes = expression_graph.nodes_for(&computed.id);
3800    nodes
3801        .iter()
3802        .filter_map(|node| {
3803            let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3804                return None;
3805            };
3806            resource_declarations
3807                .contains_key(&ResourceId::for_owner(&component.id, name))
3808                .then_some((node, name))
3809        })
3810        .filter(|(node, _)| {
3811            !nodes.iter().any(|candidate| {
3812                is_direct_resource_projection(candidate, &nodes)
3813                    && matches!(
3814                        &candidate.kind,
3815                        ExpressionNodeKind::MemberAccess { object, .. } if object == &node.id
3816                    )
3817            })
3818        })
3819        .map(|(node, name)| ComponentDiagnostic {
3820            severity: ComponentDiagnosticSeverity::Error,
3821            effect_id: None,
3822            statement_id: None,
3823            context_declaration_candidate_id: None,
3824            context_id: None,
3825            provider_id: None,
3826            consumer_id: None,
3827            slot_id: None,
3828            invocation_id: None,
3829            component_instance_id: None,
3830            slot_binding_id: None,
3831            structural_region_id: None,
3832            component_id: None,
3833            provider_instance_id: None,
3834            consumer_instance_id: None,
3835            secondary_labels: Vec::new(),
3836            code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3837            message: format!(
3838                "computed getter `{}` may only directly project `this.{name}.data`, `this.{name}.error`, or `this.{name}.state`",
3839                computed.name
3840            ),
3841            provenance: Some(node.provenance.clone()),
3842        })
3843        .collect()
3844}
3845
3846fn is_direct_resource_projection(node: &ExpressionNode, nodes: &[&ExpressionNode]) -> bool {
3847    let ExpressionNodeKind::MemberAccess {
3848        property,
3849        optional: false,
3850        ..
3851    } = &node.kind
3852    else {
3853        return false;
3854    };
3855    matches!(property.as_str(), "data" | "error" | "state")
3856        && !nodes.iter().any(|candidate| {
3857            matches!(
3858                &candidate.kind,
3859                ExpressionNodeKind::MemberAccess { object, .. }
3860                    | ExpressionNodeKind::IndexAccess { object, .. }
3861                    if object == &node.id
3862            )
3863        })
3864}
3865
3866fn collect_computed_type_diagnostics(
3867    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3868    semantic_types: &SemanticTypeModel,
3869) -> Vec<ComponentDiagnostic> {
3870    let mut diagnostics = Vec::new();
3871
3872    for computed_type in semantic_types.computed_values.values() {
3873        let computed = computed_values
3874            .get(&computed_type.computed)
3875            .expect("computed type records should have computed entities");
3876        if computed_type.declared_return_compatible == Some(false) {
3877            let declared = computed_type
3878                .declared_return_type
3879                .as_ref()
3880                .expect("incompatible computed return types should be declared");
3881            diagnostics.push(ComponentDiagnostic {
3882                severity: ComponentDiagnosticSeverity::Error,
3883                effect_id: None,
3884                statement_id: None,
3885                context_declaration_candidate_id: None,
3886                context_id: None,
3887                provider_id: None,
3888                consumer_id: None,
3889                slot_id: None,
3890                invocation_id: None,
3891                component_instance_id: None,
3892                slot_binding_id: None,
3893                structural_region_id: None,
3894                component_id: None,
3895                provider_instance_id: None,
3896                consumer_instance_id: None,
3897                secondary_labels: Vec::new(),
3898                code: ComputedDiagnosticCode::TypeMismatch.as_str().to_string(),
3899                message: format!(
3900                    "computed getter `{}` returns `{}` but declares `{}`",
3901                    computed.name,
3902                    crate::semantic_type_text(&computed_type.semantic_type),
3903                    crate::semantic_type_text(declared)
3904                ),
3905                provenance: Some(computed_type.provenance.clone()),
3906            });
3907        }
3908        if computed_type.serialization == crate::SerializationCompatibility::NotSerializable {
3909            diagnostics.push(ComponentDiagnostic {
3910                severity: ComponentDiagnosticSeverity::Error,
3911                effect_id: None,
3912                statement_id: None,
3913                context_declaration_candidate_id: None,
3914                context_id: None,
3915                provider_id: None,
3916                consumer_id: None,
3917                slot_id: None,
3918                invocation_id: None,
3919                component_instance_id: None,
3920                slot_binding_id: None,
3921                structural_region_id: None,
3922                component_id: None,
3923                provider_instance_id: None,
3924                consumer_instance_id: None,
3925                secondary_labels: Vec::new(),
3926                code: ComputedDiagnosticCode::SerializationViolation
3927                    .as_str()
3928                    .to_string(),
3929                message: format!(
3930                    "computed getter `{}` has a non-serializable result",
3931                    computed.name
3932                ),
3933                provenance: Some(computed_type.provenance.clone()),
3934            });
3935        }
3936    }
3937
3938    diagnostics
3939}
3940
3941fn sort_computed_diagnostics(diagnostics: &mut [ComponentDiagnostic]) {
3942    diagnostics.sort_by(|left, right| {
3943        (
3944            left.code.as_str(),
3945            left.provenance.as_ref().map(|provenance| &provenance.path),
3946            left.provenance
3947                .as_ref()
3948                .map(|provenance| provenance.span.start),
3949            left.provenance
3950                .as_ref()
3951                .map(|provenance| provenance.span.end),
3952            left.message.as_str(),
3953        )
3954            .cmp(&(
3955                right.code.as_str(),
3956                right.provenance.as_ref().map(|provenance| &provenance.path),
3957                right
3958                    .provenance
3959                    .as_ref()
3960                    .map(|provenance| provenance.span.start),
3961                right
3962                    .provenance
3963                    .as_ref()
3964                    .map(|provenance| provenance.span.end),
3965                right.message.as_str(),
3966            ))
3967    });
3968}
3969
3970fn build_computed_reactive_products(
3971    components: &[ComponentNode],
3972    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3973    effects: &BTreeMap<SemanticId, Effect>,
3974    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3975    references: &[SemanticReference],
3976    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3977) -> (
3978    IrReactiveGraph,
3979    IrReactiveTransitiveAnalysis,
3980    IrReactiveCycleAnalysis,
3981    IrComputedEvaluationPlan,
3982) {
3983    let graph = crate::build_reactive_graph(
3984        components,
3985        computed_values,
3986        effects,
3987        resource_declarations,
3988        references,
3989        provenance,
3990    );
3991    let transitive_analysis = crate::analyze_reactive_transitive_graph(&graph);
3992    let cycle_analysis = crate::analyze_reactive_cycles(&graph);
3993    let evaluation_plan = crate::plan_computed_evaluation(&graph);
3994    (graph, transitive_analysis, cycle_analysis, evaluation_plan)
3995}
3996
3997fn extend_template_references(
3998    references: &mut Vec<SemanticReference>,
3999    components: &[ComponentNode],
4000    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4001    template_entities: &[TemplateSemanticEntity],
4002    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4003) {
4004    references.extend(build_template_state_references(
4005        components,
4006        template_entities,
4007        ownership,
4008    ));
4009    references.extend(build_template_computed_references(
4010        components,
4011        computed_values,
4012        template_entities,
4013        ownership,
4014    ));
4015    references.extend(build_template_event_references(
4016        components,
4017        template_entities,
4018        ownership,
4019    ));
4020    references.extend(build_template_local_references(
4021        components,
4022        template_entities,
4023        ownership,
4024    ));
4025}
4026
4027fn build_form_field_binding_references(
4028    bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4029) -> Vec<SemanticReference> {
4030    bindings
4031        .values()
4032        .flat_map(|binding| {
4033            [
4034                SemanticReference {
4035                    kind: SemanticReferenceKind::FieldBindingField,
4036                    source: binding.id.as_semantic_id().clone(),
4037                    target: binding.field.as_semantic_id().clone(),
4038                    provenance: binding.expression_provenance.clone(),
4039                },
4040                SemanticReference {
4041                    kind: SemanticReferenceKind::FieldBindingForm,
4042                    source: binding.id.as_semantic_id().clone(),
4043                    target: binding.form.as_semantic_id().clone(),
4044                    provenance: binding.expression_provenance.clone(),
4045                },
4046            ]
4047        })
4048        .collect()
4049}
4050
4051fn computed_call_purity_kind(
4052    component: &ComponentNode,
4053    callee: &str,
4054) -> crate::ComputedPurityViolationKind {
4055    if matches!(callee, "resource" | "this.resource") {
4056        return crate::ComputedPurityViolationKind::Resource;
4057    }
4058    if matches!(
4059        callee,
4060        "Math.random"
4061            | "Date.now"
4062            | "performance.now"
4063            | "crypto.randomUUID"
4064            | "crypto.getRandomValues"
4065    ) {
4066        return crate::ComputedPurityViolationKind::NondeterministicOperation;
4067    }
4068    if callee.starts_with("console.")
4069        || matches!(
4070            callee,
4071            "fetch" | "setTimeout" | "setInterval" | "queueMicrotask"
4072        )
4073    {
4074        return crate::ComputedPurityViolationKind::Effect;
4075    }
4076    if let Some(name) = callee.strip_prefix("this.") {
4077        if let Some(method) = component.methods.iter().find(|method| method.name == name) {
4078            if method.is_action()
4079                || method
4080                    .decorators
4081                    .iter()
4082                    .any(|decorator| decorator == "action")
4083            {
4084                return crate::ComputedPurityViolationKind::Action;
4085            }
4086            if method
4087                .decorators
4088                .iter()
4089                .any(|decorator| decorator == "effect")
4090            {
4091                return crate::ComputedPurityViolationKind::Effect;
4092            }
4093            if method
4094                .decorators
4095                .iter()
4096                .any(|decorator| decorator == "resource")
4097            {
4098                return crate::ComputedPurityViolationKind::Resource;
4099            }
4100        }
4101    }
4102    crate::ComputedPurityViolationKind::ArbitraryMethodCall
4103}
4104
4105fn build_computed_references(
4106    components: &[ComponentNode],
4107    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4108    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4109    expression_graph: &ExpressionGraph,
4110) -> Vec<SemanticReference> {
4111    let mut references = BTreeMap::new();
4112
4113    for computed in computed_values.values() {
4114        let Some(component_id) = computed.owner.entity_id() else {
4115            continue;
4116        };
4117        let Some(component) = components
4118            .iter()
4119            .find(|component| component.id == *component_id)
4120        else {
4121            continue;
4122        };
4123
4124        let nodes = expression_graph.nodes_for(&computed.id);
4125        let direct_resource_projection_objects = nodes
4126            .iter()
4127            .filter(|node| is_direct_resource_projection(node, &nodes))
4128            .filter_map(|node| match &node.kind {
4129                crate::ExpressionNodeKind::MemberAccess { object, .. } => Some(object.clone()),
4130                _ => None,
4131            })
4132            .collect::<BTreeSet<_>>();
4133
4134        for node in nodes {
4135            let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4136                continue;
4137            };
4138            let reference = if let Some(field) = component
4139                .state_fields
4140                .iter()
4141                .find(|field| field.name == *name)
4142            {
4143                SemanticReference {
4144                    kind: SemanticReferenceKind::ComputedState,
4145                    source: computed.id.clone(),
4146                    target: field.id.clone(),
4147                    provenance: computed.provenance.clone(),
4148                }
4149            } else if let Some(target) = computed_values.get(&component.id.computed(name)) {
4150                SemanticReference {
4151                    kind: SemanticReferenceKind::ComputedComputed,
4152                    source: computed.id.clone(),
4153                    target: target.id.clone(),
4154                    provenance: computed.provenance.clone(),
4155                }
4156            } else if direct_resource_projection_objects.contains(&node.id) {
4157                let target = ResourceId::for_owner(&component.id, name);
4158                let Some(declaration) = resource_declarations.get(&target) else {
4159                    continue;
4160                };
4161                SemanticReference {
4162                    kind: SemanticReferenceKind::ComputedResource,
4163                    source: computed.id.clone(),
4164                    target: declaration.id.as_semantic_id().clone(),
4165                    provenance: node.provenance.clone(),
4166                }
4167            } else {
4168                continue;
4169            };
4170            references.insert(
4171                (reference.source.clone(), reference.target.clone()),
4172                reference,
4173            );
4174        }
4175    }
4176
4177    references.into_values().collect()
4178}
4179
4180fn build_effect_references(
4181    components: &[ComponentNode],
4182    effects: &BTreeMap<SemanticId, Effect>,
4183    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4184    expression_graph: &ExpressionGraph,
4185) -> Vec<SemanticReference> {
4186    let mut references = Vec::new();
4187    for effect in effects.values() {
4188        let Some(component_id) = effect.owner.entity_id() else {
4189            continue;
4190        };
4191        let Some(component) = components
4192            .iter()
4193            .find(|component| component.id == *component_id)
4194        else {
4195            continue;
4196        };
4197        for node in expression_graph.nodes_for(&effect.id) {
4198            let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4199                continue;
4200            };
4201            let reference = if let Some(field) = component
4202                .state_fields
4203                .iter()
4204                .find(|field| field.name == *name)
4205            {
4206                SemanticReference {
4207                    kind: SemanticReferenceKind::EffectState,
4208                    source: effect.id.clone(),
4209                    target: field.id.clone(),
4210                    provenance: node.provenance.clone(),
4211                }
4212            } else if let Some(computed) = computed_values.get(&component.id.computed(name)) {
4213                SemanticReference {
4214                    kind: SemanticReferenceKind::EffectComputed,
4215                    source: effect.id.clone(),
4216                    target: computed.id.clone(),
4217                    provenance: node.provenance.clone(),
4218                }
4219            } else {
4220                continue;
4221            };
4222            references.push(reference);
4223        }
4224    }
4225    references.sort_by(|left, right| {
4226        (
4227            left.source.as_str(),
4228            left.target.as_str(),
4229            left.provenance.span.start,
4230        )
4231            .cmp(&(
4232                right.source.as_str(),
4233                right.target.as_str(),
4234                right.provenance.span.start,
4235            ))
4236    });
4237    references.dedup_by(|left, right| {
4238        left.kind == right.kind && left.source == right.source && left.target == right.target
4239    });
4240    references
4241}
4242
4243fn build_provider_references(
4244    providers: &BTreeMap<ProviderId, ProviderEntity>,
4245) -> Vec<SemanticReference> {
4246    providers
4247        .values()
4248        .map(|provider| SemanticReference {
4249            kind: SemanticReferenceKind::ProvidesContext,
4250            source: provider.id.as_semantic_id().clone(),
4251            target: provider.context.as_semantic_id().clone(),
4252            provenance: provider.provenance.clone(),
4253        })
4254        .collect()
4255}
4256
4257fn build_consumer_references(
4258    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4259) -> Vec<SemanticReference> {
4260    consumers
4261        .values()
4262        .filter_map(|consumer| {
4263            let ContextResolutionState::Resolved(context) = &consumer.context_resolution else {
4264                return None;
4265            };
4266            Some(SemanticReference {
4267                kind: SemanticReferenceKind::ConsumesContext,
4268                source: consumer.id.as_semantic_id().clone(),
4269                target: context.as_semantic_id().clone(),
4270                provenance: consumer.provenance.clone(),
4271            })
4272        })
4273        .collect()
4274}
4275
4276fn build_context_resolution_references(
4277    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
4278) -> Vec<SemanticReference> {
4279    resolutions
4280        .values()
4281        .filter_map(|resolution| {
4282            let ContextResolutionResult::Provider { provider, .. } = &resolution.result else {
4283                return None;
4284            };
4285            Some(SemanticReference {
4286                kind: SemanticReferenceKind::ResolvesToProvider,
4287                source: resolution.consumer.as_semantic_id().clone(),
4288                target: provider.as_semantic_id().clone(),
4289                provenance: resolution.provenance.clone(),
4290            })
4291        })
4292        .collect()
4293}
4294
4295fn build_template_state_references(
4296    components: &[ComponentNode],
4297    template_entities: &[TemplateSemanticEntity],
4298    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4299) -> Vec<SemanticReference> {
4300    template_entities
4301        .iter()
4302        .filter(|entity| {
4303            matches!(
4304                entity.kind,
4305                TemplateSemanticKind::Binding
4306                    | TemplateSemanticKind::AttributeBinding
4307                    | TemplateSemanticKind::Conditional
4308                    | TemplateSemanticKind::List
4309            )
4310        })
4311        .filter_map(|entity| {
4312            let field_name = entity.expression.as_deref().and_then(this_member_name)?;
4313            let component = template_entity_component(components, ownership, entity)?;
4314            let field = component
4315                .state_fields
4316                .iter()
4317                .find(|field| field.name == field_name)?;
4318
4319            Some(SemanticReference {
4320                kind: SemanticReferenceKind::TemplateState,
4321                source: entity.id.clone(),
4322                target: field.id.clone(),
4323                provenance: entity.provenance.clone(),
4324            })
4325        })
4326        .collect()
4327}
4328
4329fn build_template_computed_references(
4330    components: &[ComponentNode],
4331    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4332    template_entities: &[TemplateSemanticEntity],
4333    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4334) -> Vec<SemanticReference> {
4335    template_entities
4336        .iter()
4337        .filter(|entity| {
4338            matches!(
4339                entity.kind,
4340                TemplateSemanticKind::Binding
4341                    | TemplateSemanticKind::AttributeBinding
4342                    | TemplateSemanticKind::Conditional
4343                    | TemplateSemanticKind::List
4344            )
4345        })
4346        .filter_map(|entity| {
4347            let computed_name = entity.expression.as_deref().and_then(this_member_name)?;
4348            let component = template_entity_component(components, ownership, entity)?;
4349            if component
4350                .state_fields
4351                .iter()
4352                .any(|field| field.name == computed_name)
4353            {
4354                return None;
4355            }
4356            let computed = computed_values.get(&component.id.computed(computed_name))?;
4357
4358            Some(SemanticReference {
4359                kind: SemanticReferenceKind::TemplateComputed,
4360                source: entity.id.clone(),
4361                target: computed.id.clone(),
4362                provenance: entity.provenance.clone(),
4363            })
4364        })
4365        .collect()
4366}
4367
4368fn build_template_event_references(
4369    components: &[ComponentNode],
4370    template_entities: &[TemplateSemanticEntity],
4371    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4372) -> Vec<SemanticReference> {
4373    template_entities
4374        .iter()
4375        .filter(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
4376        .filter_map(|entity| {
4377            let method_name = entity.expression.as_deref().and_then(this_member_name)?;
4378            let component = template_entity_component(components, ownership, entity)?;
4379            let method = component
4380                .methods
4381                .iter()
4382                .find(|method| method.name == method_name)?;
4383
4384            Some(SemanticReference {
4385                kind: SemanticReferenceKind::EventMethod,
4386                source: entity.id.clone(),
4387                target: method.id.clone(),
4388                provenance: entity.provenance.clone(),
4389            })
4390        })
4391        .collect()
4392}
4393
4394fn build_template_local_references(
4395    components: &[ComponentNode],
4396    template_entities: &[TemplateSemanticEntity],
4397    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4398) -> Vec<SemanticReference> {
4399    let mut references = template_entities
4400        .iter()
4401        .filter(|entity| {
4402            matches!(
4403                entity.kind,
4404                TemplateSemanticKind::Binding | TemplateSemanticKind::AttributeBinding
4405            ) && entity.scope == TemplateSemanticScope::Render
4406        })
4407        .filter_map(|entity| {
4408            let name = entity.expression.as_deref()?;
4409            let component = template_entity_component(components, ownership, entity)?;
4410            let render = component
4411                .methods
4412                .iter()
4413                .find(|method| method.name == "render")?;
4414            let local = unique_local_variable(render, name)?;
4415
4416            Some(SemanticReference {
4417                kind: SemanticReferenceKind::TemplateLocal,
4418                source: entity.id.clone(),
4419                target: local.id.clone(),
4420                provenance: entity.provenance.clone(),
4421            })
4422        })
4423        .collect::<Vec<_>>();
4424    references.sort_by(|left, right| {
4425        (left.source.as_str(), left.target.as_str())
4426            .cmp(&(right.source.as_str(), right.target.as_str()))
4427    });
4428    references
4429}
4430
4431fn unique_local_variable<'a>(
4432    method: &'a ComponentMethod,
4433    name: &str,
4434) -> Option<&'a MethodLocalVariable> {
4435    let mut locals = method
4436        .local_variables
4437        .iter()
4438        .filter(|local| local.name == name);
4439    let local = locals.next()?;
4440    locals.next().is_none().then_some(local)
4441}
4442
4443fn template_entity_component<'a>(
4444    components: &'a [ComponentNode],
4445    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4446    entity: &TemplateSemanticEntity,
4447) -> Option<&'a ComponentNode> {
4448    let template_id = ownership.get(&entity.id)?.entity_id()?;
4449    let component_id = ownership.get(template_id)?.entity_id()?;
4450
4451    components
4452        .iter()
4453        .find(|component| component.id == *component_id)
4454}
4455
4456fn this_member_name(expression: &str) -> Option<&str> {
4457    expression.strip_prefix("this.").filter(|name| {
4458        !name.is_empty()
4459            && name
4460                .bytes()
4461                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
4462    })
4463}
4464
4465#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
4466fn collect_ownership(
4467    components: &[ComponentNode],
4468    contexts: &BTreeMap<ContextId, ContextEntity>,
4469    forms: &BTreeMap<FormId, FormEntity>,
4470    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
4471    form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4472    providers: &BTreeMap<ProviderId, ProviderEntity>,
4473    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4474    slots: &BTreeMap<SlotId, SlotEntity>,
4475    component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
4476    slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
4477    slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
4478    component_instance_plan: &ComponentInstancePlan,
4479    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4480    effects: &BTreeMap<SemanticId, Effect>,
4481    templates: &[TemplateNode],
4482    template_entities: &[TemplateSemanticEntity],
4483) -> BTreeMap<SemanticId, SemanticOwner> {
4484    let mut ownership = BTreeMap::new();
4485
4486    for component in components {
4487        ownership.insert(component.id.clone(), SemanticOwner::Application);
4488
4489        for field in &component.state_fields {
4490            ownership.insert(
4491                field.id.clone(),
4492                SemanticOwner::entity(component.id.clone()),
4493            );
4494        }
4495        for method in &component.methods {
4496            ownership.insert(
4497                method.id.clone(),
4498                SemanticOwner::entity(component.id.clone()),
4499            );
4500            for parameter in &method.parameters {
4501                ownership.insert(
4502                    parameter.id.clone(),
4503                    SemanticOwner::entity(method.id.clone()),
4504                );
4505            }
4506            for local in &method.local_variables {
4507                ownership.insert(local.id.clone(), SemanticOwner::entity(method.id.clone()));
4508            }
4509        }
4510        for computed in computed_values
4511            .values()
4512            .filter(|computed| computed.owner.entity_id() == Some(&component.id))
4513        {
4514            ownership.insert(computed.id.clone(), computed.owner.clone());
4515        }
4516        for context in contexts
4517            .values()
4518            .filter(|context| context.owner.entity_id() == Some(&component.id))
4519        {
4520            ownership.insert(context.id.as_semantic_id().clone(), context.owner.clone());
4521        }
4522        for form in forms
4523            .values()
4524            .filter(|form| form.owner.entity_id() == Some(&component.id))
4525        {
4526            ownership.insert(form.id.as_semantic_id().clone(), form.owner.clone());
4527        }
4528        for field in form_fields
4529            .values()
4530            .filter(|field| field.owner_component == component.id)
4531        {
4532            ownership.insert(
4533                field.id.as_semantic_id().clone(),
4534                SemanticOwner::entity(field.owner_form.as_semantic_id().clone()),
4535            );
4536        }
4537        for provider in providers
4538            .values()
4539            .filter(|provider| provider.owner.entity_id() == Some(&component.id))
4540        {
4541            ownership.insert(provider.id.as_semantic_id().clone(), provider.owner.clone());
4542        }
4543        for consumer in consumers
4544            .values()
4545            .filter(|consumer| consumer.owner.entity_id() == Some(&component.id))
4546        {
4547            ownership.insert(consumer.id.as_semantic_id().clone(), consumer.owner.clone());
4548        }
4549        for slot in slots.values().filter(|slot| slot.owner == component.id) {
4550            ownership.insert(slot.id.as_semantic_id().clone(), slot.semantic_owner());
4551        }
4552        for invocation in component_invocations
4553            .values()
4554            .filter(|invocation| invocation.owner_component == component.id)
4555        {
4556            ownership.insert(
4557                invocation.id.as_semantic_id().clone(),
4558                SemanticOwner::entity(component.id.clone()),
4559            );
4560        }
4561        for fragment in slot_content_fragments
4562            .values()
4563            .filter(|fragment| fragment.owner_component == component.id)
4564        {
4565            ownership.insert(
4566                fragment.id.as_semantic_id().clone(),
4567                SemanticOwner::entity(component.id.clone()),
4568            );
4569        }
4570        for outlet in slot_outlets
4571            .values()
4572            .filter(|outlet| outlet.owner_component == component.id)
4573        {
4574            ownership.insert(
4575                outlet.id.as_semantic_id().clone(),
4576                SemanticOwner::entity(component.id.clone()),
4577            );
4578        }
4579        for effect in effects
4580            .values()
4581            .filter(|effect| effect.owner.entity_id() == Some(&component.id))
4582        {
4583            ownership.insert(effect.id.clone(), effect.owner.clone());
4584        }
4585        for endpoint in &component.action_endpoints {
4586            ownership.insert(endpoint.id.clone(), endpoint.owner.clone());
4587        }
4588        for action in &component.actions {
4589            // Legacy method actions derive their owner from the method graph,
4590            // while decorator-free action fields retain their canonical endpoint owner.
4591            let owner = component
4592                .methods
4593                .iter()
4594                .find(|method| method.name == action.method)
4595                .map(|method| SemanticOwner::entity(method.id.clone()))
4596                .unwrap_or_else(|| action.owner.clone());
4597            ownership.insert(action.id.clone(), owner);
4598        }
4599        if let Some(render) = &component.render {
4600            for handler in render_event_handlers(render) {
4601                ownership.insert(
4602                    handler.id.clone(),
4603                    SemanticOwner::entity(component.id.template()),
4604                );
4605            }
4606        }
4607    }
4608
4609    for instance in component_instance_plan.instances.values() {
4610        ownership.insert(
4611            instance.id.as_semantic_id().clone(),
4612            instance
4613                .parent_instance
4614                .as_ref()
4615                .map_or(SemanticOwner::Application, |parent| {
4616                    SemanticOwner::entity(parent.as_semantic_id().clone())
4617                }),
4618        );
4619    }
4620    for blocked in component_instance_plan.blocked.values() {
4621        ownership.insert(
4622            blocked.id.as_semantic_id().clone(),
4623            SemanticOwner::entity(blocked.parent_instance.as_semantic_id().clone()),
4624        );
4625    }
4626
4627    for template in templates {
4628        let component = components
4629            .iter()
4630            .find(|component| component.id.template() == template.id)
4631            .expect("template graph should only contain component templates");
4632        ownership.insert(
4633            template.id.clone(),
4634            SemanticOwner::entity(component.id.clone()),
4635        );
4636    }
4637
4638    for binding in form_field_bindings.values() {
4639        ownership.insert(
4640            binding.id.as_semantic_id().clone(),
4641            SemanticOwner::entity(binding.control_entity.clone()),
4642        );
4643    }
4644
4645    for entity in template_entities {
4646        ownership.insert(entity.id.clone(), entity.owner.clone());
4647    }
4648
4649    ownership
4650}
4651
4652#[cfg(test)]
4653mod tests {
4654    use std::collections::BTreeMap;
4655
4656    use super::{
4657        build_application_semantic_model, build_application_semantic_model_for_unit,
4658        build_file_route_application_semantic_model_for_unit_with_packages,
4659        build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring,
4660        collect_ownership,
4661    };
4662    use crate::{
4663        build_component_graph_for_module, build_template_graph, build_template_manifest_from_asm,
4664        build_template_semantic_entities, build_v2_component_graph_for_module,
4665        CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredDeclarationV1,
4666        CanonicalAuthoredSemanticModelV1, CompilationUnit, ComponentInstancePlan, SemanticEntity,
4667        SemanticEntityKind, SemanticOwner, SemanticReferenceKind, TemplateSemanticKind,
4668    };
4669
4670    #[test]
4671    fn file_route_assembly_projects_explicit_canonical_v2_components_without_decorators() {
4672        let unit = CompilationUnit::parse_sources([(
4673            "app/routes/index.tsx",
4674            "import { Component, state, action } from \"presolve\"; export class Home extends Component { count = state(0); increment = action(() => { this.count += 1; }); render() { return <button onClick={() => this.increment()}>Home</button>; } }",
4675        )]);
4676        let model = CanonicalAuthoredSemanticModelV1 {
4677            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4678            source_path: "app/routes/index.tsx".into(),
4679            declarations: vec![
4680                CanonicalAuthoredDeclarationV1 {
4681                    kind: CanonicalAuthoredDeclarationKindV1::Component,
4682                    subject: "Home".into(),
4683                    source: crate::AuthoredSourceRangeV1 {
4684                        start: 37,
4685                        end: 106,
4686                        line: 1,
4687                        column: 38,
4688                    },
4689                    intrinsic_identity: None,
4690                    derived_evidence: None,
4691                },
4692                CanonicalAuthoredDeclarationV1 {
4693                    kind: CanonicalAuthoredDeclarationKindV1::State,
4694                    subject: "Home.count".into(),
4695                    source: crate::AuthoredSourceRangeV1 {
4696                        start: 83,
4697                        end: 99,
4698                        line: 1,
4699                        column: 84,
4700                    },
4701                    intrinsic_identity: None,
4702                    derived_evidence: None,
4703                },
4704                CanonicalAuthoredDeclarationV1 {
4705                    kind: CanonicalAuthoredDeclarationKindV1::Action,
4706                    subject: "Home.increment".into(),
4707                    source: crate::AuthoredSourceRangeV1 {
4708                        start: 100,
4709                        end: 148,
4710                        line: 1,
4711                        column: 101,
4712                    },
4713                    intrinsic_identity: None,
4714                    derived_evidence: None,
4715                },
4716            ],
4717        };
4718        let asm =
4719            build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
4720                &unit,
4721                &crate::SemanticPackageResolutionTable::default(),
4722                &BTreeMap::from([("app/routes/index.tsx".into(), model)]),
4723            )
4724            .expect("canonical V2 route assembly");
4725        assert_eq!(asm.components.len(), 1);
4726        assert_eq!(asm.components[0].class_name, "Home");
4727        assert_eq!(asm.components[0].state_fields[0].name, "count");
4728        assert!(asm.components[0].methods.is_empty());
4729        let endpoint = &asm.components[0].action_endpoints[0];
4730        assert_eq!(endpoint.name, "increment");
4731        assert_eq!(
4732            endpoint.id,
4733            asm.components[0].id.action_endpoint("increment")
4734        );
4735        assert_eq!(
4736            asm.components[0].actions[0].owner,
4737            SemanticOwner::entity(endpoint.id.clone())
4738        );
4739        let batch = asm
4740            .effect_trigger_plan
4741            .action_batches
4742            .values()
4743            .find(|batch| batch.authored_action_endpoint == endpoint.id)
4744            .expect("V2 field endpoint action batch");
4745        let manifest = build_template_manifest_from_asm(&asm);
4746        assert_eq!(
4747            manifest.components[0].template.events[0]
4748                .method_id
4749                .as_deref(),
4750            Some(endpoint.id.as_str())
4751        );
4752        assert_eq!(
4753            manifest.components[0].template.events[0]
4754                .action_batch_id
4755                .as_deref(),
4756            Some(batch.id.as_str())
4757        );
4758        assert!(asm
4759            .diagnostics
4760            .iter()
4761            .all(|diagnostic| diagnostic.code != "PSC1001"));
4762    }
4763
4764    #[test]
4765    fn file_route_v2_slot_fields_bind_caller_content_through_the_existing_slot_model() {
4766        let unit = CompilationUnit::parse_sources([
4767            (
4768                "app/components/Panel.tsx",
4769                "import { Component, slot, type SlotContent } from \"presolve\"; export class Panel extends Component { children: SlotContent = slot(); render() { return <section><slot /></section>; } }",
4770            ),
4771            (
4772                "app/routes/index.tsx",
4773                "import { Component } from \"presolve\"; import { Panel } from \"../components/Panel\"; export class Index extends Component { render() { return <main><Panel><button>Projected</button></Panel></main>; } }",
4774            ),
4775        ]);
4776        let models = unit
4777            .files()
4778            .iter()
4779            .map(|parsed| {
4780                let component_sites = crate::component_inheritance_sites_v1(parsed);
4781                let components = crate::lower_component_inheritance_v1(
4782                    parsed,
4783                    component_sites
4784                        .into_iter()
4785                        .map(|site| crate::ResolvedComponentInheritanceV1 {
4786                            heritage_source: site.heritage_source,
4787                            component_identity: crate::ResolvedIntrinsicIdentityV1 {
4788                                name: "Component".into(),
4789                                flags: 32,
4790                                declaration_modules: vec!["presolve".into()],
4791                            },
4792                        }),
4793                )
4794                .expect("V2 component authority")
4795                .model;
4796                let slots = crate::slot_field_sites_v1(parsed, &components).expect("V2 Slot sites");
4797                let lowering = crate::lower_v2_authoring_v1(
4798                    parsed,
4799                    crate::V2AuthoringResolutionsV1 {
4800                        components: crate::component_inheritance_sites_v1(parsed)
4801                            .into_iter()
4802                            .map(|site| crate::ResolvedComponentInheritanceV1 {
4803                                heritage_source: site.heritage_source,
4804                                component_identity: crate::ResolvedIntrinsicIdentityV1 {
4805                                    name: "Component".into(),
4806                                    flags: 32,
4807                                    declaration_modules: vec!["presolve".into()],
4808                                },
4809                            })
4810                            .collect(),
4811                        states: Vec::new(),
4812                        actions: Vec::new(),
4813                        effects: Vec::new(),
4814                        slots: slots
4815                            .into_iter()
4816                            .map(|site| crate::ResolvedSlotFieldV1 {
4817                                callee_source: site.callee_source,
4818                                slot_identity: crate::ResolvedIntrinsicIdentityV1 {
4819                                    name: "slot".into(),
4820                                    flags: 32,
4821                                    declaration_modules: vec!["presolve".into()],
4822                                },
4823                            })
4824                            .collect(),
4825                    },
4826                )
4827                .expect("V2 Slot lowering");
4828                (parsed.path.clone(), lowering.model)
4829            })
4830            .collect::<BTreeMap<_, _>>();
4831        let asm =
4832            build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
4833                &unit,
4834                &crate::SemanticPackageResolutionTable::default(),
4835                &models,
4836            )
4837            .expect("V2 Slot route assembly");
4838        assert_eq!(asm.slots().len(), 1);
4839        assert_eq!(asm.slot_content_fragments().len(), 1);
4840        assert_eq!(asm.slot_binding_registry().bindings.len(), 1);
4841    }
4842
4843    #[test]
4844    fn canonical_v2_action_rejects_unsupported_handler_before_publication() {
4845        let parsed = presolve_parser::parse_file(
4846            "app/routes/index.tsx",
4847            "import { Component, state, action } from \"presolve\"; export class Home extends Component { count = state(0); increment = action(() => { unrelated(); }); render() { return <main>Home</main>; } }",
4848        );
4849        let model = CanonicalAuthoredSemanticModelV1 {
4850            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4851            source_path: parsed.path.clone(),
4852            declarations: vec![
4853                CanonicalAuthoredDeclarationV1 {
4854                    kind: CanonicalAuthoredDeclarationKindV1::Component,
4855                    subject: "Home".into(),
4856                    source: crate::AuthoredSourceRangeV1 {
4857                        start: 0,
4858                        end: parsed.syntax.source.len(),
4859                        line: 1,
4860                        column: 1,
4861                    },
4862                    intrinsic_identity: None,
4863                    derived_evidence: None,
4864                },
4865                CanonicalAuthoredDeclarationV1 {
4866                    kind: CanonicalAuthoredDeclarationKindV1::State,
4867                    subject: "Home.count".into(),
4868                    source: crate::AuthoredSourceRangeV1 {
4869                        start: 0,
4870                        end: 1,
4871                        line: 1,
4872                        column: 1,
4873                    },
4874                    intrinsic_identity: None,
4875                    derived_evidence: None,
4876                },
4877                CanonicalAuthoredDeclarationV1 {
4878                    kind: CanonicalAuthoredDeclarationKindV1::Action,
4879                    subject: "Home.increment".into(),
4880                    source: crate::AuthoredSourceRangeV1 {
4881                        start: 0,
4882                        end: 1,
4883                        line: 1,
4884                        column: 1,
4885                    },
4886                    intrinsic_identity: None,
4887                    derived_evidence: None,
4888                },
4889            ],
4890        };
4891        let graph = build_v2_component_graph_for_module(&parsed, &model);
4892        assert!(graph.components[0].action_endpoints.is_empty());
4893        assert!(graph
4894            .diagnostics
4895            .iter()
4896            .any(|diagnostic| diagnostic.code == "PSV2A1004"));
4897    }
4898
4899    #[test]
4900    fn canonical_v2_action_projects_typed_parameter_ordinals_and_rejects_bad_events() {
4901        let parsed = presolve_parser::parse_file(
4902            "app/routes/index.tsx",
4903            "import { Component, state, action } from \"presolve\"; export class Home extends Component { count: number = state(0); setExact = action((value: number) => { this.count = value; }); setLocal = action(() => { const exact = 23; this.count = exact; }); render() { return <button onClick={() => this.setExact(\"wrong\")}>Set</button>; } }",
4904        );
4905        let model = CanonicalAuthoredSemanticModelV1 {
4906            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4907            source_path: parsed.path.clone(),
4908            declarations: vec![
4909                CanonicalAuthoredDeclarationV1 {
4910                    kind: CanonicalAuthoredDeclarationKindV1::Component,
4911                    subject: "Home".into(),
4912                    source: crate::AuthoredSourceRangeV1 {
4913                        start: 0,
4914                        end: parsed.syntax.source.len(),
4915                        line: 1,
4916                        column: 1,
4917                    },
4918                    intrinsic_identity: None,
4919                    derived_evidence: None,
4920                },
4921                CanonicalAuthoredDeclarationV1 {
4922                    kind: CanonicalAuthoredDeclarationKindV1::State,
4923                    subject: "Home.count".into(),
4924                    source: crate::AuthoredSourceRangeV1 {
4925                        start: 0,
4926                        end: 1,
4927                        line: 1,
4928                        column: 1,
4929                    },
4930                    intrinsic_identity: None,
4931                    derived_evidence: None,
4932                },
4933                CanonicalAuthoredDeclarationV1 {
4934                    kind: CanonicalAuthoredDeclarationKindV1::Action,
4935                    subject: "Home.setExact".into(),
4936                    source: crate::AuthoredSourceRangeV1 {
4937                        start: 0,
4938                        end: 1,
4939                        line: 1,
4940                        column: 1,
4941                    },
4942                    intrinsic_identity: None,
4943                    derived_evidence: None,
4944                },
4945                CanonicalAuthoredDeclarationV1 {
4946                    kind: CanonicalAuthoredDeclarationKindV1::Action,
4947                    subject: "Home.setLocal".into(),
4948                    source: crate::AuthoredSourceRangeV1 {
4949                        start: 0,
4950                        end: 1,
4951                        line: 1,
4952                        column: 1,
4953                    },
4954                    intrinsic_identity: None,
4955                    derived_evidence: None,
4956                },
4957            ],
4958        };
4959        let graph = build_v2_component_graph_for_module(&parsed, &model);
4960        assert_eq!(graph.components[0].actions.len(), 2);
4961        assert!(matches!(
4962            graph.components[0].actions[0].operation,
4963            crate::StateOperation::AssignParameter(ref ordinal) if ordinal == "0"
4964        ));
4965        assert!(matches!(
4966            graph.components[0].actions[1].operation,
4967            crate::StateOperation::Assign(crate::SerializableValue::Number(ref value)) if value == "23"
4968        ));
4969        assert!(graph
4970            .diagnostics
4971            .iter()
4972            .any(|diagnostic| diagnostic.code == "PSV2A1006"));
4973    }
4974
4975    #[test]
4976    fn lowers_supported_primitive_state_annotations_into_canonical_types() {
4977        let parsed = presolve_parser::parse_file(
4978            "src/TypedState.tsx",
4979            r#"
4980@component("x-typed-state")
4981class TypedState extends Component {
4982  count: number = state(0);
4983}
4984"#,
4985        );
4986
4987        let asm = build_application_semantic_model(&parsed);
4988
4989        let field = &asm.components[0].state_fields[0];
4990        let assignment = asm
4991            .semantic_types
4992            .assignments
4993            .get(&field.id)
4994            .expect("canonical declared type");
4995
4996        assert_eq!(assignment.semantic_type, crate::SemanticType::Number);
4997        assert_eq!(assignment.status, crate::SemanticTypeStatus::Declared);
4998        assert_eq!(
4999            assignment.provenance,
5000            field.declared_type.as_ref().unwrap().provenance
5001        );
5002    }
5003
5004    #[test]
5005    fn queries_canonical_type_information_from_the_asm() {
5006        let parsed = presolve_parser::parse_file(
5007            "src/TypeQueries.tsx",
5008            r#"
5009@component("x-type-queries")
5010class TypeQueries extends Component {
5011  count: number = state(0);
5012  label = state("Presolve");
5013}
5014"#,
5015        );
5016        let asm = build_application_semantic_model(&parsed);
5017        let count = &asm.components[0].state_fields[0];
5018        let label = &asm.components[0].state_fields[1];
5019
5020        assert_eq!(
5021            asm.semantic_type_of(&count.id),
5022            Some(&crate::SemanticType::Number)
5023        );
5024        assert_eq!(asm.type_declarations(&crate::SemanticType::Number).len(), 1);
5025        assert_eq!(asm.type_usages(&crate::SemanticType::String).len(), 1);
5026        assert_eq!(
5027            asm.serialization_compatibility_of(&count.id),
5028            Some(crate::SerializationCompatibility::Serializable)
5029        );
5030        assert_eq!(asm.is_type_assignable(&label.id, &count.id), Some(false));
5031    }
5032
5033    #[test]
5034    fn lowers_typed_method_parameters_into_canonical_entities() {
5035        let parsed = presolve_parser::parse_file(
5036            "src/Parameters.tsx",
5037            r#"
5038@component("x-parameters")
5039class Parameters extends Component {
5040  save(title: string, retries?: number) {}
5041}
5042"#,
5043        );
5044
5045        let asm = build_application_semantic_model(&parsed);
5046        let method = &asm.components[0].methods[0];
5047        let parameters = &method.parameters;
5048
5049        assert_eq!(parameters.len(), 2);
5050        assert_eq!(
5051            asm.semantic_types.assignments[&parameters[0].id].semantic_type,
5052            crate::SemanticType::String
5053        );
5054        assert_eq!(
5055            asm.semantic_types.assignments[&parameters[1].id].semantic_type,
5056            crate::SemanticType::Number
5057        );
5058        assert!(parameters.iter().all(|parameter| {
5059            asm.owner(&parameter.id) == Some(&SemanticOwner::entity(method.id.clone()))
5060                && asm
5061                    .entity(&parameter.id)
5062                    .is_some_and(|entity| entity.kind() == SemanticEntityKind::Parameter)
5063        }));
5064    }
5065
5066    #[test]
5067    fn lowers_declared_and_inferred_method_return_types() {
5068        let parsed = presolve_parser::parse_file(
5069            "src/Returns.tsx",
5070            r#"
5071@component("x-returns")
5072class Returns extends Component {
5073  declared(): string { return "Presolve"; }
5074  inferred() { return 1; }
5075}
5076"#,
5077        );
5078
5079        let asm = build_application_semantic_model(&parsed);
5080        let methods = &asm.components[0].methods;
5081        let declared = &methods[0];
5082        let inferred = &methods[1];
5083
5084        assert_eq!(
5085            asm.semantic_types.assignments[&declared.id].semantic_type,
5086            crate::SemanticType::String
5087        );
5088        assert_eq!(
5089            asm.semantic_types.assignments[&declared.id].status,
5090            crate::SemanticTypeStatus::Declared
5091        );
5092        assert_eq!(
5093            asm.semantic_types.assignments[&inferred.id].semantic_type,
5094            crate::SemanticType::Number
5095        );
5096        assert_eq!(
5097            asm.semantic_types.assignments[&inferred.id].status,
5098            crate::SemanticTypeStatus::Inferred
5099        );
5100    }
5101
5102    #[test]
5103    fn establishes_typed_computed_getter_contracts() {
5104        let parsed = presolve_parser::parse_file(
5105            "src/Computed.tsx",
5106            r#"
5107@component("x-computed")
5108class Computed extends Component {
5109  @computed()
5110  get remainingCount(): number { return 1; }
5111
5112  render() { return <p />; }
5113}
5114"#,
5115        );
5116
5117        let asm = build_application_semantic_model(&parsed);
5118        let method = &asm.components[0].methods[0];
5119        let computed_id = asm.components[0].id.computed("remainingCount");
5120        let computed = asm
5121            .semantic_types
5122            .computed_values
5123            .get(&computed_id)
5124            .expect("computed type contract");
5125        let computed_entity = asm
5126            .computed_value(&computed_id)
5127            .expect("first-class computed entity");
5128
5129        assert!(method.is_getter);
5130        assert!(method.is_computed());
5131        assert_eq!(
5132            computed.semantic_type,
5133            crate::SemanticType::NumberLiteral("1".to_string())
5134        );
5135        assert_eq!(
5136            computed.declared_return_type,
5137            Some(crate::SemanticType::Number)
5138        );
5139        assert_eq!(computed.declared_return_compatible, Some(true));
5140        assert_eq!(computed_entity.method, method.id);
5141        assert_eq!(
5142            computed_entity.owner,
5143            crate::SemanticOwner::entity(asm.components[0].id.clone())
5144        );
5145        assert_eq!(
5146            computed_entity.cache_policy,
5147            crate::ComputedCachePolicy::Memoized
5148        );
5149        assert_eq!(computed_entity.purity, crate::ComputedPurity::Pure);
5150        assert!(computed_entity.purity_violations.is_empty());
5151        assert_eq!(
5152            computed_entity.execution_boundary,
5153            crate::ExecutionBoundary::Client
5154        );
5155        assert_eq!(
5156            asm.entity(&computed_id),
5157            Some(SemanticEntity::Computed(computed_entity))
5158        );
5159        assert_eq!(
5160            asm.owner(&computed_id),
5161            Some(&crate::SemanticOwner::entity(asm.components[0].id.clone()))
5162        );
5163        assert_eq!(asm.provenance(&computed_id), asm.provenance(&method.id));
5164        assert_eq!(
5165            asm.entities_of_kind(SemanticEntityKind::Computed),
5166            vec![&computed_id]
5167        );
5168    }
5169
5170    #[test]
5171    fn resolves_computed_reads_to_canonical_state_and_computed_references() {
5172        let parsed = presolve_parser::parse_file(
5173            "src/ComputedReads.tsx",
5174            r#"
5175@component("x-computed-reads")
5176class ComputedReads extends Component {
5177  count = state(1);
5178  profile = state({ hidden: false });
5179
5180  @computed()
5181  get doubled() { return this.count * 2; }
5182
5183  @computed()
5184  get visible() { return this.doubled + this.count + this.count; }
5185
5186  @computed()
5187  get profileHidden() { return this.profile.hidden; }
5188
5189  @computed()
5190  get unresolved() { return this.missing; }
5191}
5192"#,
5193        );
5194        let asm = build_application_semantic_model(&parsed);
5195        let component = &asm.components[0];
5196        let count = component.id.state_field("count");
5197        let profile = component.id.state_field("profile");
5198        let doubled = component.id.computed("doubled");
5199        let visible = component.id.computed("visible");
5200        let profile_hidden = component.id.computed("profileHidden");
5201        let unresolved = component.id.computed("unresolved");
5202
5203        let state_references = asm.references_of_kind(SemanticReferenceKind::ComputedState);
5204        assert_eq!(state_references.len(), 3);
5205        assert!(state_references
5206            .iter()
5207            .any(|reference| reference.source == doubled && reference.target == count));
5208        assert!(state_references
5209            .iter()
5210            .any(|reference| reference.source == visible && reference.target == count));
5211        assert!(state_references.iter().any(|reference| {
5212            reference.source == profile_hidden && reference.target == profile
5213        }));
5214
5215        let computed_references = asm.references_of_kind(SemanticReferenceKind::ComputedComputed);
5216        assert_eq!(computed_references.len(), 1);
5217        assert_eq!(computed_references[0].source, visible);
5218        assert_eq!(computed_references[0].target, doubled);
5219        assert!(!asm
5220            .references
5221            .iter()
5222            .any(|reference| reference.source == unresolved));
5223        assert!(asm
5224            .references
5225            .iter()
5226            .all(|reference| { asm.provenance(&reference.source) == Some(&reference.provenance) }));
5227    }
5228
5229    #[test]
5230    fn populates_reactive_graph_from_direct_computed_reads() {
5231        let parsed = presolve_parser::parse_file(
5232            "src/ComputedReactiveGraph.tsx",
5233            r#"
5234@component("x-computed-reactive-graph")
5235class ComputedReactiveGraph extends Component {
5236  count = state(1);
5237
5238  @computed()
5239  get doubled() { return this.count * 2; }
5240
5241  @computed()
5242  get label() { return this.doubled + 1; }
5243}
5244"#,
5245        );
5246        let asm = build_application_semantic_model(&parsed);
5247        let component = &asm.components[0];
5248        let count = component.id.state_field("count");
5249        let doubled = component.id.computed("doubled");
5250        let label = component.id.computed("label");
5251        let graph = &asm.reactive_graph;
5252
5253        assert_eq!(graph.nodes.len(), 3);
5254        assert_eq!(
5255            graph.nodes[count.as_str()].kind,
5256            crate::IrReactiveNodeKind::State
5257        );
5258        assert_eq!(
5259            graph.nodes[doubled.as_str()].kind,
5260            crate::IrReactiveNodeKind::Computed
5261        );
5262        assert_eq!(
5263            graph.nodes[label.as_str()].kind,
5264            crate::IrReactiveNodeKind::Computed
5265        );
5266
5267        let doubled_dependencies = graph.computed_dependencies(doubled.as_str());
5268        assert_eq!(doubled_dependencies.len(), 1);
5269        assert_eq!(doubled_dependencies[0].target, count.as_str());
5270        let label_dependencies = graph.computed_dependencies(label.as_str());
5271        assert_eq!(label_dependencies.len(), 1);
5272        assert_eq!(label_dependencies[0].target, doubled.as_str());
5273
5274        let count_invalidations = graph.invalidations_from(count.as_str());
5275        assert_eq!(count_invalidations.len(), 1);
5276        assert_eq!(count_invalidations[0].target, doubled.as_str());
5277        let doubled_invalidations = graph.invalidations_from(doubled.as_str());
5278        assert_eq!(doubled_invalidations.len(), 1);
5279        assert_eq!(doubled_invalidations[0].target, label.as_str());
5280        assert!(graph
5281            .invalidations_from(count.as_str())
5282            .iter()
5283            .all(|edge| { edge.target != label.as_str() }));
5284    }
5285
5286    #[test]
5287    fn computes_deterministic_transitive_reactive_dependencies_and_dependents() {
5288        let parsed = presolve_parser::parse_file(
5289            "src/ComputedTransitiveReactiveGraph.tsx",
5290            r#"
5291@component("x-computed-transitive-reactive-graph")
5292class ComputedTransitiveReactiveGraph extends Component {
5293  count = state(1);
5294
5295  @computed()
5296  get doubled() { return this.count * 2; }
5297
5298  @computed()
5299  get label() { return this.doubled + 1; }
5300}
5301"#,
5302        );
5303        let asm = build_application_semantic_model(&parsed);
5304        let component = &asm.components[0];
5305        let count = component.id.state_field("count");
5306        let doubled = component.id.computed("doubled");
5307        let label = component.id.computed("label");
5308        let analysis = &asm.reactive_transitive_analysis;
5309
5310        assert_eq!(analysis.dependencies.len(), 3);
5311        assert_eq!(analysis.dependents.len(), 3);
5312        assert_eq!(
5313            analysis.dependencies_of(count.as_str()),
5314            Vec::<String>::new()
5315        );
5316        assert_eq!(
5317            analysis.dependencies_of(doubled.as_str()),
5318            vec![count.as_str().to_string()]
5319        );
5320        assert_eq!(
5321            analysis.dependencies_of(label.as_str()),
5322            vec![doubled.as_str().to_string(), count.as_str().to_string()]
5323        );
5324        assert_eq!(
5325            analysis.dependents_of(count.as_str()),
5326            vec![doubled.as_str().to_string(), label.as_str().to_string()]
5327        );
5328        assert_eq!(
5329            analysis.dependents_of(doubled.as_str()),
5330            vec![label.as_str().to_string()]
5331        );
5332        assert_eq!(analysis.dependents_of(label.as_str()), Vec::<String>::new());
5333    }
5334
5335    #[test]
5336    fn detects_computed_dependency_cycles_with_stable_diagnostics() {
5337        let parsed = presolve_parser::parse_file(
5338            "src/ComputedDependencyCycles.tsx",
5339            r#"
5340@component("x-computed-dependency-cycles")
5341class ComputedDependencyCycles extends Component {
5342  @computed()
5343  get alpha() { return this.beta; }
5344
5345  @computed()
5346  get beta() { return this.alpha; }
5347
5348  @computed()
5349  get selfLoop() { return this.selfLoop; }
5350}
5351"#,
5352        );
5353        let asm = build_application_semantic_model(&parsed);
5354        let component = &asm.components[0];
5355        let alpha = component.id.computed("alpha");
5356        let beta = component.id.computed("beta");
5357        let self_loop = component.id.computed("selfLoop");
5358
5359        assert_eq!(
5360            asm.reactive_cycle_analysis.cycles,
5361            vec![
5362                crate::IrReactiveCycle {
5363                    nodes: vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5364                },
5365                crate::IrReactiveCycle {
5366                    nodes: vec![self_loop.as_str().to_string()],
5367                },
5368            ]
5369        );
5370        let diagnostics = asm
5371            .diagnostics
5372            .iter()
5373            .filter(|diagnostic| diagnostic.code == "PSC1035")
5374            .collect::<Vec<_>>();
5375        assert_eq!(diagnostics.len(), 2);
5376        assert_eq!(
5377            diagnostics[0].message,
5378            format!("computed dependency cycle detected among: {alpha}, {beta}")
5379        );
5380        assert_eq!(
5381            diagnostics[0].provenance,
5382            Some(
5383                asm.computed_value(&alpha)
5384                    .expect("alpha computed value")
5385                    .provenance
5386                    .clone()
5387            )
5388        );
5389        assert_eq!(
5390            diagnostics[1].message,
5391            format!("computed dependency cycle detected among: {self_loop}")
5392        );
5393        assert_eq!(
5394            diagnostics[1].provenance,
5395            Some(
5396                asm.computed_value(&self_loop)
5397                    .expect("self-loop computed value")
5398                    .provenance
5399                    .clone()
5400            )
5401        );
5402    }
5403
5404    #[test]
5405    fn plans_deterministic_computed_evaluation_order_and_batches() {
5406        let parsed = presolve_parser::parse_file(
5407            "src/ComputedEvaluationPlan.tsx",
5408            r#"
5409@component("x-computed-evaluation-plan")
5410class ComputedEvaluationPlan extends Component {
5411  count = state(1);
5412  other = state(2);
5413
5414  @computed()
5415  get alpha() { return this.count; }
5416
5417  @computed()
5418  get beta() { return this.other; }
5419
5420  @computed()
5421  get gamma() { return this.alpha; }
5422}
5423"#,
5424        );
5425        let asm = build_application_semantic_model(&parsed);
5426        let component = &asm.components[0];
5427        let alpha = component.id.computed("alpha");
5428        let beta = component.id.computed("beta");
5429        let gamma = component.id.computed("gamma");
5430
5431        assert_eq!(
5432            asm.computed_evaluation_plan.evaluation_order,
5433            vec![
5434                alpha.as_str().to_string(),
5435                beta.as_str().to_string(),
5436                gamma.as_str().to_string(),
5437            ]
5438        );
5439        assert_eq!(
5440            asm.computed_evaluation_plan.update_batches,
5441            vec![
5442                vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5443                vec![gamma.as_str().to_string()],
5444            ]
5445        );
5446        assert!(asm.computed_evaluation_plan.unplanned.is_empty());
5447    }
5448
5449    #[test]
5450    fn leaves_cyclic_computed_values_unplanned() {
5451        let parsed = presolve_parser::parse_file(
5452            "src/CyclicComputedEvaluationPlan.tsx",
5453            r#"
5454@component("x-cyclic-computed-evaluation-plan")
5455class CyclicComputedEvaluationPlan extends Component {
5456  @computed()
5457  get alpha() { return this.beta; }
5458
5459  @computed()
5460  get beta() { return this.alpha; }
5461}
5462"#,
5463        );
5464        let asm = build_application_semantic_model(&parsed);
5465        let component = &asm.components[0];
5466        let alpha = component.id.computed("alpha");
5467        let beta = component.id.computed("beta");
5468
5469        assert!(asm.computed_evaluation_plan.evaluation_order.is_empty());
5470        assert!(asm.computed_evaluation_plan.update_batches.is_empty());
5471        assert_eq!(
5472            asm.computed_evaluation_plan.unplanned,
5473            vec![alpha.as_str().to_string(), beta.as_str().to_string()]
5474        );
5475    }
5476
5477    #[test]
5478    fn assigns_inferred_computed_types_and_validates_declared_returns() {
5479        let parsed = presolve_parser::parse_file(
5480            "src/ComputedTypes.tsx",
5481            r#"
5482@component("x-computed-types")
5483class ComputedTypes extends Component {
5484  count: number = state(1);
5485  profile = state({ label: "Presolve" });
5486
5487  @computed()
5488  get doubled(): number { return this.count * 2; }
5489
5490  @computed()
5491  get chained() { return this.doubled + 1; }
5492
5493  @computed()
5494  get label(): string { return this.profile.label; }
5495
5496  @computed()
5497  get invalid(): number { return "wrong"; }
5498
5499  @computed()
5500  get unresolved() { return this.missing; }
5501}
5502"#,
5503        );
5504        let asm = build_application_semantic_model(&parsed);
5505        let component = &asm.components[0];
5506        let doubled = component.id.computed("doubled");
5507        let chained = component.id.computed("chained");
5508        let label = component.id.computed("label");
5509        let invalid = component.id.computed("invalid");
5510        let unresolved = component.id.computed("unresolved");
5511
5512        let types = &asm.semantic_types.computed_values;
5513        assert_eq!(types[&doubled].semantic_type, crate::SemanticType::Number);
5514        assert_eq!(types[&chained].semantic_type, crate::SemanticType::Number);
5515        assert_eq!(types[&label].semantic_type, crate::SemanticType::String);
5516        assert_eq!(
5517            types[&invalid].semantic_type,
5518            crate::SemanticType::StringLiteral("wrong".to_string())
5519        );
5520        assert_eq!(types[&invalid].declared_return_compatible, Some(false));
5521        assert_eq!(types[&doubled].declared_return_compatible, Some(true));
5522        assert_eq!(
5523            types[&unresolved].semantic_type,
5524            crate::SemanticType::Unknown
5525        );
5526        assert_eq!(
5527            types[&unresolved].serialization,
5528            crate::SerializationCompatibility::NotSerializable
5529        );
5530        assert_eq!(
5531            types[&doubled].boundary_compatibility,
5532            crate::BoundaryCompatibility::Compatible
5533        );
5534        assert_eq!(
5535            types[&doubled].execution_boundary,
5536            crate::ExecutionBoundary::Client
5537        );
5538        assert_eq!(
5539            asm.semantic_type_of(&doubled),
5540            Some(&crate::SemanticType::Number)
5541        );
5542        assert_eq!(
5543            asm.serialization_compatibility_of(&chained),
5544            Some(crate::SerializationCompatibility::Serializable)
5545        );
5546    }
5547
5548    #[test]
5549    fn reports_stable_diagnostics_for_computed_semantics() {
5550        let parsed = presolve_parser::parse_file(
5551            "src/ComputedDiagnostics.tsx",
5552            r#"
5553@component("x-computed-diagnostics")
5554class ComputedDiagnostics extends Component {
5555  count = state(1);
5556
5557  @computed()
5558  invalidDeclaration() { return 1; }
5559
5560  @computed()
5561  get unsupportedBody() {
5562    const value = this.count;
5563    return value;
5564  }
5565
5566  @computed()
5567  get unresolvedRead() { return this.missing; }
5568
5569  @computed()
5570  get mismatch(): number { return "wrong"; }
5571
5572  @computed()
5573  get impure() { return helper(); }
5574
5575  @computed()
5576  get cycleA() { return this.cycleB; }
5577
5578  @computed()
5579  get cycleB() { return this.cycleA; }
5580
5581  render() { return <div />; }
5582}
5583"#,
5584        );
5585
5586        let asm = build_application_semantic_model(&parsed);
5587        let repeated = build_application_semantic_model(&parsed);
5588        assert_eq!(asm.diagnostics, repeated.diagnostics);
5589
5590        let component_graph = crate::build_component_graph(&parsed);
5591        let asm_from_component_graph =
5592            super::build_application_semantic_model_from_component_graph(&component_graph);
5593
5594        let codes = asm
5595            .diagnostics
5596            .iter()
5597            .map(|diagnostic| diagnostic.code.as_str())
5598            .collect::<std::collections::BTreeSet<_>>();
5599        assert_eq!(
5600            codes,
5601            std::collections::BTreeSet::from([
5602                "PSC1034", "PSC1035", "PSC1036", "PSC1037", "PSC1038", "PSC1039", "PSC1040",
5603            ])
5604        );
5605        assert_eq!(
5606            asm_from_component_graph
5607                .diagnostics
5608                .iter()
5609                .map(|diagnostic| diagnostic.code.as_str())
5610                .collect::<std::collections::BTreeSet<_>>(),
5611            codes
5612        );
5613        assert!(asm
5614            .diagnostics
5615            .iter()
5616            .all(|diagnostic| diagnostic.provenance.is_some()));
5617        assert!(asm.diagnostics.iter().any(|diagnostic| {
5618            diagnostic.code == crate::ComputedDiagnosticCode::InvalidDeclaration.as_str()
5619                && diagnostic.message
5620                    == "@computed() declaration `invalidDeclaration` must decorate a getter"
5621        }));
5622        assert!(asm.diagnostics.iter().any(|diagnostic| {
5623            diagnostic.code == crate::ComputedDiagnosticCode::UnsupportedBody.as_str()
5624                && diagnostic.message == "computed getter `unsupportedBody` has an unsupported body"
5625        }));
5626        assert!(asm.diagnostics.iter().any(|diagnostic| {
5627            diagnostic.code == crate::ComputedDiagnosticCode::UnresolvedRead.as_str()
5628                && diagnostic.message
5629                    == "computed getter `unresolvedRead` reads unresolved member `this.missing`"
5630        }));
5631        assert!(asm.diagnostics.iter().any(|diagnostic| {
5632            diagnostic.code == crate::ComputedDiagnosticCode::TypeMismatch.as_str()
5633                && diagnostic.message
5634                    == "computed getter `mismatch` returns `\"wrong\"` but declares `number`"
5635        }));
5636        assert!(asm.diagnostics.iter().any(|diagnostic| {
5637            diagnostic.code == crate::ComputedDiagnosticCode::SerializationViolation.as_str()
5638                && diagnostic.message
5639                    == "computed getter `unresolvedRead` has a non-serializable result"
5640        }));
5641    }
5642
5643    #[test]
5644    fn phase_e_audit_preserves_canonical_computed_products() {
5645        let path = "fixtures/0047-computed-diamond/input/ComputedDiamond.tsx";
5646        let parsed = presolve_parser::parse_file(
5647            path,
5648            include_str!("../../../fixtures/0047-computed-diamond/input/ComputedDiamond.tsx"),
5649        );
5650        let asm = build_application_semantic_model(&parsed);
5651        let component_graph = crate::build_component_graph(&parsed);
5652        let asm_from_component_graph =
5653            super::build_application_semantic_model_from_component_graph(&component_graph);
5654        let component = &asm.components[0];
5655        let count = component.id.state_field("count");
5656        let doubled = component.id.computed("doubled");
5657        let tripled = component.id.computed("tripled");
5658        let total = component.id.computed("total");
5659        let expected_owner = crate::SemanticOwner::entity(component.id.clone());
5660
5661        assert!(crate::validate_application_semantic_model(&asm).is_empty());
5662        assert!(crate::validate_application_semantic_model(&asm_from_component_graph).is_empty());
5663        for computed in [&doubled, &tripled, &total] {
5664            assert!(matches!(
5665                asm.entity(computed),
5666                Some(crate::SemanticEntity::Computed(_))
5667            ));
5668            assert_eq!(asm.owner(computed), Some(&expected_owner));
5669            assert_eq!(
5670                asm.semantic_types
5671                    .computed_values
5672                    .get(computed)
5673                    .map(|computed_type| &computed_type.computed),
5674                Some(computed)
5675            );
5676        }
5677
5678        let reactive_references = asm
5679            .references
5680            .iter()
5681            .filter(|reference| {
5682                matches!(
5683                    reference.kind,
5684                    crate::SemanticReferenceKind::ComputedState
5685                        | crate::SemanticReferenceKind::ComputedComputed
5686                )
5687            })
5688            .collect::<Vec<_>>();
5689        assert_eq!(reactive_references.len(), 4);
5690        for reference in reactive_references {
5691            assert!(asm.reactive_graph.edges.iter().any(|edge| {
5692                edge.kind == crate::IrReactiveEdgeKind::Reads
5693                    && edge.source == reference.source.as_str()
5694                    && edge.target == reference.target.as_str()
5695                    && edge.provenance == reference.provenance
5696            }));
5697            assert!(asm.reactive_graph.edges.iter().any(|edge| {
5698                edge.kind == crate::IrReactiveEdgeKind::Invalidates
5699                    && edge.source == reference.target.as_str()
5700                    && edge.target == reference.source.as_str()
5701                    && edge.provenance == reference.provenance
5702            }));
5703        }
5704        assert!(asm.reactive_graph.nodes.contains_key(count.as_str()));
5705        assert_eq!(
5706            asm.computed_evaluation_plan,
5707            crate::plan_computed_evaluation(&asm.reactive_graph)
5708        );
5709        assert_eq!(
5710            asm.computed_evaluation_plan.evaluation_order,
5711            vec![
5712                doubled.as_str().to_string(),
5713                tripled.as_str().to_string(),
5714                total.as_str().to_string(),
5715            ]
5716        );
5717
5718        let ir = crate::lower_components_to_ir(&asm);
5719        assert!(crate::validate_intermediate_representation(&ir).is_empty());
5720        let evaluations = ir
5721            .modules
5722            .iter()
5723            .flat_map(|module| &module.computed_evaluations)
5724            .map(|evaluation| evaluation.computed.clone())
5725            .collect::<Vec<_>>();
5726        assert_eq!(evaluations, vec![doubled, tripled, total]);
5727    }
5728
5729    #[test]
5730    fn classifies_computed_purity_and_reports_unsupported_behavior() {
5731        let mut parsed = presolve_parser::parse_file(
5732            "src/ComputedPurity.tsx",
5733            r#"
5734@component("x-computed-purity")
5735class ComputedPurity extends Component {
5736  count = state(1);
5737
5738  @action()
5739  save() {}
5740
5741  @effect()
5742  sync() {}
5743
5744  @computed()
5745  get pure() { return this.count; }
5746
5747  @computed()
5748  get mutates() { this.count++; return 1; }
5749
5750  @computed()
5751  get actionCall() { return this.save(); }
5752
5753  @computed()
5754  get effectCall() { console.log("effect"); return 1; }
5755
5756  @computed()
5757  get resourceCall() { return resource(); }
5758
5759  @computed()
5760  get arbitraryCall() { return helper(); }
5761
5762  @computed()
5763  get random() { return Math.random(); }
5764
5765  @computed()
5766  get asyncValue() { return 1; }
5767}
5768"#,
5769        );
5770        parsed.classes[0]
5771            .methods
5772            .iter_mut()
5773            .find(|method| method.name == "asyncValue")
5774            .expect("async test getter")
5775            .is_async = true;
5776
5777        let asm = build_application_semantic_model(&parsed);
5778        let component = &asm.components[0];
5779        let pure = component.id.computed("pure");
5780        let mutates = component.id.computed("mutates");
5781        let action_call = component.id.computed("actionCall");
5782        let effect_call = component.id.computed("effectCall");
5783        let resource_call = component.id.computed("resourceCall");
5784        let arbitrary_call = component.id.computed("arbitraryCall");
5785        let random = component.id.computed("random");
5786        let async_value = component.id.computed("asyncValue");
5787
5788        assert_eq!(
5789            asm.computed_value(&pure).expect("pure value").purity,
5790            crate::ComputedPurity::Pure
5791        );
5792        for (computed, kind) in [
5793            (mutates, crate::ComputedPurityViolationKind::StateMutation),
5794            (action_call, crate::ComputedPurityViolationKind::Action),
5795            (effect_call, crate::ComputedPurityViolationKind::Effect),
5796            (resource_call, crate::ComputedPurityViolationKind::Resource),
5797            (
5798                arbitrary_call,
5799                crate::ComputedPurityViolationKind::ArbitraryMethodCall,
5800            ),
5801            (
5802                random,
5803                crate::ComputedPurityViolationKind::NondeterministicOperation,
5804            ),
5805            (async_value, crate::ComputedPurityViolationKind::Async),
5806        ] {
5807            let value = asm.computed_value(&computed).expect("computed value");
5808            assert_eq!(value.purity, crate::ComputedPurity::Impure);
5809            assert!(value
5810                .purity_violations
5811                .iter()
5812                .any(|violation| violation.kind == kind));
5813        }
5814        let diagnostics = asm
5815            .diagnostics
5816            .iter()
5817            .filter(|diagnostic| diagnostic.code == "PSC1034")
5818            .collect::<Vec<_>>();
5819        assert_eq!(diagnostics.len(), 7);
5820        assert!(diagnostics
5821            .iter()
5822            .all(|diagnostic| diagnostic.provenance.is_some()));
5823    }
5824
5825    #[test]
5826    fn establishes_typed_action_input_and_output_contracts() {
5827        let parsed = presolve_parser::parse_file(
5828            "src/Action.tsx",
5829            r#"
5830type ActionResult = number;
5831
5832@component("x-action")
5833class Action extends Component {
5834  @action()
5835  async addTodo(input: string): ActionResult { return 1; }
5836}
5837"#,
5838        );
5839
5840        let asm = build_application_semantic_model(&parsed);
5841        let method = &asm.components[0].methods[0];
5842        let signature = asm
5843            .semantic_types
5844            .action_signatures
5845            .get(&method.id)
5846            .expect("action signature");
5847
5848        assert!(method.is_action());
5849        assert!(signature.is_async);
5850        assert_eq!(signature.input.len(), 1);
5851        assert_eq!(signature.input[0].1, crate::SemanticType::String);
5852        assert_eq!(signature.output, Some(crate::SemanticType::Number));
5853    }
5854
5855    #[test]
5856    fn lowers_supported_literal_state_annotations_into_canonical_types() {
5857        let parsed = presolve_parser::parse_file(
5858            "src/LiteralTypes.tsx",
5859            r#"
5860@component("x-literal-types")
5861class LiteralTypes extends Component {
5862  filter: "all" = state("all");
5863  step: 42 = state(42);
5864  enabled: true = state(true);
5865}
5866"#,
5867        );
5868
5869        let asm = build_application_semantic_model(&parsed);
5870        let types = asm.components[0]
5871            .state_fields
5872            .iter()
5873            .map(|field| {
5874                (
5875                    field.name.as_str(),
5876                    &asm.semantic_types.assignments[&field.id].semantic_type,
5877                )
5878            })
5879            .collect::<Vec<_>>();
5880
5881        assert_eq!(
5882            types,
5883            vec![
5884                (
5885                    "filter",
5886                    &crate::SemanticType::StringLiteral("all".to_string())
5887                ),
5888                (
5889                    "step",
5890                    &crate::SemanticType::NumberLiteral("42".to_string())
5891                ),
5892                ("enabled", &crate::SemanticType::BooleanLiteral(true)),
5893            ]
5894        );
5895    }
5896
5897    #[test]
5898    fn lowers_array_and_tuple_state_annotations_into_canonical_types() {
5899        let parsed = presolve_parser::parse_file(
5900            "src/CollectionTypes.tsx",
5901            r#"
5902@component("x-collection-types")
5903class CollectionTypes extends Component {
5904  names: string[] = state([]);
5905  todos: Todo[] = state([]);
5906  pair: [string, number] = state(["Presolve", 1]);
5907}
5908"#,
5909        );
5910
5911        let asm = build_application_semantic_model(&parsed);
5912        let types = asm.components[0]
5913            .state_fields
5914            .iter()
5915            .map(|field| {
5916                (
5917                    field.name.as_str(),
5918                    &asm.semantic_types.assignments[&field.id].semantic_type,
5919                )
5920            })
5921            .collect::<Vec<_>>();
5922
5923        assert_eq!(
5924            types,
5925            vec![
5926                (
5927                    "names",
5928                    &crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
5929                ),
5930                (
5931                    "todos",
5932                    &crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
5933                ),
5934                (
5935                    "pair",
5936                    &crate::SemanticType::Tuple(vec![
5937                        crate::SemanticType::String,
5938                        crate::SemanticType::Number,
5939                    ]),
5940                ),
5941            ]
5942        );
5943    }
5944
5945    #[test]
5946    fn lowers_structural_object_state_annotations_into_canonical_types() {
5947        let parsed = presolve_parser::parse_file(
5948            "src/ObjectTypes.tsx",
5949            r#"
5950@component("x-object-types")
5951class ObjectTypes extends Component {
5952  todo: { id: string; title: string; completed: boolean } = state({ id: "1", title: "Presolve", completed: false });
5953}
5954"#,
5955        );
5956
5957        let asm = build_application_semantic_model(&parsed);
5958        let field = &asm.components[0].state_fields[0];
5959        let assignment = &asm.semantic_types.assignments[&field.id];
5960        let crate::SemanticType::Object(object) = &assignment.semantic_type else {
5961            panic!("expected structural object type");
5962        };
5963
5964        assert_eq!(
5965            object.properties,
5966            std::collections::BTreeMap::from([
5967                ("completed".to_string(), crate::SemanticType::Boolean),
5968                ("id".to_string(), crate::SemanticType::String),
5969                ("title".to_string(), crate::SemanticType::String),
5970            ])
5971        );
5972    }
5973
5974    #[test]
5975    fn lowers_union_and_nullable_state_annotations_into_canonical_types() {
5976        let parsed = presolve_parser::parse_file(
5977            "src/UnionTypes.tsx",
5978            r#"
5979@component("x-union-types")
5980class UnionTypes extends Component {
5981  filter: "all" | "active" | "completed" = state("all");
5982  user: { id: string } | null = state(null);
5983}
5984"#,
5985        );
5986
5987        let asm = build_application_semantic_model(&parsed);
5988        let fields = &asm.components[0].state_fields;
5989        assert_eq!(
5990            asm.semantic_types.assignments[&fields[0].id].semantic_type,
5991            crate::SemanticType::Union(vec![
5992                crate::SemanticType::StringLiteral("active".to_string()),
5993                crate::SemanticType::StringLiteral("all".to_string()),
5994                crate::SemanticType::StringLiteral("completed".to_string()),
5995            ])
5996        );
5997        assert_eq!(
5998            asm.semantic_types.assignments[&fields[1].id].semantic_type,
5999            crate::SemanticType::Union(vec![
6000                crate::SemanticType::Null,
6001                crate::SemanticType::Object(crate::ObjectType {
6002                    properties: std::collections::BTreeMap::from([(
6003                        "id".to_string(),
6004                        crate::SemanticType::String,
6005                    )]),
6006                }),
6007            ])
6008        );
6009    }
6010
6011    #[test]
6012    fn resolves_local_type_aliases_with_canonical_alias_identity() {
6013        let parsed = presolve_parser::parse_file(
6014            "src/Aliases.tsx",
6015            r#"
6016type TodoId = string;
6017type Filter = "all" | "active" | "completed";
6018
6019@component("x-aliases")
6020class Aliases extends Component {
6021  id: TodoId = state("todo-1");
6022  filter: Filter = state("all");
6023}
6024"#,
6025        );
6026
6027        let asm = build_application_semantic_model(&parsed);
6028        let fields = &asm.components[0].state_fields;
6029        let todo_id = asm
6030            .semantic_types
6031            .aliases
6032            .values()
6033            .find(|alias| alias.name == "TodoId")
6034            .expect("TodoId alias");
6035        let filter = asm
6036            .semantic_types
6037            .aliases
6038            .values()
6039            .find(|alias| alias.name == "Filter")
6040            .expect("Filter alias");
6041
6042        assert_eq!(todo_id.semantic_type, crate::SemanticType::String);
6043        assert_eq!(
6044            filter.semantic_type,
6045            crate::SemanticType::Union(vec![
6046                crate::SemanticType::StringLiteral("active".to_string()),
6047                crate::SemanticType::StringLiteral("all".to_string()),
6048                crate::SemanticType::StringLiteral("completed".to_string()),
6049            ])
6050        );
6051        assert_eq!(
6052            asm.semantic_types.assignments[&fields[0].id].origin,
6053            todo_id.id
6054        );
6055        assert_eq!(
6056            asm.semantic_types.assignments[&fields[1].id].origin,
6057            filter.id
6058        );
6059    }
6060
6061    #[test]
6062    fn resolves_imported_type_aliases_through_named_reexports() {
6063        let unit = CompilationUnit::parse_sources([
6064            (
6065                "src/types.ts",
6066                r#"export type Filter = "all" | "active" | "completed";"#,
6067            ),
6068            ("src/index.ts", r#"export { Filter } from "./types";"#),
6069            (
6070                "src/App.tsx",
6071                r#"
6072import { Filter } from "./index";
6073
6074@component("x-app")
6075class App extends Component {
6076  filter: Filter = state("all");
6077}
6078"#,
6079            ),
6080        ]);
6081
6082        let asm = build_application_semantic_model_for_unit(&unit);
6083        let field = &asm
6084            .components
6085            .iter()
6086            .find(|component| component.class_name == "App")
6087            .expect("App component")
6088            .state_fields[0];
6089        let assignment = &asm.semantic_types.assignments[&field.id];
6090
6091        assert_eq!(
6092            assignment.semantic_type,
6093            crate::SemanticType::Union(vec![
6094                crate::SemanticType::StringLiteral("active".to_string()),
6095                crate::SemanticType::StringLiteral("all".to_string()),
6096                crate::SemanticType::StringLiteral("completed".to_string()),
6097            ])
6098        );
6099        assert_eq!(
6100            assignment.origin,
6101            crate::SemanticId::type_alias_in_module("src/types.ts", "Filter")
6102        );
6103    }
6104
6105    #[test]
6106    fn infers_state_types_from_direct_serializable_initializers() {
6107        let parsed = presolve_parser::parse_file(
6108            "src/InferredState.tsx",
6109            r#"
6110@component("x-inferred-state")
6111class InferredState extends Component {
6112  count = state(0);
6113  todos = state([]);
6114  tags = state(["Presolve"]);
6115  todo = state({ id: "1", completed: false });
6116}
6117"#,
6118        );
6119
6120        let asm = build_application_semantic_model(&parsed);
6121        let fields = &asm.components[0].state_fields;
6122        let types = fields
6123            .iter()
6124            .map(|field| {
6125                let assignment = &asm.semantic_types.assignments[&field.id];
6126                assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6127                (field.name.as_str(), assignment.semantic_type.clone())
6128            })
6129            .collect::<Vec<_>>();
6130
6131        assert_eq!(
6132            types,
6133            vec![
6134                ("count", crate::SemanticType::Number),
6135                (
6136                    "todos",
6137                    crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
6138                ),
6139                (
6140                    "tags",
6141                    crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
6142                ),
6143                (
6144                    "todo",
6145                    crate::SemanticType::Object(crate::ObjectType {
6146                        properties: std::collections::BTreeMap::from([
6147                            ("completed".to_string(), crate::SemanticType::Boolean),
6148                            ("id".to_string(), crate::SemanticType::String),
6149                        ]),
6150                    }),
6151                ),
6152            ]
6153        );
6154    }
6155
6156    #[test]
6157    fn propagates_canonical_types_to_expression_graph_nodes() {
6158        let parsed = presolve_parser::parse_file(
6159            "src/ExpressionTypes.tsx",
6160            r#"
6161@component("x-expression-types")
6162class ExpressionTypes extends Component {
6163  total = state((1 + 2) * 3);
6164  ready = state(1 < 2);
6165}
6166"#,
6167        );
6168
6169        let asm = build_application_semantic_model(&parsed);
6170        let total = &asm.components[0].state_fields[0];
6171        let ready = &asm.components[0].state_fields[1];
6172        let total_root = asm
6173            .expression_root(&total.id)
6174            .expect("total expression root");
6175        let ready_root = asm
6176            .expression_root(&ready.id)
6177            .expect("ready expression root");
6178
6179        assert_eq!(
6180            asm.semantic_types.assignments[total_root].semantic_type,
6181            crate::SemanticType::Number
6182        );
6183        assert_eq!(
6184            asm.semantic_types.assignments[ready_root].semantic_type,
6185            crate::SemanticType::Boolean
6186        );
6187        assert!(asm
6188            .expression_dependencies(total_root)
6189            .iter()
6190            .all(|id| asm.semantic_types.assignments.contains_key(*id)));
6191    }
6192
6193    #[test]
6194    fn derives_component_ownership_without_legacy_owner_fields() {
6195        let parsed = presolve_parser::parse_file(
6196            "src/Counter.tsx",
6197            r#"
6198@component("x-counter")
6199class Counter extends Component {
6200  count = state(0);
6201
6202  increment() {
6203    this.count++;
6204  }
6205
6206  render() {
6207    return <button onClick={() => this.increment()}>{this.count}</button>;
6208  }
6209}
6210"#,
6211        );
6212        let mut component_graph = build_component_graph_for_module(&parsed);
6213        let mut templates = build_template_graph(&component_graph).templates;
6214        let template_entities = build_template_semantic_entities(&templates);
6215        let component = component_graph
6216            .components
6217            .first_mut()
6218            .expect("component graph should contain Counter");
6219        let component_id = component.id.clone();
6220        let method_id = component.methods[0].id.clone();
6221        let action_id = component.actions[0].id.clone();
6222        let state_id = component.state_fields[0].id.clone();
6223        let event_id = component
6224            .render
6225            .as_ref()
6226            .expect("Counter should render")
6227            .event_handlers[0]
6228            .id
6229            .clone();
6230
6231        component.owner = SemanticOwner::entity(component_id.clone());
6232        component.state_fields[0].owner = SemanticOwner::Application;
6233        component.methods[0].owner = SemanticOwner::Application;
6234        component.actions[0].owner = SemanticOwner::Application;
6235        component
6236            .render
6237            .as_mut()
6238            .expect("Counter should render")
6239            .event_handlers[0]
6240            .owner = SemanticOwner::Application;
6241        templates[0].owner = SemanticOwner::Application;
6242
6243        let ownership = collect_ownership(
6244            &component_graph.components,
6245            &std::collections::BTreeMap::new(),
6246            &std::collections::BTreeMap::new(),
6247            &std::collections::BTreeMap::new(),
6248            &std::collections::BTreeMap::new(),
6249            &std::collections::BTreeMap::new(),
6250            &std::collections::BTreeMap::new(),
6251            &std::collections::BTreeMap::new(),
6252            &std::collections::BTreeMap::new(),
6253            &std::collections::BTreeMap::new(),
6254            &std::collections::BTreeMap::new(),
6255            &ComponentInstancePlan::default(),
6256            &std::collections::BTreeMap::new(),
6257            &std::collections::BTreeMap::new(),
6258            &templates,
6259            &template_entities,
6260        );
6261        assert_eq!(ownership[&component_id], SemanticOwner::Application);
6262        assert_eq!(
6263            ownership[&state_id],
6264            SemanticOwner::entity(component_id.clone())
6265        );
6266        assert_eq!(
6267            ownership[&method_id],
6268            SemanticOwner::entity(component_id.clone())
6269        );
6270        assert_eq!(ownership[&action_id], SemanticOwner::entity(method_id));
6271        assert_eq!(
6272            ownership[&event_id],
6273            SemanticOwner::entity(component_id.clone().template())
6274        );
6275        assert_eq!(
6276            ownership[&templates[0].id],
6277            SemanticOwner::entity(component_id)
6278        );
6279    }
6280
6281    #[test]
6282    #[allow(clippy::too_many_lines)]
6283    fn traverses_application_ownership_in_semantic_id_order() {
6284        let parsed = presolve_parser::parse_file(
6285            "src/Counter.tsx",
6286            r#"
6287@component("x-counter")
6288class Counter extends Component {
6289  count = state(0);
6290
6291  increment() {
6292    this.count++;
6293  }
6294
6295  render() {
6296    return <button onClick={this.increment}>{this.count}</button>;
6297  }
6298}
6299"#,
6300        );
6301
6302        let asm = build_application_semantic_model(&parsed);
6303        let component = &asm.components[0];
6304        let roots = asm.application_roots();
6305
6306        assert_eq!(
6307            roots,
6308            vec![
6309                &component.id,
6310                asm.component_instance_plan
6311                    .instances
6312                    .values()
6313                    .find(|instance| instance.depth == 0)
6314                    .expect("build root instance")
6315                    .id
6316                    .as_semantic_id(),
6317            ]
6318        );
6319        assert_eq!(
6320            asm.children_of(&component.id),
6321            vec![
6322                &component.methods[0].id,
6323                &component.methods[1].id,
6324                &component.state_fields[0].id,
6325                &asm.templates[0].id,
6326            ]
6327        );
6328        assert_eq!(
6329            asm.children_of(&component.methods[0].id),
6330            vec![&component.actions[0].id]
6331        );
6332        assert_eq!(asm.parent_of(&component.id), None);
6333        assert_eq!(
6334            asm.parent_of(&component.actions[0].id),
6335            Some(&component.methods[0].id)
6336        );
6337        assert_eq!(
6338            asm.ancestors_of(&component.actions[0].id),
6339            vec![&component.methods[0].id, &component.id]
6340        );
6341        let descendants = asm.descendants_of(&component.id);
6342        assert_eq!(descendants[0], &component.methods[0].id);
6343        assert_eq!(descendants[1], &component.actions[0].id);
6344        assert_eq!(descendants[2], &component.methods[1].id);
6345        assert_eq!(
6346            descendants.len(),
6347            asm.ownership.len() - asm.application_roots().len()
6348        );
6349        assert_eq!(
6350            asm.entities_of_kind(SemanticEntityKind::Method),
6351            vec![&component.methods[0].id, &component.methods[1].id]
6352        );
6353        assert_eq!(
6354            asm.entities_of_kind(SemanticEntityKind::Action),
6355            vec![&component.actions[0].id]
6356        );
6357        assert_eq!(
6358            asm.entities_of_kind(SemanticEntityKind::StateField),
6359            vec![&component.state_fields[0].id]
6360        );
6361        let state_id = &component.state_fields[0].id;
6362        let state_provenance = asm.provenance(state_id).expect("state provenance");
6363        assert_eq!(
6364            asm.entities_in_file(state_provenance.path.as_path()).len(),
6365            asm.ownership.len()
6366        );
6367        let at_state =
6368            asm.entities_at(state_provenance.path.as_path(), state_provenance.span.start);
6369        assert!(at_state.contains(&state_id));
6370        assert!(at_state.iter().all(|id| {
6371            let provenance = asm.provenance(id).expect("entity provenance");
6372            provenance.span.start <= state_provenance.span.start
6373                && state_provenance.span.start < provenance.span.end
6374        }));
6375        let action_references = asm.references_of_kind(SemanticReferenceKind::ActionState);
6376        assert_eq!(action_references.len(), 1);
6377        assert_eq!(action_references[0].source, component.actions[0].id);
6378        assert_eq!(action_references[0].target, component.state_fields[0].id);
6379        let action_provenance = &action_references[0].provenance;
6380        assert_eq!(
6381            asm.references_in_file(action_provenance.path.as_path())
6382                .len(),
6383            asm.references.len()
6384        );
6385        let at_action = asm.references_at(
6386            action_provenance.path.as_path(),
6387            action_provenance.span.start,
6388        );
6389        assert!(at_action.iter().any(|reference| {
6390            reference.source == component.actions[0].id
6391                && reference.target == component.state_fields[0].id
6392        }));
6393    }
6394
6395    #[test]
6396    fn resolves_template_state_dependencies() {
6397        let parsed = presolve_parser::parse_file(
6398            "src/Panel.tsx",
6399            r#"
6400@component("x-panel")
6401class Panel extends Component {
6402  enabled = state(true);
6403
6404  render() {
6405    return <section hidden={this.enabled}>{this.enabled}{this.enabled ? <span>On</span> : <span>Off</span>}</section>;
6406  }
6407}
6408"#,
6409        );
6410
6411        let asm = build_application_semantic_model(&parsed);
6412        let component = &asm.components[0];
6413        let state = &component.state_fields[0];
6414        let state_references = asm.references_to(&state.id);
6415
6416        assert_eq!(state_references.len(), 3);
6417        assert!(state_references.iter().all(|reference| {
6418            reference.kind == SemanticReferenceKind::TemplateState
6419                && reference.target == state.id
6420                && reference.provenance.path.as_path() == std::path::Path::new("src/Panel.tsx")
6421        }));
6422        assert!(state_references.iter().any(|reference| {
6423            asm.template_entity(&reference.source)
6424                .is_some_and(|entity| entity.kind == TemplateSemanticKind::Binding)
6425        }));
6426        assert!(state_references.iter().any(|reference| {
6427            asm.template_entity(&reference.source)
6428                .is_some_and(|entity| entity.kind == TemplateSemanticKind::AttributeBinding)
6429        }));
6430        assert!(state_references.iter().any(|reference| {
6431            asm.template_entity(&reference.source)
6432                .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6433        }));
6434    }
6435
6436    #[test]
6437    fn resolves_template_bindings_to_canonical_computed_entities() {
6438        let parsed = presolve_parser::parse_file(
6439            "src/ComputedTemplate.tsx",
6440            r#"
6441@component("x-computed-template")
6442class ComputedTemplate extends Component {
6443  @computed()
6444  get label(): string { return "Ready"; }
6445
6446  @computed()
6447  get visible(): boolean { return true; }
6448
6449  render() {
6450    return <section title={this.label}>{this.label}{this.visible ? <span>Visible</span> : <span>Hidden</span>}</section>;
6451  }
6452}
6453"#,
6454        );
6455
6456        let asm = build_application_semantic_model(&parsed);
6457        let component = &asm.components[0];
6458        let label = component.id.computed("label");
6459        let visible = component.id.computed("visible");
6460        let references = asm.references_of_kind(SemanticReferenceKind::TemplateComputed);
6461
6462        assert_eq!(references.len(), 3);
6463        assert_eq!(
6464            references
6465                .iter()
6466                .filter(|reference| reference.target == label)
6467                .count(),
6468            2
6469        );
6470        assert!(references.iter().any(|reference| {
6471            reference.target == visible
6472                && asm
6473                    .template_entity(&reference.source)
6474                    .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6475        }));
6476        assert!(references.iter().all(|reference| {
6477            asm.provenance(&reference.source) == Some(&reference.provenance)
6478                && asm.semantic_type_of(&reference.source)
6479                    == asm.semantic_type_of(&reference.target)
6480        }));
6481        assert_eq!(
6482            asm.references_of_kind(SemanticReferenceKind::TemplateState)
6483                .into_iter()
6484                .filter(|reference| reference.target == label || reference.target == visible)
6485                .count(),
6486            0
6487        );
6488
6489        let graph = crate::build_semantic_graph(&asm);
6490        assert_eq!(
6491            graph
6492                .edges
6493                .iter()
6494                .filter(|edge| edge.kind == crate::SemanticGraphEdgeKind::TemplateComputed)
6495                .count(),
6496            3
6497        );
6498    }
6499
6500    #[test]
6501    fn navigates_template_entities_from_canonical_ownership() {
6502        let parsed = presolve_parser::parse_file(
6503            "src/Panel.tsx",
6504            r#"
6505@component("x-panel")
6506class Panel extends Component {
6507  count = state(0);
6508
6509  render() {
6510    return <section>{this.count}</section>;
6511  }
6512}
6513"#,
6514        );
6515        let mut asm = build_application_semantic_model(&parsed);
6516        let template_id = asm.templates[0].id.clone();
6517        let expected = asm.template_entities_for(&template_id).len();
6518
6519        for entity in &mut asm.template_entities {
6520            entity.owner = SemanticOwner::Application;
6521        }
6522
6523        assert_eq!(asm.template_entities_for(&template_id).len(), expected);
6524    }
6525
6526    #[test]
6527    fn leaves_member_expressions_unresolved_without_expression_evaluation() {
6528        let parsed = presolve_parser::parse_file(
6529            "src/Panel.tsx",
6530            r#"
6531@component("x-panel")
6532class Panel extends Component {
6533  enabled = state(true);
6534
6535  render() {
6536    return <section data-value={this.enabled.value}>Panel</section>;
6537  }
6538}
6539"#,
6540        );
6541
6542        let asm = build_application_semantic_model(&parsed);
6543        assert_eq!(
6544            asm.references
6545                .iter()
6546                .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6547                .count(),
6548            0
6549        );
6550    }
6551
6552    #[test]
6553    fn resolves_keyed_list_iterable_to_component_state() {
6554        let parsed = presolve_parser::parse_file(
6555            "src/KeyedList.tsx",
6556            r#"
6557@component("x-keyed-list")
6558class KeyedList extends Component {
6559  items = state([]);
6560
6561  render() {
6562    return <ul>{this.items.map((item) => <li key={item}>{item}</li>)}</ul>;
6563  }
6564}
6565"#,
6566        );
6567
6568        let asm = build_application_semantic_model(&parsed);
6569        let component = &asm.components[0];
6570        let state = &component.state_fields[0];
6571        let reference = asm
6572            .references_to(&state.id)
6573            .into_iter()
6574            .find(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6575            .expect("list iterable state reference");
6576        let list = asm
6577            .template_entity(&reference.source)
6578            .expect("list semantic entity");
6579
6580        assert_eq!(list.kind, TemplateSemanticKind::List);
6581        assert_eq!(list.expression.as_deref(), Some("this.items"));
6582        assert_eq!(reference.provenance, list.provenance);
6583    }
6584
6585    #[test]
6586    fn resolves_template_event_attribute_to_component_method() {
6587        let parsed = presolve_parser::parse_file(
6588            "src/Counter.tsx",
6589            r#"
6590@component("x-counter")
6591class Counter extends Component {
6592  count = state(0);
6593
6594  increment() {
6595    this.count += 1;
6596  }
6597
6598  render() {
6599    return <button onClick={() => this.increment()}>Count</button>;
6600  }
6601}
6602"#,
6603        );
6604
6605        let asm = build_application_semantic_model(&parsed);
6606        let component = &asm.components[0];
6607        let method = component
6608            .methods
6609            .iter()
6610            .find(|method| method.name == "increment")
6611            .expect("increment method");
6612        let reference = asm
6613            .references_to(&method.id)
6614            .into_iter()
6615            .find(|reference| {
6616                reference.kind == SemanticReferenceKind::EventMethod
6617                    && asm
6618                        .template_entity(&reference.source)
6619                        .is_some_and(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
6620            })
6621            .expect("template event method reference");
6622
6623        assert_eq!(
6624            reference.provenance.path,
6625            std::path::Path::new("src/Counter.tsx")
6626        );
6627        assert_eq!(reference.provenance.span.line, 11);
6628    }
6629
6630    #[test]
6631    fn resolves_render_template_bindings_to_unique_method_locals() {
6632        let parsed = presolve_parser::parse_file(
6633            "src/LocalResolution.tsx",
6634            r#"
6635@component("x-local-resolution")
6636class LocalResolution extends Component {
6637  render() {
6638    const title = "Presolve";
6639    return <output title={title}>{title}</output>;
6640  }
6641}
6642"#,
6643        );
6644
6645        let asm = build_application_semantic_model(&parsed);
6646        let component = &asm.components[0];
6647        let render = component
6648            .methods
6649            .iter()
6650            .find(|method| method.name == "render")
6651            .expect("render method");
6652        let local = &render.local_variables[0];
6653        let references = asm.references_of_kind(SemanticReferenceKind::TemplateLocal);
6654        let assignment = asm
6655            .semantic_types
6656            .assignments
6657            .get(&local.id)
6658            .expect("canonical inferred local type");
6659
6660        assert_eq!(
6661            asm.owner(&local.id),
6662            Some(&SemanticOwner::entity(render.id.clone()))
6663        );
6664        assert_eq!(
6665            asm.entities_of_kind(SemanticEntityKind::LocalVariable),
6666            vec![&local.id]
6667        );
6668        assert_eq!(references.len(), 2);
6669        assert!(references.iter().all(|reference| {
6670            reference.target == local.id
6671                && reference.provenance.path == std::path::Path::new("src/LocalResolution.tsx")
6672        }));
6673        assert_eq!(assignment.semantic_type, crate::SemanticType::String);
6674        assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6675        assert_eq!(assignment.provenance.span, local.span);
6676
6677        let semantic_graph = crate::build_semantic_graph(&asm);
6678        assert!(semantic_graph.nodes.iter().any(|node| {
6679            node.kind == crate::SemanticGraphNodeKind::LocalVariable && node.id == local.id
6680        }));
6681        assert_eq!(
6682            semantic_graph
6683                .edges
6684                .iter()
6685                .filter(|edge| {
6686                    edge.kind == crate::SemanticGraphEdgeKind::TemplateLocal
6687                        && edge.target == local.id
6688                })
6689                .count(),
6690            2
6691        );
6692
6693        let template_graph = build_template_graph(&build_component_graph_for_module(&parsed));
6694        assert_eq!(
6695            crate::generate_static_html(&template_graph),
6696            "<output data-presolve-node=\"n0\" title=\"Presolve\" data-presolve-bindings=\"title\"><!-- presolve-binding:n2:title -->Presolve</output>\n"
6697        );
6698    }
6699
6700    #[test]
6701    fn queries_canonical_expression_graphs_by_dependency_provenance_and_owner() {
6702        let parsed = presolve_parser::parse_file(
6703            "src/ExpressionQueries.tsx",
6704            r#"
6705@component("x-expression-queries")
6706class ExpressionQueries extends Component {
6707  total = state((1 + 2) * 3);
6708}
6709"#,
6710        );
6711
6712        let asm = build_application_semantic_model(&parsed);
6713        let field = &asm.components[0].state_fields[0];
6714        let root = asm.expression_root(&field.id).expect("expression root");
6715        let left = field.id.expression("root.0");
6716        let right = field.id.expression("root.1");
6717
6718        assert_eq!(asm.expression(root).map(|node| &node.id), Some(root));
6719        assert_eq!(asm.expression_owner(root), Some(&field.id));
6720        assert_eq!(asm.expression_dependencies(root), vec![&left, &right]);
6721        assert_eq!(
6722            asm.expression_dependents(&left)
6723                .into_iter()
6724                .map(|node| &node.id)
6725                .collect::<Vec<_>>(),
6726            vec![root]
6727        );
6728        assert_eq!(asm.expressions_for(&field.id).len(), 5);
6729
6730        let provenance = asm
6731            .expression_provenance(root)
6732            .expect("expression provenance");
6733        assert_eq!(
6734            provenance.path,
6735            std::path::Path::new("src/ExpressionQueries.tsx")
6736        );
6737        assert_eq!(asm.expressions_in_file(&provenance.path).len(), 5);
6738        assert_eq!(
6739            asm.expressions_at(&provenance.path, provenance.span.start)
6740                .into_iter()
6741                .map(|node| &node.id)
6742                .collect::<Vec<_>>(),
6743            vec![root]
6744        );
6745    }
6746
6747    #[test]
6748    fn file_route_model_composes_layout_default_slots_into_canonical_instances() {
6749        let unit = CompilationUnit::parse_sources([
6750            (
6751                "app/layout.tsx",
6752                r#"
6753@component()
6754class AppLayout extends Component {
6755  @slot() children!: SlotContent;
6756  render() { return <main><slot /></main>; }
6757}
6758"#,
6759            ),
6760            (
6761                "app/routes/index.tsx",
6762                r#"
6763@component()
6764class Home extends Component {
6765  render() { return <article>Home</article>; }
6766}
6767"#,
6768            ),
6769        ]);
6770        let asm = build_file_route_application_semantic_model_for_unit_with_packages(
6771            &unit,
6772            &crate::SemanticPackageResolutionTable::default(),
6773        )
6774        .expect("valid conventional layout");
6775
6776        assert_eq!(asm.component_instance_plan.roots.len(), 1);
6777        let binding = asm
6778            .slot_bindings
6779            .bindings
6780            .values()
6781            .find(|binding| binding.direct_child_instance.is_some())
6782            .expect("compiler-issued layout Slot binding");
6783        assert_eq!(binding.status, crate::SlotBindingStatus::Bound);
6784        assert!(binding.content_fragment.is_none());
6785        assert!(crate::validate_application_semantic_model(&asm).is_empty());
6786
6787        let html = crate::generate_ordinary_instance_html(&asm);
6788        assert!(html.contains("<main"));
6789        assert!(html.contains("<article"));
6790        assert!(html.contains("Home"));
6791    }
6792
6793    #[test]
6794    fn retains_route_loader_source_facts_before_server_capability_resolution() {
6795        let asm = build_application_semantic_model(&presolve_parser::parse_file(
6796            "app/routes/posts/[slug].tsx",
6797            r#"
6798@component()
6799class Post {
6800  @loader("loadPost") post!: Resource<Post, NotFound>;
6801  render() { return <article />; }
6802}
6803"#,
6804        ));
6805
6806        let loader = &asm.components[0].route_loader_declaration_candidates[0];
6807        assert_eq!(loader.field, "post");
6808        assert!(loader.decorator_invoked);
6809        assert_eq!(loader.decorator_argument_count, 1);
6810        assert_eq!(loader.endpoint_designator.as_deref(), Some("loadPost"));
6811        assert_eq!(
6812            loader
6813                .declared_type
6814                .as_ref()
6815                .map(|type_| type_.text.as_str()),
6816            Some("Resource<Post, NotFound>")
6817        );
6818        assert!(asm
6819            .diagnostics
6820            .iter()
6821            .any(|diagnostic| diagnostic.code == "PSC1132"));
6822    }
6823
6824    #[test]
6825    fn retains_server_action_source_facts_before_route_capability_resolution() {
6826        let asm = build_application_semantic_model(&presolve_parser::parse_file(
6827            "app/routes/posts/[slug].tsx",
6828            r#"
6829@component()
6830class Post {
6831  @serverAction("savePost") save(): void {}
6832  render() { return <form />; }
6833}
6834"#,
6835        ));
6836
6837        let action = &asm.components[0].server_action_facts[0];
6838        assert_eq!(action.method_name, "save");
6839        assert!(action.invoked);
6840        assert_eq!(action.argument_count, 1);
6841        assert_eq!(action.endpoint_designator.as_deref(), Some("savePost"));
6842        assert!(!action.is_action);
6843        assert!(!action.action_invoked);
6844        assert!(!action.has_body_effects);
6845        assert!(asm
6846            .diagnostics
6847            .iter()
6848            .any(|diagnostic| diagnostic.code == "PSC1133"));
6849    }
6850}