Skip to main content

presolve_compiler/
application_semantic_model.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
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        None,
1653        None,
1654    );
1655    let serialization = collect_serialization_products(
1656        &component_graph.components,
1657        &forms,
1658        &form_fields,
1659        &submissions.plans,
1660    );
1661    let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
1662    let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
1663    let optimized_form_ir = optimize_form_ir(&form_ir);
1664    let submission_host_products = collect_submission_host_products(
1665        &templates,
1666        &component_graph.components,
1667        &forms,
1668        &form_field_bindings,
1669        &submissions.plans,
1670        &serialization.plans,
1671        &optimized_form_ir.optimized,
1672    );
1673    let runtime_forms = build_runtime_form_registry(
1674        &forms,
1675        &form_fields,
1676        &form_field_bindings,
1677        &validation_rules,
1678        &optimized_form_ir.optimized,
1679        &submissions,
1680        &serialization,
1681        &reset,
1682        &submission_host_products.hosts,
1683    );
1684    let effect_execution_plan = plan_effect_execution(
1685        &computed_values,
1686        &effects,
1687        &effect_reactive_analysis,
1688        &effect_trigger_plan,
1689        &reactive_transitive_analysis,
1690        &computed_evaluation_plan,
1691    );
1692    let mut diagnostics = component_graph.diagnostics.clone();
1693    diagnostics.extend(computed_diagnostics);
1694    extend_computed_diagnostics(
1695        &mut diagnostics,
1696        &component_graph.components,
1697        &computed_values,
1698        &resource_declarations,
1699        &expression_graph,
1700        &semantic_types,
1701        &reactive_cycle_analysis,
1702        &provenance,
1703    );
1704    let component_ids = component_graph
1705        .components
1706        .iter()
1707        .map(|component| component.id.clone())
1708        .collect::<BTreeSet<_>>();
1709    let composition_types = collect_composition_type_products(
1710        &component_ids,
1711        &component_invocations,
1712        &slot_bindings,
1713        &slots,
1714        &slot_composition.fragments,
1715        &slot_composition.outlets,
1716        &instance_context,
1717        &context_type_products.bindings,
1718        &context_type_products.contexts,
1719        &context_type_products.providers,
1720        &context_lifetime,
1721        &ownership,
1722        &references,
1723    );
1724    let component_initialization = plan_component_initialization(
1725        &component_instance_plan,
1726        &slot_bindings,
1727        &composition_types,
1728        &instance_context,
1729    );
1730    let mut model = ApplicationSemanticModel {
1731        expression_graph,
1732        semantic_types,
1733        components: components_with_resolved_form_field_candidates(
1734            &component_graph.components,
1735            &form_field_declaration_candidates,
1736        ),
1737        contexts,
1738        providers,
1739        consumers,
1740        forms,
1741        resource_endpoint_resolutions,
1742        opaque_action_resolutions,
1743        resource_declarations,
1744        resource_activations,
1745        form_field_declaration_candidates,
1746        form_fields,
1747        form_field_binding_candidates,
1748        form_field_bindings,
1749        form_ownership,
1750        validation_rule_candidates,
1751        validation_rules,
1752        validation_graph,
1753        validation_dependency_plans,
1754        form_tracking,
1755        submissions,
1756        submission_host_candidates: submission_host_products.candidates,
1757        submission_hosts: submission_host_products.hosts,
1758        serialization,
1759        reset,
1760        form_ir,
1761        optimized_form_ir,
1762        runtime_forms,
1763        slots,
1764        component_invocations,
1765        component_instance_plan,
1766        component_instance_scope,
1767        component_composition,
1768        component_initialization,
1769        component_ir: ComponentIrReport::default(),
1770        component_ir_optimization: OptimizedComponentIrReport::default(),
1771        instance_context,
1772        slot_content_fragments: slot_composition.fragments,
1773        slot_outlets: slot_composition.outlets,
1774        slot_bindings,
1775        composition_types,
1776        context_declaration_candidates,
1777        context_ownership,
1778        context_dependency,
1779        context_lifetime,
1780        context_evaluation,
1781        component_scope,
1782        context_resolutions,
1783        context_types: context_type_products.contexts,
1784        provider_types: context_type_products.providers,
1785        consumer_types: context_type_products.consumers,
1786        context_binding_types: context_type_products.bindings,
1787        duplicate_provider_declarations,
1788        computed_values,
1789        effects,
1790        effect_reactive_analysis,
1791        effect_trigger_plan,
1792        effect_execution_plan,
1793        effect_bodies,
1794        effect_statements,
1795        reactive_graph,
1796        reactive_transitive_analysis,
1797        reactive_cycle_analysis,
1798        computed_evaluation_plan,
1799        templates,
1800        template_entities,
1801        diagnostics,
1802        ownership,
1803        references,
1804        provenance,
1805    };
1806    model
1807        .diagnostics
1808        .extend(crate::collect_effect_diagnostics(&model));
1809    model
1810        .diagnostics
1811        .extend(crate::collect_context_diagnostics(&model));
1812    model
1813        .diagnostics
1814        .extend(crate::collect_form_diagnostics(&model));
1815    model.component_ir = lower_component_ir(&model);
1816    model.component_ir_optimization = optimize_component_ir(&model.component_ir);
1817    model
1818        .diagnostics
1819        .extend(crate::collect_component_diagnostics(&model));
1820    model
1821}
1822
1823#[must_use]
1824pub fn build_application_semantic_model_for_unit(
1825    unit: &CompilationUnit,
1826) -> ApplicationSemanticModel {
1827    build_application_semantic_model_for_unit_with_packages(
1828        unit,
1829        &crate::semantic_package::SemanticPackageResolutionTable::default(),
1830    )
1831}
1832
1833/// Build the canonical application model with caller-supplied package semantics.
1834#[must_use]
1835pub fn build_application_semantic_model_for_unit_with_packages(
1836    unit: &CompilationUnit,
1837    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1838) -> ApplicationSemanticModel {
1839    let symbols = crate::build_symbol_table(unit);
1840    let modules = crate::build_module_graph(unit);
1841    let bindings =
1842        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1843    build_application_semantic_model_from_files_with_bindings(unit.files(), Some(&bindings))
1844}
1845
1846/// Builds the canonical application model for a discovered file-route source
1847/// set. Conventional layouts are compiler-issued default-Slot composition
1848/// edges, so every downstream product observes the same instance topology.
1849///
1850/// # Errors
1851///
1852/// Returns stable route or layout composition diagnostics before publication
1853/// can materialize an incomplete page.
1854pub fn build_file_route_application_semantic_model_for_unit_with_packages(
1855    unit: &CompilationUnit,
1856    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1857) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1858    let symbols = crate::build_symbol_table(unit);
1859    let modules = crate::build_module_graph(unit);
1860    let bindings =
1861        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1862    build_application_semantic_model_from_files_with_bindings_mode(
1863        unit.files(),
1864        Some(&bindings),
1865        ApplicationAssemblyMode::FileRoutes,
1866    )
1867}
1868
1869/// File-route assembly with explicit canonical V2 authoring evidence. Entries
1870/// absent from `v2_authoring` remain on the named legacy compatibility path;
1871/// entries present in the map are projected through the V2 graph adapter.
1872pub fn build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
1873    unit: &CompilationUnit,
1874    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1875    v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
1876) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1877    let symbols = crate::build_symbol_table(unit);
1878    let modules = crate::build_module_graph(unit);
1879    let mut bindings =
1880        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1881    retain_standard_schema_runtime_bindings(&mut bindings, unit, &modules, v2_authoring);
1882    build_application_semantic_model_from_files_with_bindings_mode_and_v2(
1883        unit.files(),
1884        Some(&bindings),
1885        ApplicationAssemblyMode::FileRoutes,
1886        Some(v2_authoring),
1887    )
1888}
1889
1890fn retain_standard_schema_runtime_bindings(
1891    bindings: &mut crate::BindingTable,
1892    unit: &CompilationUnit,
1893    modules: &crate::ModuleGraph,
1894    v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
1895) {
1896    let mut imports = BTreeSet::new();
1897    let mut specifiers = BTreeSet::new();
1898    let mut exports = BTreeSet::new();
1899    for model in v2_authoring.values() {
1900        let Some(parsed) = unit
1901            .files()
1902            .iter()
1903            .find(|parsed| parsed.path == model.source_path)
1904        else {
1905            continue;
1906        };
1907        for evidence in model
1908            .declarations
1909            .iter()
1910            .filter_map(|declaration| declaration.derived_evidence.as_ref())
1911        {
1912            let crate::DerivedAuthoredEvidenceV2::StandardSchemaValidation {
1913                module_specifier,
1914                export_name,
1915                declaration_modules,
1916                ..
1917            } = evidence
1918            else {
1919                continue;
1920            };
1921            let Some(import) = parsed
1922                .imports
1923                .iter()
1924                .find(|import| import.source == *module_specifier)
1925            else {
1926                continue;
1927            };
1928            if let Some(local) = import
1929                .specifiers
1930                .iter()
1931                .find(|specifier| specifier.imported == *export_name)
1932                .map(|specifier| specifier.local.clone())
1933            {
1934                imports.insert((model.source_path.clone(), local));
1935                specifiers.insert((model.source_path.clone(), module_specifier.clone()));
1936            }
1937            if let Some(crate::ModuleTarget::Resolved(target)) = modules
1938                .edges
1939                .iter()
1940                .find(|edge| {
1941                    edge.kind == crate::ModuleEdgeKind::Import
1942                        && edge.source == model.source_path
1943                        && edge.specifier == *module_specifier
1944                })
1945                .map(|edge| &edge.target)
1946            {
1947                exports.insert((target.clone(), export_name.clone()));
1948            }
1949            for target in declaration_modules {
1950                if unit
1951                    .files()
1952                    .iter()
1953                    .any(|file| file.path == Path::new(target))
1954                {
1955                    exports.insert((PathBuf::from(target), export_name.clone()));
1956                }
1957            }
1958        }
1959    }
1960    bindings.diagnostics.retain(|diagnostic| {
1961        !((diagnostic.code == "PSBIND1001"
1962            && exports.contains(&(diagnostic.module.clone(), diagnostic.name.clone())))
1963            || (diagnostic.code == "PSBIND1003"
1964                && imports.contains(&(diagnostic.module.clone(), diagnostic.name.clone())))
1965            || (matches!(diagnostic.code.as_str(), "PSBIND1002" | "PSBIND1009")
1966                && specifiers.contains(&(diagnostic.module.clone(), diagnostic.name.clone()))))
1967    });
1968}
1969
1970/// Builds the canonical composed model for one already-resolved file-route
1971/// page. Publication uses this route-scoped entry so shared layouts never
1972/// project sibling pages into the same default Slot.
1973///
1974/// # Errors
1975///
1976/// Returns stable route/layout diagnostics, including an unknown selected
1977/// route component.
1978pub fn build_file_route_application_semantic_model_for_route_with_packages(
1979    unit: &CompilationUnit,
1980    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1981    route_component: &SemanticId,
1982) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1983    let symbols = crate::build_symbol_table(unit);
1984    let modules = crate::build_module_graph(unit);
1985    let bindings =
1986        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1987    build_application_semantic_model_from_files_with_bindings_mode(
1988        unit.files(),
1989        Some(&bindings),
1990        ApplicationAssemblyMode::FileRoute(route_component.clone()),
1991    )
1992}
1993
1994/// Route-scoped V2-aware counterpart to
1995/// [`build_file_route_application_semantic_model_for_route_with_packages`].
1996pub fn build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring(
1997    unit: &CompilationUnit,
1998    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1999    route_component: &SemanticId,
2000    v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
2001) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
2002    let symbols = crate::build_symbol_table(unit);
2003    let modules = crate::build_module_graph(unit);
2004    let mut bindings =
2005        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
2006    retain_standard_schema_runtime_bindings(&mut bindings, unit, &modules, v2_authoring);
2007    build_application_semantic_model_from_files_with_bindings_mode_and_v2(
2008        unit.files(),
2009        Some(&bindings),
2010        ApplicationAssemblyMode::FileRoute(route_component.clone()),
2011        Some(v2_authoring),
2012    )
2013}
2014
2015fn build_application_semantic_model_from_files(files: &[ParsedFile]) -> ApplicationSemanticModel {
2016    build_application_semantic_model_from_files_with_bindings(files, None)
2017}
2018
2019#[allow(clippy::too_many_lines)]
2020fn build_application_semantic_model_from_files_with_bindings(
2021    files: &[ParsedFile],
2022    bindings: Option<&crate::BindingTable>,
2023) -> ApplicationSemanticModel {
2024    build_application_semantic_model_from_files_with_bindings_mode(
2025        files,
2026        bindings,
2027        ApplicationAssemblyMode::Authored,
2028    )
2029    .expect("authored application assembly cannot derive file-route diagnostics")
2030}
2031
2032#[derive(Debug, Clone, PartialEq, Eq)]
2033enum ApplicationAssemblyMode {
2034    Authored,
2035    FileRoutes,
2036    FileRoute(SemanticId),
2037}
2038
2039#[allow(clippy::too_many_lines)]
2040fn build_application_semantic_model_from_files_with_bindings_mode(
2041    files: &[ParsedFile],
2042    bindings: Option<&crate::BindingTable>,
2043    mode: ApplicationAssemblyMode,
2044) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
2045    build_application_semantic_model_from_files_with_bindings_mode_and_v2(
2046        files, bindings, mode, None,
2047    )
2048}
2049
2050#[allow(clippy::too_many_lines)]
2051fn build_application_semantic_model_from_files_with_bindings_mode_and_v2(
2052    files: &[ParsedFile],
2053    bindings: Option<&crate::BindingTable>,
2054    mode: ApplicationAssemblyMode,
2055    v2_authoring: Option<&BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>>,
2056) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
2057    let (mut components, mut templates, mut diagnostics, mut references) =
2058        (Vec::new(), Vec::new(), Vec::new(), Vec::new());
2059    let (mut provenance, mut template_entities, mut type_aliases) =
2060        (BTreeMap::new(), Vec::new(), Vec::new());
2061    for parsed in files {
2062        let component_graph = v2_authoring
2063            .and_then(|models| models.get(&parsed.path))
2064            .map_or_else(
2065                || build_component_graph_for_module(parsed),
2066                |model| build_v2_component_graph_for_module(parsed, model),
2067            );
2068        let template_graph = build_template_graph(&component_graph);
2069        let file_template_entities = build_template_semantic_entities(&template_graph.templates);
2070
2071        components.extend(component_graph.components);
2072        templates.extend(template_graph.templates);
2073        diagnostics.extend(component_graph.diagnostics);
2074        references.extend(component_graph.references);
2075        provenance.extend(component_graph.provenance);
2076        extend_template_entity_provenance(&mut provenance, &file_template_entities);
2077        template_entities.extend(file_template_entities);
2078        type_aliases.extend(
2079            parsed
2080                .type_aliases
2081                .iter()
2082                .cloned()
2083                .map(|alias| (parsed.path.clone(), alias)),
2084        );
2085    }
2086
2087    resolve_builtin_pure_calls(&mut components);
2088
2089    if let Some(bindings) = bindings {
2090        diagnostics.extend(bindings.diagnostics.iter().map(|diagnostic| {
2091            ComponentDiagnostic::error(
2092                diagnostic.code.clone(),
2093                format!("{}: {}", diagnostic.module.display(), diagnostic.message),
2094            )
2095        }));
2096        resolve_semantic_package_pure_calls(&mut components, bindings);
2097    }
2098
2099    let resource_endpoint_resolutions =
2100        collect_resource_endpoint_resolutions(&components, bindings);
2101    diagnostics.extend(collect_resource_lowering_diagnostics(
2102        &resource_endpoint_resolutions,
2103    ));
2104    let opaque_action_resolutions = collect_opaque_action_resolutions(&components, bindings);
2105    diagnostics.extend(collect_opaque_action_lowering_diagnostics(
2106        &opaque_action_resolutions,
2107    ));
2108
2109    let mut component_invocations = collect_component_invocations(
2110        &components,
2111        &templates,
2112        &template_entities,
2113        files,
2114        bindings,
2115        &provenance,
2116    );
2117    let virtual_invocations = match mode {
2118        ApplicationAssemblyMode::Authored => BTreeMap::new(),
2119        ApplicationAssemblyMode::FileRoutes | ApplicationAssemblyMode::FileRoute(_) => {
2120            let mut graph = crate::build_validated_file_route_graph_from_components_v1(&components)
2121                .map_err(|error| FileRouteApplicationModelErrorV1 {
2122                    code: error.code,
2123                    message: error.message,
2124                })?;
2125            if let ApplicationAssemblyMode::FileRoute(selected) = &mode {
2126                graph.routes.retain(|route| route.component == *selected);
2127                if graph.routes.is_empty() {
2128                    return Err(FileRouteApplicationModelErrorV1 {
2129                        code: "PSROUTE1014_FILE_ROUTE_COMPONENT_UNRESOLVED",
2130                        message: format!("component `{selected}` is not a conventional file route"),
2131                    });
2132                }
2133            }
2134            let plan = crate::build_layout_composition_plan_from_components_v1(&components, &graph)
2135                .map_err(|error| FileRouteApplicationModelErrorV1 {
2136                    code: error.code,
2137                    message: error.message,
2138                })?;
2139            crate::layout_composition_virtual_invocations_from_provenance_v1(&plan, &provenance)
2140        }
2141    };
2142    let component_instance_plan = plan_component_instances_with_virtual_invocations(
2143        &components,
2144        &component_invocations,
2145        &virtual_invocations,
2146        &template_entities,
2147        &provenance,
2148    );
2149    component_invocations.extend(virtual_invocations.clone());
2150    let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
2151    let component_composition = analyze_component_composition(
2152        &components
2153            .iter()
2154            .map(|component| component.id.clone())
2155            .collect(),
2156        &component_invocations,
2157        &component_instance_plan,
2158    );
2159
2160    let (computed_values, computed_diagnostics) = classify_computed_values(
2161        &components,
2162        collect_computed_values(&components, &provenance),
2163        &provenance,
2164    );
2165    let effects = collect_effects(&components, &provenance);
2166    let expression_graph = ExpressionGraph::from_components(&components, &provenance);
2167    let contexts = collect_context_entities(&components, &expression_graph);
2168    let forms = collect_form_entities(&components);
2169    let base_semantic_types = SemanticTypeModel::from_components_with_aliases_and_bindings(
2170        &components,
2171        &provenance,
2172        &type_aliases,
2173        bindings,
2174    );
2175    let resource_declarations = collect_resource_declarations(
2176        &components,
2177        &resource_endpoint_resolutions,
2178        &base_semantic_types,
2179        bindings,
2180    );
2181    let resource_activations =
2182        collect_resource_activations(&resource_declarations, &component_instance_plan);
2183    let form_field_products =
2184        collect_form_field_products(&components, &forms, &base_semantic_types, bindings);
2185    let form_field_declaration_candidates = form_field_products.candidates;
2186    let form_fields = form_field_products.fields;
2187    let form_field_binding_products = collect_form_field_binding_products(
2188        &components,
2189        &templates,
2190        &forms,
2191        &form_fields,
2192        &form_field_declaration_candidates,
2193    );
2194    let form_field_binding_candidates = form_field_binding_products.candidates;
2195    let form_field_bindings = form_field_binding_products.bindings;
2196    let slots = collect_slot_entities(&components);
2197    let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
2198    let slot_bindings = collect_slot_bindings_with_virtual_invocations(
2199        &component_instance_plan,
2200        &component_invocations,
2201        &virtual_invocations,
2202        &slots,
2203        &slot_composition.fragments,
2204        &slot_composition.outlets,
2205    );
2206    let (providers, duplicate_provider_declarations) =
2207        collect_provider_entities(&components, &contexts, &expression_graph, bindings);
2208    let consumers = collect_consumer_entities(&components, &contexts, bindings);
2209    let instance_context = collect_instance_context_registry(
2210        &component_instance_scope,
2211        &contexts,
2212        &providers,
2213        &consumers,
2214    );
2215    let context_declaration_candidates =
2216        collect_context_declaration_candidates(&components, &contexts, &providers, &consumers);
2217    let component_scope = ComponentScopeGraph::reflexive(&components);
2218    let context_resolutions = collect_context_resolutions(
2219        &consumers,
2220        &contexts,
2221        &providers,
2222        &expression_graph,
2223        &component_scope,
2224    );
2225    diagnostics.extend(computed_diagnostics);
2226    extend_derived_entity_provenance(
2227        &mut provenance,
2228        &contexts,
2229        &forms,
2230        &form_fields,
2231        &form_field_bindings,
2232        &providers,
2233        &consumers,
2234        &slots,
2235        &component_invocations,
2236        &slot_composition.fragments,
2237        &slot_composition.outlets,
2238        &component_instance_plan,
2239        &computed_values,
2240        &effects,
2241    );
2242    let mut ownership = collect_ownership(
2243        &components,
2244        &contexts,
2245        &forms,
2246        &form_fields,
2247        &form_field_bindings,
2248        &providers,
2249        &consumers,
2250        &slots,
2251        &component_invocations,
2252        &slot_composition.fragments,
2253        &slot_composition.outlets,
2254        &component_instance_plan,
2255        &computed_values,
2256        &effects,
2257        &templates,
2258        &template_entities,
2259    );
2260    let context_ownership = collect_context_ownership_graph(
2261        &components,
2262        &contexts,
2263        &providers,
2264        &consumers,
2265        &expression_graph,
2266        &provenance,
2267    );
2268    let (effect_bodies, effect_statements) =
2269        lower_effect_bodies(&components, &effects, &expression_graph);
2270    references.extend(build_computed_references(
2271        &components,
2272        &computed_values,
2273        &resource_declarations,
2274        &expression_graph,
2275    ));
2276    references.extend(build_effect_references(
2277        &components,
2278        &effects,
2279        &computed_values,
2280        &expression_graph,
2281    ));
2282    references.extend(build_provider_references(&providers));
2283    references.extend(build_consumer_references(&consumers));
2284    references.extend(build_context_resolution_references(&context_resolutions));
2285    extend_template_references(
2286        &mut references,
2287        &components,
2288        &computed_values,
2289        &template_entities,
2290        &ownership,
2291    );
2292    references.extend(build_form_field_binding_references(&form_field_bindings));
2293    let form_ownership = collect_form_ownership_graph(
2294        &component_instance_plan.roots,
2295        &components,
2296        &forms,
2297        &form_fields,
2298        &form_field_bindings,
2299        &template_entities,
2300        &ownership,
2301        &references,
2302        &provenance,
2303    );
2304    let validation_products = collect_validation_products(&components, &forms, &form_fields);
2305    let validation_rule_candidates = validation_products.candidates;
2306    let validation_rules = validation_products.rules;
2307    extend_validation_products(
2308        &mut provenance,
2309        &mut ownership,
2310        &mut references,
2311        &validation_rules,
2312    );
2313    let validation_graph = collect_validation_graph(
2314        &component_instance_plan.roots,
2315        &form_ownership,
2316        &forms,
2317        &form_fields,
2318        &validation_rules,
2319        &validation_rule_candidates,
2320        &validation_products.cycles,
2321        &ownership,
2322        &references,
2323    );
2324    let validation_dependency_plans = collect_validation_dependency_plans(
2325        &forms,
2326        &form_fields,
2327        &validation_rules,
2328        &form_ownership,
2329        &validation_graph,
2330    );
2331    let form_tracking =
2332        collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
2333    let semantic_types = finalize_semantic_types(
2334        base_semantic_types.with_resource_types(&resource_declarations),
2335        &components,
2336        &contexts,
2337        &forms,
2338        &form_fields,
2339        &providers,
2340        &consumers,
2341        &slots,
2342        &computed_values,
2343        &effects,
2344        &effect_statements,
2345        &expression_graph,
2346        &references,
2347        &template_entities,
2348    );
2349    let context_type_products = collect_context_type_products(
2350        &contexts,
2351        &providers,
2352        &consumers,
2353        &context_resolutions,
2354        &expression_graph,
2355        &semantic_types,
2356    );
2357    let context_dependency = collect_context_dependency_graph(
2358        &components,
2359        &contexts,
2360        &providers,
2361        &consumers,
2362        &context_resolutions,
2363        &context_type_products.contexts,
2364        &context_type_products.providers,
2365        &context_type_products.bindings,
2366        &computed_values,
2367        &expression_graph,
2368    );
2369    let context_lifetime = collect_context_lifetime_analysis(
2370        &components,
2371        &contexts,
2372        &providers,
2373        &consumers,
2374        &computed_values,
2375        &context_ownership,
2376        &component_scope,
2377        &context_resolutions,
2378        &context_dependency,
2379        &provenance,
2380    );
2381    let effects = validate_effects(&components, effects, &effect_statements, &semantic_types);
2382    let (
2383        reactive_graph,
2384        reactive_transitive_analysis,
2385        reactive_cycle_analysis,
2386        computed_evaluation_plan,
2387    ) = build_computed_reactive_products(
2388        &components,
2389        &computed_values,
2390        &effects,
2391        &resource_declarations,
2392        &references,
2393        &provenance,
2394    );
2395    let context_evaluation = collect_context_evaluation_plan(
2396        &contexts,
2397        &providers,
2398        &context_resolutions,
2399        &context_type_products.contexts,
2400        &context_type_products.providers,
2401        &context_type_products.bindings,
2402        &context_lifetime,
2403        &context_dependency,
2404        &computed_evaluation_plan,
2405        &component_scope,
2406    );
2407    let effect_reactive_analysis = analyze_effect_reactivity(
2408        &components,
2409        &computed_values,
2410        &effects,
2411        &reactive_transitive_analysis,
2412    );
2413    let effect_trigger_plan = derive_effect_trigger_plan(
2414        &components,
2415        &effects,
2416        &effect_reactive_analysis,
2417        &provenance,
2418    );
2419    let submissions = collect_submission_products(
2420        &components,
2421        &forms,
2422        &form_fields,
2423        &validation_rules,
2424        &effect_trigger_plan,
2425        bindings,
2426        None,
2427    );
2428    let serialization =
2429        collect_serialization_products(&components, &forms, &form_fields, &submissions.plans);
2430    let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
2431    let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
2432    let optimized_form_ir = optimize_form_ir(&form_ir);
2433    let submission_host_products = collect_submission_host_products(
2434        &templates,
2435        &components,
2436        &forms,
2437        &form_field_bindings,
2438        &submissions.plans,
2439        &serialization.plans,
2440        &optimized_form_ir.optimized,
2441    );
2442    let runtime_forms = build_runtime_form_registry(
2443        &forms,
2444        &form_fields,
2445        &form_field_bindings,
2446        &validation_rules,
2447        &optimized_form_ir.optimized,
2448        &submissions,
2449        &serialization,
2450        &reset,
2451        &submission_host_products.hosts,
2452    );
2453    let effect_execution_plan = plan_effect_execution(
2454        &computed_values,
2455        &effects,
2456        &effect_reactive_analysis,
2457        &effect_trigger_plan,
2458        &reactive_transitive_analysis,
2459        &computed_evaluation_plan,
2460    );
2461    extend_computed_diagnostics(
2462        &mut diagnostics,
2463        &components,
2464        &computed_values,
2465        &resource_declarations,
2466        &expression_graph,
2467        &semantic_types,
2468        &reactive_cycle_analysis,
2469        &provenance,
2470    );
2471    let component_ids = components
2472        .iter()
2473        .map(|component| component.id.clone())
2474        .collect::<BTreeSet<_>>();
2475    let composition_types = collect_composition_type_products(
2476        &component_ids,
2477        &component_invocations,
2478        &slot_bindings,
2479        &slots,
2480        &slot_composition.fragments,
2481        &slot_composition.outlets,
2482        &instance_context,
2483        &context_type_products.bindings,
2484        &context_type_products.contexts,
2485        &context_type_products.providers,
2486        &context_lifetime,
2487        &ownership,
2488        &references,
2489    );
2490    let component_initialization = plan_component_initialization(
2491        &component_instance_plan,
2492        &slot_bindings,
2493        &composition_types,
2494        &instance_context,
2495    );
2496    let components = components_with_resolved_form_field_candidates(
2497        &components,
2498        &form_field_declaration_candidates,
2499    );
2500    let mut model = ApplicationSemanticModel {
2501        expression_graph,
2502        semantic_types,
2503        components,
2504        contexts,
2505        providers,
2506        consumers,
2507        forms,
2508        resource_endpoint_resolutions,
2509        opaque_action_resolutions,
2510        resource_declarations,
2511        resource_activations,
2512        form_field_declaration_candidates,
2513        form_fields,
2514        form_field_binding_candidates,
2515        form_field_bindings,
2516        form_ownership,
2517        validation_rule_candidates,
2518        validation_rules,
2519        validation_graph,
2520        validation_dependency_plans,
2521        form_tracking,
2522        submissions,
2523        submission_host_candidates: submission_host_products.candidates,
2524        submission_hosts: submission_host_products.hosts,
2525        serialization,
2526        reset,
2527        form_ir,
2528        optimized_form_ir,
2529        runtime_forms,
2530        slots,
2531        component_invocations,
2532        component_instance_plan,
2533        component_instance_scope,
2534        component_composition,
2535        component_initialization,
2536        component_ir: ComponentIrReport::default(),
2537        component_ir_optimization: OptimizedComponentIrReport::default(),
2538        instance_context,
2539        slot_content_fragments: slot_composition.fragments,
2540        slot_outlets: slot_composition.outlets,
2541        slot_bindings,
2542        composition_types,
2543        context_declaration_candidates,
2544        context_ownership,
2545        context_dependency,
2546        context_lifetime,
2547        context_evaluation,
2548        component_scope,
2549        context_resolutions,
2550        context_types: context_type_products.contexts,
2551        provider_types: context_type_products.providers,
2552        consumer_types: context_type_products.consumers,
2553        context_binding_types: context_type_products.bindings,
2554        duplicate_provider_declarations,
2555        computed_values,
2556        effects,
2557        effect_reactive_analysis,
2558        effect_trigger_plan,
2559        effect_execution_plan,
2560        effect_bodies,
2561        effect_statements,
2562        reactive_graph,
2563        reactive_transitive_analysis,
2564        reactive_cycle_analysis,
2565        computed_evaluation_plan,
2566        templates,
2567        template_entities,
2568        diagnostics,
2569        ownership,
2570        references,
2571        provenance,
2572    };
2573    model
2574        .diagnostics
2575        .extend(crate::collect_effect_diagnostics(&model));
2576    model
2577        .diagnostics
2578        .extend(crate::collect_context_diagnostics(&model));
2579    model
2580        .diagnostics
2581        .extend(crate::collect_form_diagnostics(&model));
2582    model.component_ir = lower_component_ir(&model);
2583    model.component_ir_optimization = optimize_component_ir(&model.component_ir);
2584    model
2585        .diagnostics
2586        .extend(crate::collect_component_diagnostics(&model));
2587    Ok(model)
2588}
2589
2590fn resolve_builtin_pure_calls(components: &mut [ComponentNode]) {
2591    for component in components {
2592        for method in component
2593            .methods
2594            .iter_mut()
2595            .filter(|method| method.is_computed())
2596        {
2597            if let Some(expression) = method.computed_expression.as_mut() {
2598                resolve_builtin_pure_call(expression);
2599            }
2600        }
2601    }
2602}
2603
2604fn resolve_builtin_pure_call(expression: &mut ComputedExpression) {
2605    match &mut expression.kind {
2606        ComputedExpressionKind::Call { callee, arguments } => {
2607            for argument in arguments.iter_mut() {
2608                resolve_builtin_pure_call(argument);
2609            }
2610            let operation = match (callee.as_str(), arguments.len()) {
2611                ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
2612                ("Math.floor", 1) => Some(crate::component_graph::BuiltinPureOperation::MathFloor),
2613                ("Math.ceil", 1) => Some(crate::component_graph::BuiltinPureOperation::MathCeil),
2614                ("Math.round", 1) => Some(crate::component_graph::BuiltinPureOperation::MathRound),
2615                ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
2616                ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
2617                _ => None,
2618            };
2619            if let Some(operation) = operation {
2620                expression.kind = ComputedExpressionKind::BuiltinPureCall {
2621                    operation,
2622                    arguments: std::mem::take(arguments),
2623                };
2624            }
2625        }
2626        ComputedExpressionKind::BuiltinPureCall { arguments, .. }
2627        | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
2628            for argument in arguments {
2629                resolve_builtin_pure_call(argument);
2630            }
2631        }
2632        ComputedExpressionKind::Template { expressions, .. } => {
2633            for expression in expressions {
2634                resolve_builtin_pure_call(expression);
2635            }
2636        }
2637        ComputedExpressionKind::MemberAccess { object, .. }
2638        | ComputedExpressionKind::Unary {
2639            operand: object, ..
2640        } => {
2641            resolve_builtin_pure_call(object);
2642        }
2643        ComputedExpressionKind::IndexAccess { object, index } => {
2644            resolve_builtin_pure_call(object);
2645            resolve_builtin_pure_call(index);
2646        }
2647        ComputedExpressionKind::Conditional {
2648            condition,
2649            when_true,
2650            when_false,
2651        } => {
2652            resolve_builtin_pure_call(condition);
2653            resolve_builtin_pure_call(when_true);
2654            resolve_builtin_pure_call(when_false);
2655        }
2656        ComputedExpressionKind::Arithmetic { left, right, .. }
2657        | ComputedExpressionKind::Comparison { left, right, .. }
2658        | ComputedExpressionKind::Logical { left, right, .. }
2659        | ComputedExpressionKind::NullishCoalescing { left, right } => {
2660            resolve_builtin_pure_call(left);
2661            resolve_builtin_pure_call(right);
2662        }
2663        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
2664    }
2665}
2666
2667fn components_with_resolved_form_field_candidates(
2668    components: &[ComponentNode],
2669    candidates: &[crate::FormFieldDeclarationCandidate],
2670) -> Vec<ComponentNode> {
2671    let candidates = candidates
2672        .iter()
2673        .map(|candidate| (candidate.id.clone(), candidate))
2674        .collect::<BTreeMap<_, _>>();
2675    let mut components = components.to_vec();
2676    for component in &mut components {
2677        for candidate in &mut component.form_field_declaration_candidates {
2678            if let Some(resolved) = candidates.get(&candidate.id) {
2679                *candidate = (*resolved).clone();
2680            }
2681        }
2682    }
2683    components
2684}
2685
2686fn extend_template_entity_provenance(
2687    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2688    template_entities: &[TemplateSemanticEntity],
2689) {
2690    provenance.extend(
2691        template_entities
2692            .iter()
2693            .map(|entity| (entity.id.clone(), entity.provenance.clone())),
2694    );
2695}
2696
2697fn extend_validation_products(
2698    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2699    ownership: &mut BTreeMap<SemanticId, SemanticOwner>,
2700    references: &mut Vec<SemanticReference>,
2701    rules: &BTreeMap<ValidationRuleId, ValidationRule>,
2702) {
2703    for rule in rules.values() {
2704        provenance.insert(rule.id.as_semantic_id().clone(), rule.provenance.clone());
2705        ownership.insert(
2706            rule.id.as_semantic_id().clone(),
2707            SemanticOwner::entity(rule.target_field.as_semantic_id().clone()),
2708        );
2709        if let Some(dependency) = &rule.dependency {
2710            references.push(SemanticReference {
2711                kind: SemanticReferenceKind::ValidationRuleField,
2712                source: rule.id.as_semantic_id().clone(),
2713                target: dependency.as_semantic_id().clone(),
2714                provenance: rule
2715                    .argument_provenance
2716                    .clone()
2717                    .unwrap_or_else(|| rule.decorator_provenance.clone()),
2718            });
2719        }
2720    }
2721    references.sort_by(|left, right| {
2722        (left.source.as_str(), left.kind, left.target.as_str()).cmp(&(
2723            right.source.as_str(),
2724            right.kind,
2725            right.target.as_str(),
2726        ))
2727    });
2728    references.dedup();
2729}
2730
2731#[allow(clippy::too_many_arguments)]
2732fn extend_derived_entity_provenance(
2733    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2734    contexts: &BTreeMap<ContextId, ContextEntity>,
2735    forms: &BTreeMap<FormId, FormEntity>,
2736    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2737    form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
2738    providers: &BTreeMap<ProviderId, ProviderEntity>,
2739    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2740    slots: &BTreeMap<SlotId, SlotEntity>,
2741    component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
2742    slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
2743    slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
2744    component_instance_plan: &ComponentInstancePlan,
2745    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2746    effects: &BTreeMap<SemanticId, Effect>,
2747) {
2748    provenance.extend(contexts.values().map(|context| {
2749        (
2750            context.id.as_semantic_id().clone(),
2751            context.provenance.clone(),
2752        )
2753    }));
2754    provenance.extend(
2755        forms
2756            .values()
2757            .map(|form| (form.id.as_semantic_id().clone(), form.provenance.clone())),
2758    );
2759    provenance.extend(
2760        form_fields
2761            .values()
2762            .map(|field| (field.id.as_semantic_id().clone(), field.provenance.clone())),
2763    );
2764    provenance.extend(form_field_bindings.values().map(|binding| {
2765        (
2766            binding.id.as_semantic_id().clone(),
2767            binding.provenance.clone(),
2768        )
2769    }));
2770    provenance.extend(providers.values().map(|provider| {
2771        (
2772            provider.id.as_semantic_id().clone(),
2773            provider.provenance.clone(),
2774        )
2775    }));
2776    provenance.extend(consumers.values().map(|consumer| {
2777        (
2778            consumer.id.as_semantic_id().clone(),
2779            consumer.provenance.clone(),
2780        )
2781    }));
2782    provenance.extend(
2783        slots
2784            .values()
2785            .map(|slot| (slot.id.as_semantic_id().clone(), slot.provenance.clone())),
2786    );
2787    provenance.extend(component_invocations.values().map(|invocation| {
2788        (
2789            invocation.id.as_semantic_id().clone(),
2790            invocation.provenance.clone(),
2791        )
2792    }));
2793    provenance.extend(slot_content_fragments.values().map(|fragment| {
2794        (
2795            fragment.id.as_semantic_id().clone(),
2796            fragment.provenance.clone(),
2797        )
2798    }));
2799    provenance.extend(slot_outlets.values().map(|outlet| {
2800        (
2801            outlet.id.as_semantic_id().clone(),
2802            outlet.provenance.clone(),
2803        )
2804    }));
2805    provenance.extend(component_instance_plan.instances.values().map(|instance| {
2806        (
2807            instance.id.as_semantic_id().clone(),
2808            instance.provenance.clone(),
2809        )
2810    }));
2811    provenance.extend(component_instance_plan.blocked.values().map(|blocked| {
2812        (
2813            blocked.id.as_semantic_id().clone(),
2814            blocked.provenance.clone(),
2815        )
2816    }));
2817    provenance.extend(
2818        computed_values
2819            .iter()
2820            .map(|(id, computed)| (id.clone(), computed.provenance.clone())),
2821    );
2822    provenance.extend(
2823        effects
2824            .iter()
2825            .map(|(id, effect)| (id.clone(), effect.provenance.clone())),
2826    );
2827}
2828
2829#[allow(clippy::too_many_arguments)]
2830fn finalize_semantic_types(
2831    semantic_types: SemanticTypeModel,
2832    components: &[ComponentNode],
2833    contexts: &BTreeMap<ContextId, ContextEntity>,
2834    forms: &BTreeMap<FormId, FormEntity>,
2835    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2836    providers: &BTreeMap<ProviderId, ProviderEntity>,
2837    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2838    slots: &BTreeMap<SlotId, SlotEntity>,
2839    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2840    effects: &BTreeMap<SemanticId, Effect>,
2841    effect_statements: &BTreeMap<SemanticId, EffectStatement>,
2842    expression_graph: &ExpressionGraph,
2843    references: &[SemanticReference],
2844    template_entities: &[TemplateSemanticEntity],
2845) -> SemanticTypeModel {
2846    semantic_types
2847        .with_slot_types(slots)
2848        .with_form_types(forms)
2849        .with_form_field_types(form_fields)
2850        .with_context_types(contexts)
2851        .with_provider_types(providers)
2852        .with_consumer_types(consumers)
2853        .with_expression_types(expression_graph, components, contexts, providers)
2854        .with_computed_value_types(components, computed_values, expression_graph, references)
2855        .with_context_source_expression_types(components, contexts, providers, expression_graph)
2856        .with_effect_statement_types(components, effects, effect_statements, expression_graph)
2857        .with_template_binding_types(template_entities, references)
2858        .normalized()
2859}
2860
2861#[allow(clippy::too_many_arguments)]
2862fn extend_computed_diagnostics(
2863    diagnostics: &mut Vec<ComponentDiagnostic>,
2864    components: &[ComponentNode],
2865    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2866    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
2867    expression_graph: &ExpressionGraph,
2868    semantic_types: &SemanticTypeModel,
2869    reactive_cycle_analysis: &IrReactiveCycleAnalysis,
2870    provenance: &BTreeMap<SemanticId, SourceProvenance>,
2871) {
2872    diagnostics.extend(collect_computed_cycle_diagnostics(
2873        reactive_cycle_analysis,
2874        computed_values,
2875    ));
2876    diagnostics.extend(collect_computed_semantic_diagnostics(
2877        components,
2878        computed_values,
2879        resource_declarations,
2880        expression_graph,
2881        semantic_types,
2882        provenance,
2883    ));
2884}
2885
2886#[allow(clippy::too_many_lines)]
2887fn classify_computed_values(
2888    components: &[ComponentNode],
2889    mut computed_values: BTreeMap<SemanticId, ComputedValue>,
2890    provenance: &BTreeMap<SemanticId, SourceProvenance>,
2891) -> (
2892    BTreeMap<SemanticId, ComputedValue>,
2893    Vec<ComponentDiagnostic>,
2894) {
2895    let mut diagnostics = Vec::new();
2896
2897    for computed in computed_values.values_mut() {
2898        let Some(component_id) = computed.owner.entity_id() else {
2899            continue;
2900        };
2901        let Some(component) = components
2902            .iter()
2903            .find(|component| component.id == *component_id)
2904        else {
2905            continue;
2906        };
2907        let Some(method) = component
2908            .methods
2909            .iter()
2910            .find(|method| method.id == computed.method)
2911        else {
2912            continue;
2913        };
2914        let mut violations = Vec::new();
2915
2916        if method.is_async {
2917            violations.push(crate::ComputedPurityViolation {
2918                kind: crate::ComputedPurityViolationKind::Async,
2919                provenance: computed.provenance.clone(),
2920            });
2921        }
2922        if method
2923            .decorators
2924            .iter()
2925            .any(|decorator| decorator == "action")
2926        {
2927            violations.push(crate::ComputedPurityViolation {
2928                kind: crate::ComputedPurityViolationKind::Action,
2929                provenance: computed.provenance.clone(),
2930            });
2931        }
2932        if method
2933            .decorators
2934            .iter()
2935            .any(|decorator| decorator == "effect")
2936        {
2937            violations.push(crate::ComputedPurityViolation {
2938                kind: crate::ComputedPurityViolationKind::Effect,
2939                provenance: computed.provenance.clone(),
2940            });
2941        }
2942        for action in component
2943            .actions
2944            .iter()
2945            .filter(|action| action.method == method.name)
2946        {
2947            violations.push(crate::ComputedPurityViolation {
2948                kind: crate::ComputedPurityViolationKind::StateMutation,
2949                provenance: provenance
2950                    .get(&action.id)
2951                    .expect("computed state update should have canonical provenance")
2952                    .clone(),
2953            });
2954        }
2955        for call in &method.calls {
2956            if method
2957                .computed_expression
2958                .as_ref()
2959                .is_some_and(|expression| {
2960                    contains_semantic_package_pure_call(expression, &call.callee)
2961                        || contains_builtin_pure_call(expression, &call.callee)
2962                })
2963            {
2964                continue;
2965            }
2966            let kind = computed_call_purity_kind(component, &call.callee);
2967            violations.push(crate::ComputedPurityViolation {
2968                kind,
2969                provenance: SourceProvenance::new(&computed.provenance.path, call.span),
2970            });
2971        }
2972
2973        violations.sort_by(|left, right| {
2974            (
2975                left.kind,
2976                left.provenance.path.as_path(),
2977                left.provenance.span.start,
2978                left.provenance.span.end,
2979            )
2980                .cmp(&(
2981                    right.kind,
2982                    right.provenance.path.as_path(),
2983                    right.provenance.span.start,
2984                    right.provenance.span.end,
2985                ))
2986        });
2987        violations
2988            .dedup_by(|left, right| left.kind == right.kind && left.provenance == right.provenance);
2989        computed.purity = if violations.is_empty() {
2990            crate::ComputedPurity::Pure
2991        } else {
2992            crate::ComputedPurity::Impure
2993        };
2994        computed.purity_violations = violations;
2995        diagnostics.extend(computed.purity_violations.iter().map(|violation| {
2996            ComponentDiagnostic {
2997                severity: ComponentDiagnosticSeverity::Error,
2998                effect_id: None,
2999                statement_id: None,
3000                context_declaration_candidate_id: None,
3001                context_id: None,
3002                provider_id: None,
3003                consumer_id: None,
3004                slot_id: None,
3005                invocation_id: None,
3006                component_instance_id: None,
3007                slot_binding_id: None,
3008                structural_region_id: None,
3009                component_id: None,
3010                provider_instance_id: None,
3011                consumer_instance_id: None,
3012                secondary_labels: Vec::new(),
3013                code: ComputedDiagnosticCode::PurityViolation.as_str().to_string(),
3014                message: format!(
3015                    "computed getter `{}` is impure: {}",
3016                    computed.name,
3017                    violation.kind.description()
3018                ),
3019                provenance: Some(violation.provenance.clone()),
3020            }
3021        }));
3022    }
3023
3024    (computed_values, diagnostics)
3025}
3026
3027fn resolve_semantic_package_pure_calls(
3028    components: &mut [ComponentNode],
3029    bindings: &crate::BindingTable,
3030) {
3031    for component in components {
3032        for method in component
3033            .methods
3034            .iter_mut()
3035            .filter(|method| method.is_computed())
3036        {
3037            let Some(expression) = method.computed_expression.as_mut() else {
3038                continue;
3039            };
3040            resolve_semantic_package_pure_call(expression, &component.module_path, bindings);
3041        }
3042    }
3043}
3044
3045fn collect_resource_endpoint_resolutions(
3046    components: &[ComponentNode],
3047    bindings: Option<&crate::BindingTable>,
3048) -> Vec<crate::ResourceEndpointResolution> {
3049    let mut resolutions = Vec::new();
3050    for component in components {
3051        for resource in &component.resource_declaration_candidates {
3052            let outcome = match resource.endpoint_designator.as_deref() {
3053                None => crate::ResourceEndpointResolutionOutcome::MissingDesignator,
3054                Some(designator) => {
3055                    let Some(bindings) = bindings else {
3056                        resolutions.push(crate::ResourceEndpointResolution {
3057                            owner_component: resource.owner_component.clone(),
3058                            field: resource.field.clone(),
3059                            endpoint_designator: resource.endpoint_designator.clone(),
3060                            outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
3061                                designator: designator.to_string(),
3062                            },
3063                            provenance: resource.provenance.clone(),
3064                        });
3065                        continue;
3066                    };
3067                    let Some(binding) = bindings.resolve_import(&component.module_path, designator)
3068                    else {
3069                        resolutions.push(crate::ResourceEndpointResolution {
3070                            owner_component: resource.owner_component.clone(),
3071                            field: resource.field.clone(),
3072                            endpoint_designator: resource.endpoint_designator.clone(),
3073                            outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
3074                                designator: designator.to_string(),
3075                            },
3076                            provenance: resource.provenance.clone(),
3077                        });
3078                        continue;
3079                    };
3080                    match &binding.target {
3081                        crate::ImportBindingTarget::SemanticPackage {
3082                            package,
3083                            version,
3084                            integrity,
3085                            export,
3086                            kind: crate::SemanticPackageKind::Resource,
3087                            type_signature,
3088                            runtime_module,
3089                            resume_policy,
3090                            resource_endpoint: Some(endpoint),
3091                            ..
3092                        } => crate::ResourceEndpointResolutionOutcome::Resolved(
3093                            crate::ResourceEndpointBinding {
3094                                local_name: binding.local_name.clone(),
3095                                package: package.clone(),
3096                                version: version.clone(),
3097                                integrity: integrity.clone(),
3098                                export: export.clone(),
3099                                type_signature: type_signature.clone(),
3100                                runtime_module: runtime_module.clone(),
3101                                resume_policy: resume_policy.clone(),
3102                                endpoint: endpoint.clone(),
3103                            },
3104                        ),
3105                        crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3106                            crate::ResourceEndpointResolutionOutcome::NonResourceBinding {
3107                                designator: designator.to_string(),
3108                                kind: kind.clone(),
3109                            }
3110                        }
3111                        _ => crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding {
3112                            designator: designator.to_string(),
3113                        },
3114                    }
3115                }
3116            };
3117            resolutions.push(crate::ResourceEndpointResolution {
3118                owner_component: resource.owner_component.clone(),
3119                field: resource.field.clone(),
3120                endpoint_designator: resource.endpoint_designator.clone(),
3121                outcome,
3122                provenance: resource.provenance.clone(),
3123            });
3124        }
3125    }
3126    resolutions
3127}
3128
3129fn collect_opaque_action_resolutions(
3130    components: &[ComponentNode],
3131    bindings: Option<&crate::BindingTable>,
3132) -> Vec<crate::OpaqueActionResolution> {
3133    let mut resolutions = Vec::new();
3134    for component in components {
3135        for fact in component
3136            .opaque_action_facts
3137            .iter()
3138            .filter(|fact| crate::component_graph::is_valid_opaque_action_fact(fact))
3139        {
3140            let (Some(owner_component), Some(method), Some(package_specifier), Some(export_name)) = (
3141                fact.owner_component.as_ref(),
3142                fact.method.as_ref(),
3143                fact.package.as_ref(),
3144                fact.export.as_ref(),
3145            ) else {
3146                continue;
3147            };
3148            let outcome = bindings
3149                .and_then(|bindings| bindings.module(&component.module_path))
3150                .and_then(|module| {
3151                    module.imports.values().find(|binding| {
3152                        binding.source_module == *package_specifier
3153                            && binding.imported_name == *export_name
3154                    })
3155                })
3156                .map_or_else(
3157                    || crate::OpaqueActionResolutionOutcome::UnboundImport {
3158                        package: package_specifier.clone(),
3159                        export: export_name.clone(),
3160                    },
3161                    |binding| match &binding.target {
3162                        crate::ImportBindingTarget::SemanticPackage {
3163                            package,
3164                            version,
3165                            integrity,
3166                            export,
3167                            kind: crate::SemanticPackageKind::Opaque,
3168                            type_signature,
3169                            runtime_module,
3170                            resume_policy,
3171                            opaque_terminal: Some(terminal),
3172                            ..
3173                        } => crate::OpaqueActionResolutionOutcome::Resolved(
3174                            crate::OpaqueTerminalBinding {
3175                                local_name: binding.local_name.clone(),
3176                                package: package.clone(),
3177                                version: version.clone(),
3178                                integrity: integrity.clone(),
3179                                export: export.clone(),
3180                                type_signature: type_signature.clone(),
3181                                runtime_module: runtime_module.clone(),
3182                                resume_policy: resume_policy.clone(),
3183                                terminal: terminal.clone(),
3184                            },
3185                        ),
3186                        crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3187                            crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3188                                package: package_specifier.clone(),
3189                                export: export_name.clone(),
3190                                kind: kind.clone(),
3191                            }
3192                        }
3193                        _ => crate::OpaqueActionResolutionOutcome::UnboundImport {
3194                            package: package_specifier.clone(),
3195                            export: export_name.clone(),
3196                        },
3197                    },
3198                );
3199            resolutions.push(crate::OpaqueActionResolution {
3200                activation: fact.id.clone(),
3201                owner_component: owner_component.clone(),
3202                method: method.clone(),
3203                method_name: fact.method_name.clone(),
3204                package_specifier: package_specifier.clone(),
3205                export_name: export_name.clone(),
3206                outcome,
3207                provenance: fact.provenance.clone(),
3208            });
3209        }
3210    }
3211    resolutions.sort_by(|left, right| left.activation.cmp(&right.activation));
3212    resolutions
3213}
3214
3215fn collect_opaque_action_lowering_diagnostics(
3216    resolutions: &[crate::OpaqueActionResolution],
3217) -> Vec<ComponentDiagnostic> {
3218    resolutions
3219        .iter()
3220        .filter_map(|resolution| {
3221            let message = match &resolution.outcome {
3222                crate::OpaqueActionResolutionOutcome::Resolved(_) => return None,
3223                crate::OpaqueActionResolutionOutcome::UnboundImport { package, export } => format!(
3224                    "opaque Action `{}` requires an imported opaque semantic-package export `{package}` / `{export}`",
3225                    resolution.method_name
3226                ),
3227                crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3228                    package,
3229                    export,
3230                    kind,
3231                } => format!(
3232                    "opaque Action `{}` selects `{package}` / `{export}` with package kind {kind:?}, not opaque",
3233                    resolution.method_name
3234                ),
3235            };
3236            let mut diagnostic = ComponentDiagnostic::error("PSC1131", message);
3237            diagnostic.provenance = Some(resolution.provenance.clone());
3238            Some(diagnostic)
3239        })
3240        .collect()
3241}
3242
3243fn collect_resource_declarations(
3244    components: &[ComponentNode],
3245    resolutions: &[crate::ResourceEndpointResolution],
3246    semantic_types: &SemanticTypeModel,
3247    bindings: Option<&crate::BindingTable>,
3248) -> BTreeMap<ResourceId, crate::ResourceDeclaration> {
3249    let mut declarations = BTreeMap::new();
3250    for component in components {
3251        for resource in &component.resource_declaration_candidates {
3252            let Some(resolution) = resolutions.iter().find(|resolution| {
3253                resolution.owner_component == resource.owner_component
3254                    && resolution.field == resource.field
3255            }) else {
3256                continue;
3257            };
3258            let crate::ResourceEndpointResolutionOutcome::Resolved(endpoint) = &resolution.outcome
3259            else {
3260                continue;
3261            };
3262            let Some(declared_type) = resource.declared_type.as_ref() else {
3263                continue;
3264            };
3265            let Some(resolved_type) = semantic_types.resolve_declared_type(declared_type, bindings)
3266            else {
3267                continue;
3268            };
3269            let crate::SemanticType::Resource(resource_type) = resolved_type.semantic_type else {
3270                continue;
3271            };
3272            let execution_boundary = match endpoint.endpoint.execution_boundary {
3273                crate::SemanticPackageResourceExecutionBoundary::Client => {
3274                    crate::ResourceExecutionBoundary::Client
3275                }
3276                crate::SemanticPackageResourceExecutionBoundary::Server => {
3277                    crate::ResourceExecutionBoundary::Server
3278                }
3279                crate::SemanticPackageResourceExecutionBoundary::Shared => {
3280                    crate::ResourceExecutionBoundary::Shared
3281                }
3282            };
3283            let key = format!("{}:{}", component.id.as_str(), resource.field);
3284            let declaration = crate::ResourceDeclaration::new(
3285                resource.owner_component.clone(),
3286                resource.field.clone(),
3287                key,
3288                *resource_type.data,
3289                *resource_type.error,
3290                execution_boundary,
3291                BTreeSet::new(),
3292                crate::ResourceRetryPolicy::ExplicitOnly,
3293                crate::ResourceInvalidationPolicy::ExplicitOnly,
3294                resource.provenance.clone(),
3295            );
3296            let Ok(declaration) = declaration else {
3297                continue;
3298            };
3299            declarations.insert(declaration.id.clone(), declaration);
3300        }
3301    }
3302    declarations
3303}
3304
3305fn collect_resource_lowering_diagnostics(
3306    resolutions: &[crate::ResourceEndpointResolution],
3307) -> Vec<ComponentDiagnostic> {
3308    resolutions
3309        .iter()
3310        .filter(|resolution| {
3311            !matches!(
3312                resolution.outcome,
3313                crate::ResourceEndpointResolutionOutcome::Resolved(_)
3314            )
3315        })
3316        .map(|resolution| {
3317            let message = match &resolution.outcome {
3318                crate::ResourceEndpointResolutionOutcome::MissingDesignator => format!(
3319                    "resource declaration `{}` requires one imported semantic-package resource endpoint designator",
3320                    resolution.field
3321                ),
3322                crate::ResourceEndpointResolutionOutcome::UnboundDesignator { designator } => format!(
3323                    "resource declaration `{}` designator `{designator}` does not resolve to an imported semantic-package endpoint",
3324                    resolution.field
3325                ),
3326                crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding { designator } => format!(
3327                    "resource declaration `{}` designator `{designator}` must bind a semantic-package resource export",
3328                    resolution.field
3329                ),
3330                crate::ResourceEndpointResolutionOutcome::NonResourceBinding { designator, kind } => format!(
3331                    "resource declaration `{}` designator `{designator}` resolves to package kind {kind:?}, not resource",
3332                    resolution.field
3333                ),
3334                crate::ResourceEndpointResolutionOutcome::Resolved(_) => unreachable!(
3335                    "resolved resource endpoints are valid N6-C13 lowering inputs"
3336                ),
3337            };
3338            let mut diagnostic = ComponentDiagnostic::error("PSC1128", message);
3339            diagnostic.provenance = Some(resolution.provenance.clone());
3340            diagnostic
3341        })
3342        .collect()
3343}
3344
3345fn collect_resource_activations(
3346    declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3347    instances: &ComponentInstancePlan,
3348) -> BTreeMap<ResourceActivationId, crate::ResourceActivation> {
3349    let mut activations = BTreeMap::new();
3350    for instance in instances.instances.values() {
3351        for declaration in declarations
3352            .values()
3353            .filter(|declaration| declaration.owner_component == instance.component)
3354        {
3355            let activation = declaration.activation_for(instance.id.clone());
3356            activations.insert(activation.id.clone(), activation);
3357        }
3358    }
3359    activations
3360}
3361
3362fn resolve_semantic_package_pure_call(
3363    expression: &mut ComputedExpression,
3364    module_path: &Path,
3365    bindings: &crate::BindingTable,
3366) {
3367    match &mut expression.kind {
3368        ComputedExpressionKind::Call { callee, arguments } => {
3369            for argument in arguments.iter_mut() {
3370                resolve_semantic_package_pure_call(argument, module_path, bindings);
3371            }
3372            let Some(binding) = bindings.resolve_import(module_path, callee) else {
3373                let operation = match (callee.as_str(), arguments.len()) {
3374                    ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
3375                    ("Math.floor", 1) => {
3376                        Some(crate::component_graph::BuiltinPureOperation::MathFloor)
3377                    }
3378                    ("Math.ceil", 1) => {
3379                        Some(crate::component_graph::BuiltinPureOperation::MathCeil)
3380                    }
3381                    ("Math.round", 1) => {
3382                        Some(crate::component_graph::BuiltinPureOperation::MathRound)
3383                    }
3384                    ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
3385                    ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
3386                    _ => None,
3387                };
3388                if let Some(operation) = operation {
3389                    expression.kind = ComputedExpressionKind::BuiltinPureCall {
3390                        operation,
3391                        arguments: std::mem::take(arguments),
3392                    };
3393                }
3394                return;
3395            };
3396            let crate::ImportBindingTarget::SemanticPackage {
3397                package,
3398                version,
3399                integrity,
3400                export,
3401                kind: crate::semantic_package::SemanticPackageKind::Pure,
3402                runtime_module,
3403                resume_policy,
3404                pure_operation: Some(operation),
3405                ..
3406            } = &binding.target
3407            else {
3408                return;
3409            };
3410            expression.kind = ComputedExpressionKind::SemanticPackagePureCall {
3411                local_name: callee.clone(),
3412                package: package.clone(),
3413                version: version.clone(),
3414                integrity: integrity.clone(),
3415                export: export.clone(),
3416                runtime_module: runtime_module.clone(),
3417                resume_policy: resume_policy.clone(),
3418                operation: *operation,
3419                arguments: std::mem::take(arguments),
3420            };
3421        }
3422        ComputedExpressionKind::BuiltinPureCall { arguments, .. } => {
3423            for argument in arguments {
3424                resolve_semantic_package_pure_call(argument, module_path, bindings);
3425            }
3426        }
3427        ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
3428            for argument in arguments {
3429                resolve_semantic_package_pure_call(argument, module_path, bindings);
3430            }
3431        }
3432        ComputedExpressionKind::Template { expressions, .. } => {
3433            for expression in expressions {
3434                resolve_semantic_package_pure_call(expression, module_path, bindings);
3435            }
3436        }
3437        ComputedExpressionKind::MemberAccess { object, .. }
3438        | ComputedExpressionKind::Unary {
3439            operand: object, ..
3440        } => {
3441            resolve_semantic_package_pure_call(object, module_path, bindings);
3442        }
3443        ComputedExpressionKind::IndexAccess { object, index } => {
3444            resolve_semantic_package_pure_call(object, module_path, bindings);
3445            resolve_semantic_package_pure_call(index, module_path, bindings);
3446        }
3447        ComputedExpressionKind::Conditional {
3448            condition,
3449            when_true,
3450            when_false,
3451        } => {
3452            resolve_semantic_package_pure_call(condition, module_path, bindings);
3453            resolve_semantic_package_pure_call(when_true, module_path, bindings);
3454            resolve_semantic_package_pure_call(when_false, module_path, bindings);
3455        }
3456        ComputedExpressionKind::Arithmetic { left, right, .. }
3457        | ComputedExpressionKind::Comparison { left, right, .. }
3458        | ComputedExpressionKind::Logical { left, right, .. }
3459        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3460            resolve_semantic_package_pure_call(left, module_path, bindings);
3461            resolve_semantic_package_pure_call(right, module_path, bindings);
3462        }
3463        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
3464    }
3465}
3466
3467fn contains_semantic_package_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3468    match &expression.kind {
3469        ComputedExpressionKind::SemanticPackagePureCall {
3470            local_name,
3471            arguments,
3472            ..
3473        } => {
3474            local_name == callee
3475                || arguments
3476                    .iter()
3477                    .any(|argument| contains_semantic_package_pure_call(argument, callee))
3478        }
3479        ComputedExpressionKind::Call { arguments, .. } => arguments
3480            .iter()
3481            .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3482        ComputedExpressionKind::BuiltinPureCall { arguments, .. } => arguments
3483            .iter()
3484            .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3485        ComputedExpressionKind::Template { expressions, .. } => expressions
3486            .iter()
3487            .any(|expression| contains_semantic_package_pure_call(expression, callee)),
3488        ComputedExpressionKind::MemberAccess { object, .. }
3489        | ComputedExpressionKind::Unary {
3490            operand: object, ..
3491        } => contains_semantic_package_pure_call(object, callee),
3492        ComputedExpressionKind::IndexAccess { object, index } => {
3493            contains_semantic_package_pure_call(object, callee)
3494                || contains_semantic_package_pure_call(index, callee)
3495        }
3496        ComputedExpressionKind::Conditional {
3497            condition,
3498            when_true,
3499            when_false,
3500        } => {
3501            contains_semantic_package_pure_call(condition, callee)
3502                || contains_semantic_package_pure_call(when_true, callee)
3503                || contains_semantic_package_pure_call(when_false, callee)
3504        }
3505        ComputedExpressionKind::Arithmetic { left, right, .. }
3506        | ComputedExpressionKind::Comparison { left, right, .. }
3507        | ComputedExpressionKind::Logical { left, right, .. }
3508        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3509            contains_semantic_package_pure_call(left, callee)
3510                || contains_semantic_package_pure_call(right, callee)
3511        }
3512        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3513    }
3514}
3515
3516fn contains_builtin_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3517    match &expression.kind {
3518        ComputedExpressionKind::BuiltinPureCall {
3519            operation,
3520            arguments,
3521        } => {
3522            let operation_callee = match operation {
3523                crate::component_graph::BuiltinPureOperation::MathAbs => "Math.abs",
3524                crate::component_graph::BuiltinPureOperation::MathFloor => "Math.floor",
3525                crate::component_graph::BuiltinPureOperation::MathCeil => "Math.ceil",
3526                crate::component_graph::BuiltinPureOperation::MathRound => "Math.round",
3527                crate::component_graph::BuiltinPureOperation::MathMin => "Math.min",
3528                crate::component_graph::BuiltinPureOperation::MathMax => "Math.max",
3529            };
3530            operation_callee == callee
3531                || arguments
3532                    .iter()
3533                    .any(|argument| contains_builtin_pure_call(argument, callee))
3534        }
3535        ComputedExpressionKind::Call { arguments, .. }
3536        | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => arguments
3537            .iter()
3538            .any(|argument| contains_builtin_pure_call(argument, callee)),
3539        ComputedExpressionKind::Template { expressions, .. } => expressions
3540            .iter()
3541            .any(|expression| contains_builtin_pure_call(expression, callee)),
3542        ComputedExpressionKind::MemberAccess { object, .. }
3543        | ComputedExpressionKind::Unary {
3544            operand: object, ..
3545        } => contains_builtin_pure_call(object, callee),
3546        ComputedExpressionKind::IndexAccess { object, index } => {
3547            contains_builtin_pure_call(object, callee) || contains_builtin_pure_call(index, callee)
3548        }
3549        ComputedExpressionKind::Conditional {
3550            condition,
3551            when_true,
3552            when_false,
3553        } => {
3554            contains_builtin_pure_call(condition, callee)
3555                || contains_builtin_pure_call(when_true, callee)
3556                || contains_builtin_pure_call(when_false, callee)
3557        }
3558        ComputedExpressionKind::Arithmetic { left, right, .. }
3559        | ComputedExpressionKind::Comparison { left, right, .. }
3560        | ComputedExpressionKind::Logical { left, right, .. }
3561        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3562            contains_builtin_pure_call(left, callee) || contains_builtin_pure_call(right, callee)
3563        }
3564        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3565    }
3566}
3567
3568fn collect_computed_cycle_diagnostics(
3569    analysis: &IrReactiveCycleAnalysis,
3570    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3571) -> Vec<ComponentDiagnostic> {
3572    analysis
3573        .cycles
3574        .iter()
3575        .filter_map(|cycle| {
3576            let first = cycle.nodes.first()?;
3577            let computed = computed_values
3578                .values()
3579                .find(|computed| computed.id.as_str() == first)?;
3580            Some(ComponentDiagnostic {
3581                severity: ComponentDiagnosticSeverity::Error,
3582                effect_id: None,
3583                statement_id: None,
3584                context_declaration_candidate_id: None,
3585                context_id: None,
3586                provider_id: None,
3587                consumer_id: None,
3588                slot_id: None,
3589                invocation_id: None,
3590                component_instance_id: None,
3591                slot_binding_id: None,
3592                structural_region_id: None,
3593                component_id: None,
3594                provider_instance_id: None,
3595                consumer_instance_id: None,
3596                secondary_labels: Vec::new(),
3597                code: ComputedDiagnosticCode::DependencyCycle.as_str().to_string(),
3598                message: format!(
3599                    "computed dependency cycle detected among: {}",
3600                    cycle.nodes.join(", ")
3601                ),
3602                provenance: Some(computed.provenance.clone()),
3603            })
3604        })
3605        .collect()
3606}
3607
3608fn collect_computed_semantic_diagnostics(
3609    components: &[ComponentNode],
3610    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3611    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3612    expression_graph: &ExpressionGraph,
3613    semantic_types: &SemanticTypeModel,
3614    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3615) -> Vec<ComponentDiagnostic> {
3616    let mut diagnostics = collect_invalid_computed_declaration_diagnostics(components, provenance);
3617    diagnostics.extend(collect_computed_body_and_read_diagnostics(
3618        components,
3619        computed_values,
3620        resource_declarations,
3621        expression_graph,
3622    ));
3623    diagnostics.extend(collect_computed_type_diagnostics(
3624        computed_values,
3625        semantic_types,
3626    ));
3627    diagnostics.extend(collect_computed_conditional_diagnostics(
3628        components,
3629        expression_graph,
3630        semantic_types,
3631    ));
3632    sort_computed_diagnostics(&mut diagnostics);
3633    diagnostics
3634}
3635
3636fn collect_computed_conditional_diagnostics(
3637    components: &[ComponentNode],
3638    expression_graph: &ExpressionGraph,
3639    semantic_types: &SemanticTypeModel,
3640) -> Vec<ComponentDiagnostic> {
3641    expression_graph
3642        .nodes
3643        .values()
3644        .filter_map(|node| {
3645            let crate::ExpressionNodeKind::Conditional { condition, .. } = &node.kind else {
3646                return None;
3647            };
3648            let condition_type = semantic_types
3649                .assignments
3650                .get(condition)
3651                .map(|assignment| assignment.semantic_type.clone())
3652                .or_else(|| match &expression_graph.node(condition)?.kind {
3653                    crate::ExpressionNodeKind::ThisMember { name } => components
3654                        .iter()
3655                        .find(|component| {
3656                            component
3657                                .methods
3658                                .iter()
3659                                .any(|method| component.id.computed(&method.name) == node.owner)
3660                        })
3661                        .and_then(|component| {
3662                            component
3663                                .state_fields
3664                                .iter()
3665                                .find(|field| field.name == *name)
3666                        })
3667                        .and_then(|field| semantic_types.assignments.get(&field.id))
3668                        .map(|assignment| assignment.semantic_type.clone()),
3669                    _ => None,
3670                })
3671                .unwrap_or(crate::SemanticType::Unknown);
3672            if is_boolean_computed_condition(&condition_type) {
3673                None
3674            } else {
3675                Some(ComponentDiagnostic {
3676                    severity: ComponentDiagnosticSeverity::Error,
3677                    effect_id: None,
3678                    statement_id: None,
3679                    context_declaration_candidate_id: None,
3680                    context_id: None,
3681                    provider_id: None,
3682                    consumer_id: None,
3683                    slot_id: None,
3684                    invocation_id: None,
3685                    component_instance_id: None,
3686                    slot_binding_id: None,
3687                    structural_region_id: None,
3688                    component_id: None,
3689                    provider_instance_id: None,
3690                    consumer_instance_id: None,
3691                    secondary_labels: Vec::new(),
3692                    code: crate::TypeDiagnosticCode::InvalidCondition
3693                        .as_str()
3694                        .to_string(),
3695                    message: format!(
3696                        "computed conditional requires boolean, but has {}",
3697                        crate::semantic_type_text(&condition_type)
3698                    ),
3699                    provenance: Some(node.provenance.clone()),
3700                })
3701            }
3702        })
3703        .collect()
3704}
3705
3706fn is_boolean_computed_condition(semantic_type: &crate::SemanticType) -> bool {
3707    match semantic_type {
3708        crate::SemanticType::Boolean | crate::SemanticType::BooleanLiteral(_) => true,
3709        crate::SemanticType::Union(members) => members.iter().all(is_boolean_computed_condition),
3710        _ => false,
3711    }
3712}
3713
3714fn collect_invalid_computed_declaration_diagnostics(
3715    components: &[ComponentNode],
3716    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3717) -> Vec<ComponentDiagnostic> {
3718    components
3719        .iter()
3720        .flat_map(|component| &component.methods)
3721        .filter(|method| {
3722            method
3723                .decorators
3724                .iter()
3725                .any(|decorator| decorator == "computed")
3726                && !method.is_getter
3727        })
3728        .map(|method| ComponentDiagnostic {
3729            severity: ComponentDiagnosticSeverity::Error,
3730            effect_id: None,
3731            statement_id: None,
3732            context_declaration_candidate_id: None,
3733            context_id: None,
3734            provider_id: None,
3735            consumer_id: None,
3736            slot_id: None,
3737            invocation_id: None,
3738            component_instance_id: None,
3739            slot_binding_id: None,
3740            structural_region_id: None,
3741            component_id: None,
3742            provider_instance_id: None,
3743            consumer_instance_id: None,
3744            secondary_labels: Vec::new(),
3745            code: ComputedDiagnosticCode::InvalidDeclaration
3746                .as_str()
3747                .to_string(),
3748            message: format!(
3749                "@computed() declaration `{}` must decorate a getter",
3750                method.name
3751            ),
3752            provenance: Some(
3753                provenance
3754                    .get(&method.id)
3755                    .expect("component methods should have canonical provenance")
3756                    .clone(),
3757            ),
3758        })
3759        .collect()
3760}
3761
3762fn collect_computed_body_and_read_diagnostics(
3763    components: &[ComponentNode],
3764    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3765    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3766    expression_graph: &ExpressionGraph,
3767) -> Vec<ComponentDiagnostic> {
3768    let mut diagnostics = Vec::new();
3769
3770    for computed in computed_values.values() {
3771        let component_id = computed
3772            .owner
3773            .entity_id()
3774            .expect("computed values should have component owners");
3775        let component = components
3776            .iter()
3777            .find(|component| component.id == *component_id)
3778            .expect("computed values should have owning components");
3779        let method = component
3780            .methods
3781            .iter()
3782            .find(|method| method.id == computed.method)
3783            .expect("computed values should have authored methods");
3784
3785        if method.computed_expression.is_none() {
3786            diagnostics.push(ComponentDiagnostic {
3787                severity: ComponentDiagnosticSeverity::Error,
3788                effect_id: None,
3789                statement_id: None,
3790                context_declaration_candidate_id: None,
3791                context_id: None,
3792                provider_id: None,
3793                consumer_id: None,
3794                slot_id: None,
3795                invocation_id: None,
3796                component_instance_id: None,
3797                slot_binding_id: None,
3798                structural_region_id: None,
3799                component_id: None,
3800                provider_instance_id: None,
3801                consumer_instance_id: None,
3802                secondary_labels: Vec::new(),
3803                code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3804                message: format!(
3805                    "computed getter `{}` has an unsupported body",
3806                    computed.name
3807                ),
3808                provenance: Some(computed.provenance.clone()),
3809            });
3810        }
3811
3812        diagnostics.extend(
3813            expression_graph
3814                .nodes_for(&computed.id)
3815                .into_iter()
3816                .filter_map(|node| {
3817                    unresolved_computed_read_diagnostic(
3818                        component,
3819                        computed_values,
3820                        resource_declarations,
3821                        computed,
3822                        node,
3823                    )
3824                }),
3825        );
3826        diagnostics.extend(computed_resource_projection_diagnostics(
3827            component,
3828            computed,
3829            resource_declarations,
3830            expression_graph,
3831        ));
3832    }
3833
3834    diagnostics
3835}
3836
3837fn unresolved_computed_read_diagnostic(
3838    component: &ComponentNode,
3839    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3840    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3841    computed: &ComputedValue,
3842    node: &ExpressionNode,
3843) -> Option<ComponentDiagnostic> {
3844    let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3845        return None;
3846    };
3847    let resolved = component
3848        .state_fields
3849        .iter()
3850        .any(|field| field.name == *name)
3851        || computed_values.contains_key(&component.id.computed(name))
3852        || resource_declarations.contains_key(&ResourceId::for_owner(&component.id, name));
3853    (!resolved).then(|| ComponentDiagnostic {
3854        severity: ComponentDiagnosticSeverity::Error,
3855        effect_id: None,
3856        statement_id: None,
3857        context_declaration_candidate_id: None,
3858        context_id: None,
3859        provider_id: None,
3860        consumer_id: None,
3861        slot_id: None,
3862        invocation_id: None,
3863        component_instance_id: None,
3864        slot_binding_id: None,
3865        structural_region_id: None,
3866        component_id: None,
3867        provider_instance_id: None,
3868        consumer_instance_id: None,
3869        secondary_labels: Vec::new(),
3870        code: ComputedDiagnosticCode::UnresolvedRead.as_str().to_string(),
3871        message: format!(
3872            "computed getter `{}` reads unresolved member `this.{name}`",
3873            computed.name
3874        ),
3875        provenance: Some(node.provenance.clone()),
3876    })
3877}
3878
3879fn computed_resource_projection_diagnostics(
3880    component: &ComponentNode,
3881    computed: &ComputedValue,
3882    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3883    expression_graph: &ExpressionGraph,
3884) -> Vec<ComponentDiagnostic> {
3885    let nodes = expression_graph.nodes_for(&computed.id);
3886    nodes
3887        .iter()
3888        .filter_map(|node| {
3889            let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3890                return None;
3891            };
3892            resource_declarations
3893                .contains_key(&ResourceId::for_owner(&component.id, name))
3894                .then_some((node, name))
3895        })
3896        .filter(|(node, _)| {
3897            !nodes.iter().any(|candidate| {
3898                is_direct_resource_projection(candidate, &nodes)
3899                    && matches!(
3900                        &candidate.kind,
3901                        ExpressionNodeKind::MemberAccess { object, .. } if object == &node.id
3902                    )
3903            })
3904        })
3905        .map(|(node, name)| ComponentDiagnostic {
3906            severity: ComponentDiagnosticSeverity::Error,
3907            effect_id: None,
3908            statement_id: None,
3909            context_declaration_candidate_id: None,
3910            context_id: None,
3911            provider_id: None,
3912            consumer_id: None,
3913            slot_id: None,
3914            invocation_id: None,
3915            component_instance_id: None,
3916            slot_binding_id: None,
3917            structural_region_id: None,
3918            component_id: None,
3919            provider_instance_id: None,
3920            consumer_instance_id: None,
3921            secondary_labels: Vec::new(),
3922            code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3923            message: format!(
3924                "computed getter `{}` may only directly project `this.{name}.data`, `this.{name}.error`, or `this.{name}.state`",
3925                computed.name
3926            ),
3927            provenance: Some(node.provenance.clone()),
3928        })
3929        .collect()
3930}
3931
3932fn is_direct_resource_projection(node: &ExpressionNode, nodes: &[&ExpressionNode]) -> bool {
3933    let ExpressionNodeKind::MemberAccess {
3934        property,
3935        optional: false,
3936        ..
3937    } = &node.kind
3938    else {
3939        return false;
3940    };
3941    matches!(property.as_str(), "data" | "error" | "state")
3942        && !nodes.iter().any(|candidate| {
3943            matches!(
3944                &candidate.kind,
3945                ExpressionNodeKind::MemberAccess { object, .. }
3946                    | ExpressionNodeKind::IndexAccess { object, .. }
3947                    if object == &node.id
3948            )
3949        })
3950}
3951
3952fn collect_computed_type_diagnostics(
3953    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3954    semantic_types: &SemanticTypeModel,
3955) -> Vec<ComponentDiagnostic> {
3956    let mut diagnostics = Vec::new();
3957
3958    for computed_type in semantic_types.computed_values.values() {
3959        let computed = computed_values
3960            .get(&computed_type.computed)
3961            .expect("computed type records should have computed entities");
3962        if computed_type.declared_return_compatible == Some(false) {
3963            let declared = computed_type
3964                .declared_return_type
3965                .as_ref()
3966                .expect("incompatible computed return types should be declared");
3967            diagnostics.push(ComponentDiagnostic {
3968                severity: ComponentDiagnosticSeverity::Error,
3969                effect_id: None,
3970                statement_id: None,
3971                context_declaration_candidate_id: None,
3972                context_id: None,
3973                provider_id: None,
3974                consumer_id: None,
3975                slot_id: None,
3976                invocation_id: None,
3977                component_instance_id: None,
3978                slot_binding_id: None,
3979                structural_region_id: None,
3980                component_id: None,
3981                provider_instance_id: None,
3982                consumer_instance_id: None,
3983                secondary_labels: Vec::new(),
3984                code: ComputedDiagnosticCode::TypeMismatch.as_str().to_string(),
3985                message: format!(
3986                    "computed getter `{}` returns `{}` but declares `{}`",
3987                    computed.name,
3988                    crate::semantic_type_text(&computed_type.semantic_type),
3989                    crate::semantic_type_text(declared)
3990                ),
3991                provenance: Some(computed_type.provenance.clone()),
3992            });
3993        }
3994        if computed_type.serialization == crate::SerializationCompatibility::NotSerializable {
3995            diagnostics.push(ComponentDiagnostic {
3996                severity: ComponentDiagnosticSeverity::Error,
3997                effect_id: None,
3998                statement_id: None,
3999                context_declaration_candidate_id: None,
4000                context_id: None,
4001                provider_id: None,
4002                consumer_id: None,
4003                slot_id: None,
4004                invocation_id: None,
4005                component_instance_id: None,
4006                slot_binding_id: None,
4007                structural_region_id: None,
4008                component_id: None,
4009                provider_instance_id: None,
4010                consumer_instance_id: None,
4011                secondary_labels: Vec::new(),
4012                code: ComputedDiagnosticCode::SerializationViolation
4013                    .as_str()
4014                    .to_string(),
4015                message: format!(
4016                    "computed getter `{}` has a non-serializable result",
4017                    computed.name
4018                ),
4019                provenance: Some(computed_type.provenance.clone()),
4020            });
4021        }
4022    }
4023
4024    diagnostics
4025}
4026
4027fn sort_computed_diagnostics(diagnostics: &mut [ComponentDiagnostic]) {
4028    diagnostics.sort_by(|left, right| {
4029        (
4030            left.code.as_str(),
4031            left.provenance.as_ref().map(|provenance| &provenance.path),
4032            left.provenance
4033                .as_ref()
4034                .map(|provenance| provenance.span.start),
4035            left.provenance
4036                .as_ref()
4037                .map(|provenance| provenance.span.end),
4038            left.message.as_str(),
4039        )
4040            .cmp(&(
4041                right.code.as_str(),
4042                right.provenance.as_ref().map(|provenance| &provenance.path),
4043                right
4044                    .provenance
4045                    .as_ref()
4046                    .map(|provenance| provenance.span.start),
4047                right
4048                    .provenance
4049                    .as_ref()
4050                    .map(|provenance| provenance.span.end),
4051                right.message.as_str(),
4052            ))
4053    });
4054}
4055
4056fn build_computed_reactive_products(
4057    components: &[ComponentNode],
4058    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4059    effects: &BTreeMap<SemanticId, Effect>,
4060    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4061    references: &[SemanticReference],
4062    provenance: &BTreeMap<SemanticId, SourceProvenance>,
4063) -> (
4064    IrReactiveGraph,
4065    IrReactiveTransitiveAnalysis,
4066    IrReactiveCycleAnalysis,
4067    IrComputedEvaluationPlan,
4068) {
4069    let graph = crate::build_reactive_graph(
4070        components,
4071        computed_values,
4072        effects,
4073        resource_declarations,
4074        references,
4075        provenance,
4076    );
4077    let transitive_analysis = crate::analyze_reactive_transitive_graph(&graph);
4078    let cycle_analysis = crate::analyze_reactive_cycles(&graph);
4079    let evaluation_plan = crate::plan_computed_evaluation(&graph);
4080    (graph, transitive_analysis, cycle_analysis, evaluation_plan)
4081}
4082
4083fn extend_template_references(
4084    references: &mut Vec<SemanticReference>,
4085    components: &[ComponentNode],
4086    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4087    template_entities: &[TemplateSemanticEntity],
4088    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4089) {
4090    references.extend(build_template_state_references(
4091        components,
4092        template_entities,
4093        ownership,
4094    ));
4095    references.extend(build_template_computed_references(
4096        components,
4097        computed_values,
4098        template_entities,
4099        ownership,
4100    ));
4101    references.extend(build_template_event_references(
4102        components,
4103        template_entities,
4104        ownership,
4105    ));
4106    references.extend(build_template_local_references(
4107        components,
4108        template_entities,
4109        ownership,
4110    ));
4111}
4112
4113fn build_form_field_binding_references(
4114    bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4115) -> Vec<SemanticReference> {
4116    bindings
4117        .values()
4118        .flat_map(|binding| {
4119            [
4120                SemanticReference {
4121                    kind: SemanticReferenceKind::FieldBindingField,
4122                    source: binding.id.as_semantic_id().clone(),
4123                    target: binding.field.as_semantic_id().clone(),
4124                    provenance: binding.expression_provenance.clone(),
4125                },
4126                SemanticReference {
4127                    kind: SemanticReferenceKind::FieldBindingForm,
4128                    source: binding.id.as_semantic_id().clone(),
4129                    target: binding.form.as_semantic_id().clone(),
4130                    provenance: binding.expression_provenance.clone(),
4131                },
4132            ]
4133        })
4134        .collect()
4135}
4136
4137fn computed_call_purity_kind(
4138    component: &ComponentNode,
4139    callee: &str,
4140) -> crate::ComputedPurityViolationKind {
4141    if matches!(callee, "resource" | "this.resource") {
4142        return crate::ComputedPurityViolationKind::Resource;
4143    }
4144    if matches!(
4145        callee,
4146        "Math.random"
4147            | "Date.now"
4148            | "performance.now"
4149            | "crypto.randomUUID"
4150            | "crypto.getRandomValues"
4151    ) {
4152        return crate::ComputedPurityViolationKind::NondeterministicOperation;
4153    }
4154    if callee.starts_with("console.")
4155        || matches!(
4156            callee,
4157            "fetch" | "setTimeout" | "setInterval" | "queueMicrotask"
4158        )
4159    {
4160        return crate::ComputedPurityViolationKind::Effect;
4161    }
4162    if let Some(name) = callee.strip_prefix("this.") {
4163        if let Some(method) = component.methods.iter().find(|method| method.name == name) {
4164            if method.is_action()
4165                || method
4166                    .decorators
4167                    .iter()
4168                    .any(|decorator| decorator == "action")
4169            {
4170                return crate::ComputedPurityViolationKind::Action;
4171            }
4172            if method
4173                .decorators
4174                .iter()
4175                .any(|decorator| decorator == "effect")
4176            {
4177                return crate::ComputedPurityViolationKind::Effect;
4178            }
4179            if method
4180                .decorators
4181                .iter()
4182                .any(|decorator| decorator == "resource")
4183            {
4184                return crate::ComputedPurityViolationKind::Resource;
4185            }
4186        }
4187    }
4188    crate::ComputedPurityViolationKind::ArbitraryMethodCall
4189}
4190
4191fn build_computed_references(
4192    components: &[ComponentNode],
4193    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4194    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4195    expression_graph: &ExpressionGraph,
4196) -> Vec<SemanticReference> {
4197    let mut references = BTreeMap::new();
4198
4199    for computed in computed_values.values() {
4200        let Some(component_id) = computed.owner.entity_id() else {
4201            continue;
4202        };
4203        let Some(component) = components
4204            .iter()
4205            .find(|component| component.id == *component_id)
4206        else {
4207            continue;
4208        };
4209
4210        let nodes = expression_graph.nodes_for(&computed.id);
4211        let direct_resource_projection_objects = nodes
4212            .iter()
4213            .filter(|node| is_direct_resource_projection(node, &nodes))
4214            .filter_map(|node| match &node.kind {
4215                crate::ExpressionNodeKind::MemberAccess { object, .. } => Some(object.clone()),
4216                _ => None,
4217            })
4218            .collect::<BTreeSet<_>>();
4219
4220        for node in nodes {
4221            let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4222                continue;
4223            };
4224            let reference = if let Some(field) = component
4225                .state_fields
4226                .iter()
4227                .find(|field| field.name == *name)
4228            {
4229                SemanticReference {
4230                    kind: SemanticReferenceKind::ComputedState,
4231                    source: computed.id.clone(),
4232                    target: field.id.clone(),
4233                    provenance: computed.provenance.clone(),
4234                }
4235            } else if let Some(target) = computed_values.get(&component.id.computed(name)) {
4236                SemanticReference {
4237                    kind: SemanticReferenceKind::ComputedComputed,
4238                    source: computed.id.clone(),
4239                    target: target.id.clone(),
4240                    provenance: computed.provenance.clone(),
4241                }
4242            } else if direct_resource_projection_objects.contains(&node.id) {
4243                let target = ResourceId::for_owner(&component.id, name);
4244                let Some(declaration) = resource_declarations.get(&target) else {
4245                    continue;
4246                };
4247                SemanticReference {
4248                    kind: SemanticReferenceKind::ComputedResource,
4249                    source: computed.id.clone(),
4250                    target: declaration.id.as_semantic_id().clone(),
4251                    provenance: node.provenance.clone(),
4252                }
4253            } else {
4254                continue;
4255            };
4256            references.insert(
4257                (reference.source.clone(), reference.target.clone()),
4258                reference,
4259            );
4260        }
4261    }
4262
4263    references.into_values().collect()
4264}
4265
4266fn build_effect_references(
4267    components: &[ComponentNode],
4268    effects: &BTreeMap<SemanticId, Effect>,
4269    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4270    expression_graph: &ExpressionGraph,
4271) -> Vec<SemanticReference> {
4272    let mut references = Vec::new();
4273    for effect in effects.values() {
4274        let Some(component_id) = effect.owner.entity_id() else {
4275            continue;
4276        };
4277        let Some(component) = components
4278            .iter()
4279            .find(|component| component.id == *component_id)
4280        else {
4281            continue;
4282        };
4283        for node in expression_graph.nodes_for(&effect.id) {
4284            let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4285                continue;
4286            };
4287            let reference = if let Some(field) = component
4288                .state_fields
4289                .iter()
4290                .find(|field| field.name == *name)
4291            {
4292                SemanticReference {
4293                    kind: SemanticReferenceKind::EffectState,
4294                    source: effect.id.clone(),
4295                    target: field.id.clone(),
4296                    provenance: node.provenance.clone(),
4297                }
4298            } else if let Some(computed) = computed_values.get(&component.id.computed(name)) {
4299                SemanticReference {
4300                    kind: SemanticReferenceKind::EffectComputed,
4301                    source: effect.id.clone(),
4302                    target: computed.id.clone(),
4303                    provenance: node.provenance.clone(),
4304                }
4305            } else {
4306                continue;
4307            };
4308            references.push(reference);
4309        }
4310    }
4311    references.sort_by(|left, right| {
4312        (
4313            left.source.as_str(),
4314            left.target.as_str(),
4315            left.provenance.span.start,
4316        )
4317            .cmp(&(
4318                right.source.as_str(),
4319                right.target.as_str(),
4320                right.provenance.span.start,
4321            ))
4322    });
4323    references.dedup_by(|left, right| {
4324        left.kind == right.kind && left.source == right.source && left.target == right.target
4325    });
4326    references
4327}
4328
4329fn build_provider_references(
4330    providers: &BTreeMap<ProviderId, ProviderEntity>,
4331) -> Vec<SemanticReference> {
4332    providers
4333        .values()
4334        .map(|provider| SemanticReference {
4335            kind: SemanticReferenceKind::ProvidesContext,
4336            source: provider.id.as_semantic_id().clone(),
4337            target: provider.context.as_semantic_id().clone(),
4338            provenance: provider.provenance.clone(),
4339        })
4340        .collect()
4341}
4342
4343fn build_consumer_references(
4344    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4345) -> Vec<SemanticReference> {
4346    consumers
4347        .values()
4348        .filter_map(|consumer| {
4349            let ContextResolutionState::Resolved(context) = &consumer.context_resolution else {
4350                return None;
4351            };
4352            Some(SemanticReference {
4353                kind: SemanticReferenceKind::ConsumesContext,
4354                source: consumer.id.as_semantic_id().clone(),
4355                target: context.as_semantic_id().clone(),
4356                provenance: consumer.provenance.clone(),
4357            })
4358        })
4359        .collect()
4360}
4361
4362fn build_context_resolution_references(
4363    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
4364) -> Vec<SemanticReference> {
4365    resolutions
4366        .values()
4367        .filter_map(|resolution| {
4368            let ContextResolutionResult::Provider { provider, .. } = &resolution.result else {
4369                return None;
4370            };
4371            Some(SemanticReference {
4372                kind: SemanticReferenceKind::ResolvesToProvider,
4373                source: resolution.consumer.as_semantic_id().clone(),
4374                target: provider.as_semantic_id().clone(),
4375                provenance: resolution.provenance.clone(),
4376            })
4377        })
4378        .collect()
4379}
4380
4381fn build_template_state_references(
4382    components: &[ComponentNode],
4383    template_entities: &[TemplateSemanticEntity],
4384    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4385) -> Vec<SemanticReference> {
4386    template_entities
4387        .iter()
4388        .filter(|entity| {
4389            matches!(
4390                entity.kind,
4391                TemplateSemanticKind::Binding
4392                    | TemplateSemanticKind::AttributeBinding
4393                    | TemplateSemanticKind::Conditional
4394                    | TemplateSemanticKind::List
4395            )
4396        })
4397        .filter_map(|entity| {
4398            let field_name = entity.expression.as_deref().and_then(this_member_name)?;
4399            let component = template_entity_component(components, ownership, entity)?;
4400            let field = component
4401                .state_fields
4402                .iter()
4403                .find(|field| field.name == field_name)?;
4404
4405            Some(SemanticReference {
4406                kind: SemanticReferenceKind::TemplateState,
4407                source: entity.id.clone(),
4408                target: field.id.clone(),
4409                provenance: entity.provenance.clone(),
4410            })
4411        })
4412        .collect()
4413}
4414
4415fn build_template_computed_references(
4416    components: &[ComponentNode],
4417    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4418    template_entities: &[TemplateSemanticEntity],
4419    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4420) -> Vec<SemanticReference> {
4421    template_entities
4422        .iter()
4423        .filter(|entity| {
4424            matches!(
4425                entity.kind,
4426                TemplateSemanticKind::Binding
4427                    | TemplateSemanticKind::AttributeBinding
4428                    | TemplateSemanticKind::Conditional
4429                    | TemplateSemanticKind::List
4430            )
4431        })
4432        .filter_map(|entity| {
4433            let computed_name = entity.expression.as_deref().and_then(this_member_name)?;
4434            let component = template_entity_component(components, ownership, entity)?;
4435            if component
4436                .state_fields
4437                .iter()
4438                .any(|field| field.name == computed_name)
4439            {
4440                return None;
4441            }
4442            let computed = computed_values.get(&component.id.computed(computed_name))?;
4443
4444            Some(SemanticReference {
4445                kind: SemanticReferenceKind::TemplateComputed,
4446                source: entity.id.clone(),
4447                target: computed.id.clone(),
4448                provenance: entity.provenance.clone(),
4449            })
4450        })
4451        .collect()
4452}
4453
4454fn build_template_event_references(
4455    components: &[ComponentNode],
4456    template_entities: &[TemplateSemanticEntity],
4457    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4458) -> Vec<SemanticReference> {
4459    template_entities
4460        .iter()
4461        .filter(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
4462        .filter_map(|entity| {
4463            let method_name = entity.expression.as_deref().and_then(this_member_name)?;
4464            let component = template_entity_component(components, ownership, entity)?;
4465            let method = component
4466                .methods
4467                .iter()
4468                .find(|method| method.name == method_name)?;
4469
4470            Some(SemanticReference {
4471                kind: SemanticReferenceKind::EventMethod,
4472                source: entity.id.clone(),
4473                target: method.id.clone(),
4474                provenance: entity.provenance.clone(),
4475            })
4476        })
4477        .collect()
4478}
4479
4480fn build_template_local_references(
4481    components: &[ComponentNode],
4482    template_entities: &[TemplateSemanticEntity],
4483    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4484) -> Vec<SemanticReference> {
4485    let mut references = template_entities
4486        .iter()
4487        .filter(|entity| {
4488            matches!(
4489                entity.kind,
4490                TemplateSemanticKind::Binding | TemplateSemanticKind::AttributeBinding
4491            ) && entity.scope == TemplateSemanticScope::Render
4492        })
4493        .filter_map(|entity| {
4494            let name = entity.expression.as_deref()?;
4495            let component = template_entity_component(components, ownership, entity)?;
4496            let render = component
4497                .methods
4498                .iter()
4499                .find(|method| method.name == "render")?;
4500            let local = unique_local_variable(render, name)?;
4501
4502            Some(SemanticReference {
4503                kind: SemanticReferenceKind::TemplateLocal,
4504                source: entity.id.clone(),
4505                target: local.id.clone(),
4506                provenance: entity.provenance.clone(),
4507            })
4508        })
4509        .collect::<Vec<_>>();
4510    references.sort_by(|left, right| {
4511        (left.source.as_str(), left.target.as_str())
4512            .cmp(&(right.source.as_str(), right.target.as_str()))
4513    });
4514    references
4515}
4516
4517fn unique_local_variable<'a>(
4518    method: &'a ComponentMethod,
4519    name: &str,
4520) -> Option<&'a MethodLocalVariable> {
4521    let mut locals = method
4522        .local_variables
4523        .iter()
4524        .filter(|local| local.name == name);
4525    let local = locals.next()?;
4526    locals.next().is_none().then_some(local)
4527}
4528
4529fn template_entity_component<'a>(
4530    components: &'a [ComponentNode],
4531    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4532    entity: &TemplateSemanticEntity,
4533) -> Option<&'a ComponentNode> {
4534    let template_id = ownership.get(&entity.id)?.entity_id()?;
4535    let component_id = ownership.get(template_id)?.entity_id()?;
4536
4537    components
4538        .iter()
4539        .find(|component| component.id == *component_id)
4540}
4541
4542fn this_member_name(expression: &str) -> Option<&str> {
4543    expression.strip_prefix("this.").filter(|name| {
4544        !name.is_empty()
4545            && name
4546                .bytes()
4547                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
4548    })
4549}
4550
4551#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
4552fn collect_ownership(
4553    components: &[ComponentNode],
4554    contexts: &BTreeMap<ContextId, ContextEntity>,
4555    forms: &BTreeMap<FormId, FormEntity>,
4556    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
4557    form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4558    providers: &BTreeMap<ProviderId, ProviderEntity>,
4559    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4560    slots: &BTreeMap<SlotId, SlotEntity>,
4561    component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
4562    slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
4563    slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
4564    component_instance_plan: &ComponentInstancePlan,
4565    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4566    effects: &BTreeMap<SemanticId, Effect>,
4567    templates: &[TemplateNode],
4568    template_entities: &[TemplateSemanticEntity],
4569) -> BTreeMap<SemanticId, SemanticOwner> {
4570    let mut ownership = BTreeMap::new();
4571
4572    for component in components {
4573        ownership.insert(component.id.clone(), SemanticOwner::Application);
4574
4575        for field in &component.state_fields {
4576            ownership.insert(
4577                field.id.clone(),
4578                SemanticOwner::entity(component.id.clone()),
4579            );
4580        }
4581        for method in &component.methods {
4582            ownership.insert(
4583                method.id.clone(),
4584                SemanticOwner::entity(component.id.clone()),
4585            );
4586            for parameter in &method.parameters {
4587                ownership.insert(
4588                    parameter.id.clone(),
4589                    SemanticOwner::entity(method.id.clone()),
4590                );
4591            }
4592            for local in &method.local_variables {
4593                ownership.insert(local.id.clone(), SemanticOwner::entity(method.id.clone()));
4594            }
4595        }
4596        for computed in computed_values
4597            .values()
4598            .filter(|computed| computed.owner.entity_id() == Some(&component.id))
4599        {
4600            ownership.insert(computed.id.clone(), computed.owner.clone());
4601        }
4602        for context in contexts
4603            .values()
4604            .filter(|context| context.owner.entity_id() == Some(&component.id))
4605        {
4606            ownership.insert(context.id.as_semantic_id().clone(), context.owner.clone());
4607        }
4608        for form in forms
4609            .values()
4610            .filter(|form| form.owner.entity_id() == Some(&component.id))
4611        {
4612            ownership.insert(form.id.as_semantic_id().clone(), form.owner.clone());
4613        }
4614        for field in form_fields
4615            .values()
4616            .filter(|field| field.owner_component == component.id)
4617        {
4618            ownership.insert(
4619                field.id.as_semantic_id().clone(),
4620                SemanticOwner::entity(field.owner_form.as_semantic_id().clone()),
4621            );
4622        }
4623        for provider in providers
4624            .values()
4625            .filter(|provider| provider.owner.entity_id() == Some(&component.id))
4626        {
4627            ownership.insert(provider.id.as_semantic_id().clone(), provider.owner.clone());
4628        }
4629        for consumer in consumers
4630            .values()
4631            .filter(|consumer| consumer.owner.entity_id() == Some(&component.id))
4632        {
4633            ownership.insert(consumer.id.as_semantic_id().clone(), consumer.owner.clone());
4634        }
4635        for slot in slots.values().filter(|slot| slot.owner == component.id) {
4636            ownership.insert(slot.id.as_semantic_id().clone(), slot.semantic_owner());
4637        }
4638        for invocation in component_invocations
4639            .values()
4640            .filter(|invocation| invocation.owner_component == component.id)
4641        {
4642            ownership.insert(
4643                invocation.id.as_semantic_id().clone(),
4644                SemanticOwner::entity(component.id.clone()),
4645            );
4646        }
4647        for fragment in slot_content_fragments
4648            .values()
4649            .filter(|fragment| fragment.owner_component == component.id)
4650        {
4651            ownership.insert(
4652                fragment.id.as_semantic_id().clone(),
4653                SemanticOwner::entity(component.id.clone()),
4654            );
4655        }
4656        for outlet in slot_outlets
4657            .values()
4658            .filter(|outlet| outlet.owner_component == component.id)
4659        {
4660            ownership.insert(
4661                outlet.id.as_semantic_id().clone(),
4662                SemanticOwner::entity(component.id.clone()),
4663            );
4664        }
4665        for effect in effects
4666            .values()
4667            .filter(|effect| effect.owner.entity_id() == Some(&component.id))
4668        {
4669            ownership.insert(effect.id.clone(), effect.owner.clone());
4670        }
4671        for endpoint in &component.action_endpoints {
4672            ownership.insert(endpoint.id.clone(), endpoint.owner.clone());
4673        }
4674        for action in &component.actions {
4675            // Legacy method actions derive their owner from the method graph,
4676            // while decorator-free action fields retain their canonical endpoint owner.
4677            let owner = component
4678                .methods
4679                .iter()
4680                .find(|method| method.name == action.method)
4681                .map(|method| SemanticOwner::entity(method.id.clone()))
4682                .unwrap_or_else(|| action.owner.clone());
4683            ownership.insert(action.id.clone(), owner);
4684        }
4685        if let Some(render) = &component.render {
4686            for handler in render_event_handlers(render) {
4687                ownership.insert(
4688                    handler.id.clone(),
4689                    SemanticOwner::entity(component.id.template()),
4690                );
4691            }
4692        }
4693    }
4694
4695    for instance in component_instance_plan.instances.values() {
4696        ownership.insert(
4697            instance.id.as_semantic_id().clone(),
4698            instance
4699                .parent_instance
4700                .as_ref()
4701                .map_or(SemanticOwner::Application, |parent| {
4702                    SemanticOwner::entity(parent.as_semantic_id().clone())
4703                }),
4704        );
4705    }
4706    for blocked in component_instance_plan.blocked.values() {
4707        ownership.insert(
4708            blocked.id.as_semantic_id().clone(),
4709            SemanticOwner::entity(blocked.parent_instance.as_semantic_id().clone()),
4710        );
4711    }
4712
4713    for template in templates {
4714        let component = components
4715            .iter()
4716            .find(|component| component.id.template() == template.id)
4717            .expect("template graph should only contain component templates");
4718        ownership.insert(
4719            template.id.clone(),
4720            SemanticOwner::entity(component.id.clone()),
4721        );
4722    }
4723
4724    for binding in form_field_bindings.values() {
4725        ownership.insert(
4726            binding.id.as_semantic_id().clone(),
4727            SemanticOwner::entity(binding.control_entity.clone()),
4728        );
4729    }
4730
4731    for entity in template_entities {
4732        ownership.insert(entity.id.clone(), entity.owner.clone());
4733    }
4734
4735    ownership
4736}
4737
4738#[cfg(test)]
4739mod tests {
4740    use std::collections::BTreeMap;
4741
4742    use super::{
4743        build_application_semantic_model, build_application_semantic_model_for_unit,
4744        build_file_route_application_semantic_model_for_unit_with_packages,
4745        build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring,
4746        collect_ownership,
4747    };
4748    use crate::{
4749        build_component_graph_for_module, build_template_graph, build_template_manifest_from_asm,
4750        build_template_semantic_entities, build_v2_component_graph_for_module,
4751        CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredDeclarationV1,
4752        CanonicalAuthoredSemanticModelV1, CompilationUnit, ComponentInstancePlan, SemanticEntity,
4753        SemanticEntityKind, SemanticOwner, SemanticReferenceKind, TemplateSemanticKind,
4754    };
4755
4756    #[test]
4757    fn file_route_assembly_projects_explicit_canonical_v2_components_without_decorators() {
4758        let unit = CompilationUnit::parse_sources([(
4759            "app/routes/index.tsx",
4760            "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>; } }",
4761        )]);
4762        let model = CanonicalAuthoredSemanticModelV1 {
4763            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4764            source_path: "app/routes/index.tsx".into(),
4765            declarations: vec![
4766                CanonicalAuthoredDeclarationV1 {
4767                    kind: CanonicalAuthoredDeclarationKindV1::Component,
4768                    subject: "Home".into(),
4769                    source: crate::AuthoredSourceRangeV1 {
4770                        start: 37,
4771                        end: 106,
4772                        line: 1,
4773                        column: 38,
4774                    },
4775                    intrinsic_identity: None,
4776                    derived_evidence: None,
4777                },
4778                CanonicalAuthoredDeclarationV1 {
4779                    kind: CanonicalAuthoredDeclarationKindV1::State,
4780                    subject: "Home.count".into(),
4781                    source: crate::AuthoredSourceRangeV1 {
4782                        start: 83,
4783                        end: 99,
4784                        line: 1,
4785                        column: 84,
4786                    },
4787                    intrinsic_identity: None,
4788                    derived_evidence: None,
4789                },
4790                CanonicalAuthoredDeclarationV1 {
4791                    kind: CanonicalAuthoredDeclarationKindV1::Action,
4792                    subject: "Home.increment".into(),
4793                    source: crate::AuthoredSourceRangeV1 {
4794                        start: 100,
4795                        end: 148,
4796                        line: 1,
4797                        column: 101,
4798                    },
4799                    intrinsic_identity: None,
4800                    derived_evidence: None,
4801                },
4802            ],
4803        };
4804        let asm =
4805            build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
4806                &unit,
4807                &crate::SemanticPackageResolutionTable::default(),
4808                &BTreeMap::from([("app/routes/index.tsx".into(), model)]),
4809            )
4810            .expect("canonical V2 route assembly");
4811        assert_eq!(asm.components.len(), 1);
4812        assert_eq!(asm.components[0].class_name, "Home");
4813        assert_eq!(asm.components[0].state_fields[0].name, "count");
4814        assert!(asm.components[0].methods.is_empty());
4815        let endpoint = &asm.components[0].action_endpoints[0];
4816        assert_eq!(endpoint.name, "increment");
4817        assert_eq!(
4818            endpoint.id,
4819            asm.components[0].id.action_endpoint("increment")
4820        );
4821        assert_eq!(
4822            asm.components[0].actions[0].owner,
4823            SemanticOwner::entity(endpoint.id.clone())
4824        );
4825        let batch = asm
4826            .effect_trigger_plan
4827            .action_batches
4828            .values()
4829            .find(|batch| batch.authored_action_endpoint == endpoint.id)
4830            .expect("V2 field endpoint action batch");
4831        let manifest = build_template_manifest_from_asm(&asm);
4832        assert_eq!(
4833            manifest.components[0].template.events[0]
4834                .method_id
4835                .as_deref(),
4836            Some(endpoint.id.as_str())
4837        );
4838        assert_eq!(
4839            manifest.components[0].template.events[0]
4840                .action_batch_id
4841                .as_deref(),
4842            Some(batch.id.as_str())
4843        );
4844        assert!(asm
4845            .diagnostics
4846            .iter()
4847            .all(|diagnostic| diagnostic.code != "PSC1001"));
4848    }
4849
4850    #[test]
4851    fn file_route_assembly_projects_canonical_v2_form_into_existing_form_products() {
4852        let unit = CompilationUnit::parse_sources([(
4853            "app/routes/contact.tsx",
4854            "import { Component, defineForm, field, required, state } from \"presolve\"; export class Contact extends Component { contact = defineForm({ serialization: \"form-data\", fields: { email: field({ initial: \"\", validate: [required()] }) }, submit: async ({ value, signal }) => { this.submitted += 1; } }); submitted = state(0); render() { return <form form={this.contact}><input bind:value={this.contact.fields.email} /><button type=\"submit\">Send</button></form>; } }",
4855        )]);
4856        let parsed = &unit.files()[0];
4857        let class = &parsed.classes[0];
4858        let property = &class.properties[0];
4859        let field = &property
4860            .form_definition_shape
4861            .as_ref()
4862            .expect("static Form shape")
4863            .fields[0];
4864        let validation = &field.validations[0];
4865        let state_property = &class.properties[1];
4866        let model = CanonicalAuthoredSemanticModelV1 {
4867            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4868            source_path: parsed.path.clone(),
4869            declarations: vec![
4870                CanonicalAuthoredDeclarationV1 {
4871                    kind: CanonicalAuthoredDeclarationKindV1::Component,
4872                    subject: "Contact".into(),
4873                    source: crate::AuthoredSourceRangeV1 {
4874                        start: class.span.start,
4875                        end: class.span.end,
4876                        line: class.span.line,
4877                        column: class.span.column,
4878                    },
4879                    intrinsic_identity: None,
4880                    derived_evidence: None,
4881                },
4882                CanonicalAuthoredDeclarationV1 {
4883                    kind: CanonicalAuthoredDeclarationKindV1::FormField,
4884                    subject: "Contact.contact.email".into(),
4885                    source: crate::AuthoredSourceRangeV1 {
4886                        start: field.declaration_span.start,
4887                        end: field.declaration_span.end,
4888                        line: field.declaration_span.line,
4889                        column: field.declaration_span.column,
4890                    },
4891                    intrinsic_identity: None,
4892                    derived_evidence: None,
4893                },
4894                CanonicalAuthoredDeclarationV1 {
4895                    kind: CanonicalAuthoredDeclarationKindV1::Form,
4896                    subject: "Contact.contact".into(),
4897                    source: crate::AuthoredSourceRangeV1 {
4898                        start: property.span.start,
4899                        end: property.span.end,
4900                        line: property.span.line,
4901                        column: property.span.column,
4902                    },
4903                    intrinsic_identity: None,
4904                    derived_evidence: None,
4905                },
4906                CanonicalAuthoredDeclarationV1 {
4907                    kind: CanonicalAuthoredDeclarationKindV1::Validation,
4908                    subject: "Contact.contact.email.validation.0".into(),
4909                    source: crate::AuthoredSourceRangeV1 {
4910                        start: validation.span.start,
4911                        end: validation.span.end,
4912                        line: validation.span.line,
4913                        column: validation.span.column,
4914                    },
4915                    intrinsic_identity: Some(crate::ResolvedIntrinsicIdentityV1 {
4916                        name: "required".into(),
4917                        flags: 32,
4918                        declaration_modules: vec!["presolve".into()],
4919                    }),
4920                    derived_evidence: None,
4921                },
4922                CanonicalAuthoredDeclarationV1 {
4923                    kind: CanonicalAuthoredDeclarationKindV1::State,
4924                    subject: "Contact.submitted".into(),
4925                    source: crate::AuthoredSourceRangeV1 {
4926                        start: state_property.span.start,
4927                        end: state_property.span.end,
4928                        line: state_property.span.line,
4929                        column: state_property.span.column,
4930                    },
4931                    intrinsic_identity: None,
4932                    derived_evidence: None,
4933                },
4934            ],
4935        };
4936        let asm =
4937            build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
4938                &unit,
4939                &crate::SemanticPackageResolutionTable::default(),
4940                &BTreeMap::from([("app/routes/contact.tsx".into(), model)]),
4941            )
4942            .expect("canonical V2 Form route assembly");
4943        assert_eq!(asm.forms().len(), 1);
4944        assert_eq!(asm.forms()[0].name, "contact");
4945        assert_eq!(asm.form_declaration_candidates().len(), 1);
4946        assert_eq!(asm.form_fields().len(), 1);
4947        assert_eq!(asm.form_fields()[0].name, "email");
4948        assert_eq!(asm.form_fields()[0].path, ["email"]);
4949        assert_eq!(asm.form_field_bindings().len(), 1);
4950        assert_eq!(
4951            asm.form_field_bindings()[0].channel,
4952            crate::FormControlChannel::Value
4953        );
4954        assert_eq!(asm.validation_rules().len(), 1);
4955        assert_eq!(
4956            asm.validation_rules()[0].kind,
4957            crate::ValidationRuleKind::Required
4958        );
4959        assert_eq!(asm.submissions.plans.len(), 1);
4960        assert_eq!(asm.submission_hosts.len(), 1);
4961        assert_eq!(
4962            asm.serialization
4963                .plans
4964                .values()
4965                .next()
4966                .expect("Form serialization plan")
4967                .format,
4968            crate::FormSerializationFormat::FormData
4969        );
4970        assert_eq!(
4971            asm.form_declaration_candidates()[0].status,
4972            crate::FormDeclarationStatus::Valid
4973        );
4974    }
4975
4976    #[test]
4977    fn file_route_v2_slot_fields_bind_caller_content_through_the_existing_slot_model() {
4978        let unit = CompilationUnit::parse_sources([
4979            (
4980                "app/components/Panel.tsx",
4981                "import { Component, slot, type SlotContent } from \"presolve\"; export class Panel extends Component { children: SlotContent = slot(); render() { return <section><slot /></section>; } }",
4982            ),
4983            (
4984                "app/routes/index.tsx",
4985                "import { Component } from \"presolve\"; import { Panel } from \"../components/Panel\"; export class Index extends Component { render() { return <main><Panel><button>Projected</button></Panel></main>; } }",
4986            ),
4987        ]);
4988        let models = unit
4989            .files()
4990            .iter()
4991            .map(|parsed| {
4992                let component_sites = crate::component_inheritance_sites_v1(parsed);
4993                let components = crate::lower_component_inheritance_v1(
4994                    parsed,
4995                    component_sites
4996                        .into_iter()
4997                        .map(|site| crate::ResolvedComponentInheritanceV1 {
4998                            heritage_source: site.heritage_source,
4999                            component_identity: crate::ResolvedIntrinsicIdentityV1 {
5000                                name: "Component".into(),
5001                                flags: 32,
5002                                declaration_modules: vec!["presolve".into()],
5003                            },
5004                        }),
5005                )
5006                .expect("V2 component authority")
5007                .model;
5008                let slots = crate::slot_field_sites_v1(parsed, &components).expect("V2 Slot sites");
5009                let lowering = crate::lower_v2_authoring_v1(
5010                    parsed,
5011                    crate::V2AuthoringResolutionsV1 {
5012                        components: crate::component_inheritance_sites_v1(parsed)
5013                            .into_iter()
5014                            .map(|site| crate::ResolvedComponentInheritanceV1 {
5015                                heritage_source: site.heritage_source,
5016                                component_identity: crate::ResolvedIntrinsicIdentityV1 {
5017                                    name: "Component".into(),
5018                                    flags: 32,
5019                                    declaration_modules: vec!["presolve".into()],
5020                                },
5021                            })
5022                            .collect(),
5023                        states: Vec::new(),
5024                        actions: Vec::new(),
5025                        effects: Vec::new(),
5026                        slots: slots
5027                            .into_iter()
5028                            .map(|site| crate::ResolvedSlotFieldV1 {
5029                                callee_source: site.callee_source,
5030                                slot_identity: crate::ResolvedIntrinsicIdentityV1 {
5031                                    name: "slot".into(),
5032                                    flags: 32,
5033                                    declaration_modules: vec!["presolve".into()],
5034                                },
5035                            })
5036                            .collect(),
5037                        forms: Vec::new(),
5038                        form_fields: Vec::new(),
5039                        validations: Vec::new(),
5040                    },
5041                )
5042                .expect("V2 Slot lowering");
5043                (parsed.path.clone(), lowering.model)
5044            })
5045            .collect::<BTreeMap<_, _>>();
5046        let asm =
5047            build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
5048                &unit,
5049                &crate::SemanticPackageResolutionTable::default(),
5050                &models,
5051            )
5052            .expect("V2 Slot route assembly");
5053        assert_eq!(asm.slots().len(), 1);
5054        assert_eq!(asm.slot_content_fragments().len(), 1);
5055        assert_eq!(asm.slot_binding_registry().bindings.len(), 1);
5056    }
5057
5058    #[test]
5059    fn canonical_v2_action_rejects_unsupported_handler_before_publication() {
5060        let parsed = presolve_parser::parse_file(
5061            "app/routes/index.tsx",
5062            "import { Component, state, action } from \"presolve\"; export class Home extends Component { count = state(0); increment = action(() => { unrelated(); }); render() { return <main>Home</main>; } }",
5063        );
5064        let model = CanonicalAuthoredSemanticModelV1 {
5065            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
5066            source_path: parsed.path.clone(),
5067            declarations: vec![
5068                CanonicalAuthoredDeclarationV1 {
5069                    kind: CanonicalAuthoredDeclarationKindV1::Component,
5070                    subject: "Home".into(),
5071                    source: crate::AuthoredSourceRangeV1 {
5072                        start: 0,
5073                        end: parsed.syntax.source.len(),
5074                        line: 1,
5075                        column: 1,
5076                    },
5077                    intrinsic_identity: None,
5078                    derived_evidence: None,
5079                },
5080                CanonicalAuthoredDeclarationV1 {
5081                    kind: CanonicalAuthoredDeclarationKindV1::State,
5082                    subject: "Home.count".into(),
5083                    source: crate::AuthoredSourceRangeV1 {
5084                        start: 0,
5085                        end: 1,
5086                        line: 1,
5087                        column: 1,
5088                    },
5089                    intrinsic_identity: None,
5090                    derived_evidence: None,
5091                },
5092                CanonicalAuthoredDeclarationV1 {
5093                    kind: CanonicalAuthoredDeclarationKindV1::Action,
5094                    subject: "Home.increment".into(),
5095                    source: crate::AuthoredSourceRangeV1 {
5096                        start: 0,
5097                        end: 1,
5098                        line: 1,
5099                        column: 1,
5100                    },
5101                    intrinsic_identity: None,
5102                    derived_evidence: None,
5103                },
5104            ],
5105        };
5106        let graph = build_v2_component_graph_for_module(&parsed, &model);
5107        assert!(graph.components[0].action_endpoints.is_empty());
5108        assert!(graph
5109            .diagnostics
5110            .iter()
5111            .any(|diagnostic| diagnostic.code == "PSV2A1004"));
5112    }
5113
5114    #[test]
5115    fn canonical_v2_action_projects_typed_parameter_ordinals_and_rejects_bad_events() {
5116        let parsed = presolve_parser::parse_file(
5117            "app/routes/index.tsx",
5118            "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>; } }",
5119        );
5120        let model = CanonicalAuthoredSemanticModelV1 {
5121            schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
5122            source_path: parsed.path.clone(),
5123            declarations: vec![
5124                CanonicalAuthoredDeclarationV1 {
5125                    kind: CanonicalAuthoredDeclarationKindV1::Component,
5126                    subject: "Home".into(),
5127                    source: crate::AuthoredSourceRangeV1 {
5128                        start: 0,
5129                        end: parsed.syntax.source.len(),
5130                        line: 1,
5131                        column: 1,
5132                    },
5133                    intrinsic_identity: None,
5134                    derived_evidence: None,
5135                },
5136                CanonicalAuthoredDeclarationV1 {
5137                    kind: CanonicalAuthoredDeclarationKindV1::State,
5138                    subject: "Home.count".into(),
5139                    source: crate::AuthoredSourceRangeV1 {
5140                        start: 0,
5141                        end: 1,
5142                        line: 1,
5143                        column: 1,
5144                    },
5145                    intrinsic_identity: None,
5146                    derived_evidence: None,
5147                },
5148                CanonicalAuthoredDeclarationV1 {
5149                    kind: CanonicalAuthoredDeclarationKindV1::Action,
5150                    subject: "Home.setExact".into(),
5151                    source: crate::AuthoredSourceRangeV1 {
5152                        start: 0,
5153                        end: 1,
5154                        line: 1,
5155                        column: 1,
5156                    },
5157                    intrinsic_identity: None,
5158                    derived_evidence: None,
5159                },
5160                CanonicalAuthoredDeclarationV1 {
5161                    kind: CanonicalAuthoredDeclarationKindV1::Action,
5162                    subject: "Home.setLocal".into(),
5163                    source: crate::AuthoredSourceRangeV1 {
5164                        start: 0,
5165                        end: 1,
5166                        line: 1,
5167                        column: 1,
5168                    },
5169                    intrinsic_identity: None,
5170                    derived_evidence: None,
5171                },
5172            ],
5173        };
5174        let graph = build_v2_component_graph_for_module(&parsed, &model);
5175        assert_eq!(graph.components[0].actions.len(), 2);
5176        assert!(matches!(
5177            graph.components[0].actions[0].operation,
5178            crate::StateOperation::AssignParameter(ref ordinal) if ordinal == "0"
5179        ));
5180        assert!(matches!(
5181            graph.components[0].actions[1].operation,
5182            crate::StateOperation::Assign(crate::SerializableValue::Number(ref value)) if value == "23"
5183        ));
5184        assert!(graph
5185            .diagnostics
5186            .iter()
5187            .any(|diagnostic| diagnostic.code == "PSV2A1006"));
5188    }
5189
5190    #[test]
5191    fn lowers_supported_primitive_state_annotations_into_canonical_types() {
5192        let parsed = presolve_parser::parse_file(
5193            "src/TypedState.tsx",
5194            r#"
5195@component("x-typed-state")
5196class TypedState extends Component {
5197  count: number = state(0);
5198}
5199"#,
5200        );
5201
5202        let asm = build_application_semantic_model(&parsed);
5203
5204        let field = &asm.components[0].state_fields[0];
5205        let assignment = asm
5206            .semantic_types
5207            .assignments
5208            .get(&field.id)
5209            .expect("canonical declared type");
5210
5211        assert_eq!(assignment.semantic_type, crate::SemanticType::Number);
5212        assert_eq!(assignment.status, crate::SemanticTypeStatus::Declared);
5213        assert_eq!(
5214            assignment.provenance,
5215            field.declared_type.as_ref().unwrap().provenance
5216        );
5217    }
5218
5219    #[test]
5220    fn queries_canonical_type_information_from_the_asm() {
5221        let parsed = presolve_parser::parse_file(
5222            "src/TypeQueries.tsx",
5223            r#"
5224@component("x-type-queries")
5225class TypeQueries extends Component {
5226  count: number = state(0);
5227  label = state("Presolve");
5228}
5229"#,
5230        );
5231        let asm = build_application_semantic_model(&parsed);
5232        let count = &asm.components[0].state_fields[0];
5233        let label = &asm.components[0].state_fields[1];
5234
5235        assert_eq!(
5236            asm.semantic_type_of(&count.id),
5237            Some(&crate::SemanticType::Number)
5238        );
5239        assert_eq!(asm.type_declarations(&crate::SemanticType::Number).len(), 1);
5240        assert_eq!(asm.type_usages(&crate::SemanticType::String).len(), 1);
5241        assert_eq!(
5242            asm.serialization_compatibility_of(&count.id),
5243            Some(crate::SerializationCompatibility::Serializable)
5244        );
5245        assert_eq!(asm.is_type_assignable(&label.id, &count.id), Some(false));
5246    }
5247
5248    #[test]
5249    fn lowers_typed_method_parameters_into_canonical_entities() {
5250        let parsed = presolve_parser::parse_file(
5251            "src/Parameters.tsx",
5252            r#"
5253@component("x-parameters")
5254class Parameters extends Component {
5255  save(title: string, retries?: number) {}
5256}
5257"#,
5258        );
5259
5260        let asm = build_application_semantic_model(&parsed);
5261        let method = &asm.components[0].methods[0];
5262        let parameters = &method.parameters;
5263
5264        assert_eq!(parameters.len(), 2);
5265        assert_eq!(
5266            asm.semantic_types.assignments[&parameters[0].id].semantic_type,
5267            crate::SemanticType::String
5268        );
5269        assert_eq!(
5270            asm.semantic_types.assignments[&parameters[1].id].semantic_type,
5271            crate::SemanticType::Number
5272        );
5273        assert!(parameters.iter().all(|parameter| {
5274            asm.owner(&parameter.id) == Some(&SemanticOwner::entity(method.id.clone()))
5275                && asm
5276                    .entity(&parameter.id)
5277                    .is_some_and(|entity| entity.kind() == SemanticEntityKind::Parameter)
5278        }));
5279    }
5280
5281    #[test]
5282    fn lowers_declared_and_inferred_method_return_types() {
5283        let parsed = presolve_parser::parse_file(
5284            "src/Returns.tsx",
5285            r#"
5286@component("x-returns")
5287class Returns extends Component {
5288  declared(): string { return "Presolve"; }
5289  inferred() { return 1; }
5290}
5291"#,
5292        );
5293
5294        let asm = build_application_semantic_model(&parsed);
5295        let methods = &asm.components[0].methods;
5296        let declared = &methods[0];
5297        let inferred = &methods[1];
5298
5299        assert_eq!(
5300            asm.semantic_types.assignments[&declared.id].semantic_type,
5301            crate::SemanticType::String
5302        );
5303        assert_eq!(
5304            asm.semantic_types.assignments[&declared.id].status,
5305            crate::SemanticTypeStatus::Declared
5306        );
5307        assert_eq!(
5308            asm.semantic_types.assignments[&inferred.id].semantic_type,
5309            crate::SemanticType::Number
5310        );
5311        assert_eq!(
5312            asm.semantic_types.assignments[&inferred.id].status,
5313            crate::SemanticTypeStatus::Inferred
5314        );
5315    }
5316
5317    #[test]
5318    fn establishes_typed_computed_getter_contracts() {
5319        let parsed = presolve_parser::parse_file(
5320            "src/Computed.tsx",
5321            r#"
5322@component("x-computed")
5323class Computed extends Component {
5324  @computed()
5325  get remainingCount(): number { return 1; }
5326
5327  render() { return <p />; }
5328}
5329"#,
5330        );
5331
5332        let asm = build_application_semantic_model(&parsed);
5333        let method = &asm.components[0].methods[0];
5334        let computed_id = asm.components[0].id.computed("remainingCount");
5335        let computed = asm
5336            .semantic_types
5337            .computed_values
5338            .get(&computed_id)
5339            .expect("computed type contract");
5340        let computed_entity = asm
5341            .computed_value(&computed_id)
5342            .expect("first-class computed entity");
5343
5344        assert!(method.is_getter);
5345        assert!(method.is_computed());
5346        assert_eq!(
5347            computed.semantic_type,
5348            crate::SemanticType::NumberLiteral("1".to_string())
5349        );
5350        assert_eq!(
5351            computed.declared_return_type,
5352            Some(crate::SemanticType::Number)
5353        );
5354        assert_eq!(computed.declared_return_compatible, Some(true));
5355        assert_eq!(computed_entity.method, method.id);
5356        assert_eq!(
5357            computed_entity.owner,
5358            crate::SemanticOwner::entity(asm.components[0].id.clone())
5359        );
5360        assert_eq!(
5361            computed_entity.cache_policy,
5362            crate::ComputedCachePolicy::Memoized
5363        );
5364        assert_eq!(computed_entity.purity, crate::ComputedPurity::Pure);
5365        assert!(computed_entity.purity_violations.is_empty());
5366        assert_eq!(
5367            computed_entity.execution_boundary,
5368            crate::ExecutionBoundary::Client
5369        );
5370        assert_eq!(
5371            asm.entity(&computed_id),
5372            Some(SemanticEntity::Computed(computed_entity))
5373        );
5374        assert_eq!(
5375            asm.owner(&computed_id),
5376            Some(&crate::SemanticOwner::entity(asm.components[0].id.clone()))
5377        );
5378        assert_eq!(asm.provenance(&computed_id), asm.provenance(&method.id));
5379        assert_eq!(
5380            asm.entities_of_kind(SemanticEntityKind::Computed),
5381            vec![&computed_id]
5382        );
5383    }
5384
5385    #[test]
5386    fn resolves_computed_reads_to_canonical_state_and_computed_references() {
5387        let parsed = presolve_parser::parse_file(
5388            "src/ComputedReads.tsx",
5389            r#"
5390@component("x-computed-reads")
5391class ComputedReads extends Component {
5392  count = state(1);
5393  profile = state({ hidden: false });
5394
5395  @computed()
5396  get doubled() { return this.count * 2; }
5397
5398  @computed()
5399  get visible() { return this.doubled + this.count + this.count; }
5400
5401  @computed()
5402  get profileHidden() { return this.profile.hidden; }
5403
5404  @computed()
5405  get unresolved() { return this.missing; }
5406}
5407"#,
5408        );
5409        let asm = build_application_semantic_model(&parsed);
5410        let component = &asm.components[0];
5411        let count = component.id.state_field("count");
5412        let profile = component.id.state_field("profile");
5413        let doubled = component.id.computed("doubled");
5414        let visible = component.id.computed("visible");
5415        let profile_hidden = component.id.computed("profileHidden");
5416        let unresolved = component.id.computed("unresolved");
5417
5418        let state_references = asm.references_of_kind(SemanticReferenceKind::ComputedState);
5419        assert_eq!(state_references.len(), 3);
5420        assert!(state_references
5421            .iter()
5422            .any(|reference| reference.source == doubled && reference.target == count));
5423        assert!(state_references
5424            .iter()
5425            .any(|reference| reference.source == visible && reference.target == count));
5426        assert!(state_references.iter().any(|reference| {
5427            reference.source == profile_hidden && reference.target == profile
5428        }));
5429
5430        let computed_references = asm.references_of_kind(SemanticReferenceKind::ComputedComputed);
5431        assert_eq!(computed_references.len(), 1);
5432        assert_eq!(computed_references[0].source, visible);
5433        assert_eq!(computed_references[0].target, doubled);
5434        assert!(!asm
5435            .references
5436            .iter()
5437            .any(|reference| reference.source == unresolved));
5438        assert!(asm
5439            .references
5440            .iter()
5441            .all(|reference| { asm.provenance(&reference.source) == Some(&reference.provenance) }));
5442    }
5443
5444    #[test]
5445    fn populates_reactive_graph_from_direct_computed_reads() {
5446        let parsed = presolve_parser::parse_file(
5447            "src/ComputedReactiveGraph.tsx",
5448            r#"
5449@component("x-computed-reactive-graph")
5450class ComputedReactiveGraph extends Component {
5451  count = state(1);
5452
5453  @computed()
5454  get doubled() { return this.count * 2; }
5455
5456  @computed()
5457  get label() { return this.doubled + 1; }
5458}
5459"#,
5460        );
5461        let asm = build_application_semantic_model(&parsed);
5462        let component = &asm.components[0];
5463        let count = component.id.state_field("count");
5464        let doubled = component.id.computed("doubled");
5465        let label = component.id.computed("label");
5466        let graph = &asm.reactive_graph;
5467
5468        assert_eq!(graph.nodes.len(), 3);
5469        assert_eq!(
5470            graph.nodes[count.as_str()].kind,
5471            crate::IrReactiveNodeKind::State
5472        );
5473        assert_eq!(
5474            graph.nodes[doubled.as_str()].kind,
5475            crate::IrReactiveNodeKind::Computed
5476        );
5477        assert_eq!(
5478            graph.nodes[label.as_str()].kind,
5479            crate::IrReactiveNodeKind::Computed
5480        );
5481
5482        let doubled_dependencies = graph.computed_dependencies(doubled.as_str());
5483        assert_eq!(doubled_dependencies.len(), 1);
5484        assert_eq!(doubled_dependencies[0].target, count.as_str());
5485        let label_dependencies = graph.computed_dependencies(label.as_str());
5486        assert_eq!(label_dependencies.len(), 1);
5487        assert_eq!(label_dependencies[0].target, doubled.as_str());
5488
5489        let count_invalidations = graph.invalidations_from(count.as_str());
5490        assert_eq!(count_invalidations.len(), 1);
5491        assert_eq!(count_invalidations[0].target, doubled.as_str());
5492        let doubled_invalidations = graph.invalidations_from(doubled.as_str());
5493        assert_eq!(doubled_invalidations.len(), 1);
5494        assert_eq!(doubled_invalidations[0].target, label.as_str());
5495        assert!(graph
5496            .invalidations_from(count.as_str())
5497            .iter()
5498            .all(|edge| { edge.target != label.as_str() }));
5499    }
5500
5501    #[test]
5502    fn computes_deterministic_transitive_reactive_dependencies_and_dependents() {
5503        let parsed = presolve_parser::parse_file(
5504            "src/ComputedTransitiveReactiveGraph.tsx",
5505            r#"
5506@component("x-computed-transitive-reactive-graph")
5507class ComputedTransitiveReactiveGraph extends Component {
5508  count = state(1);
5509
5510  @computed()
5511  get doubled() { return this.count * 2; }
5512
5513  @computed()
5514  get label() { return this.doubled + 1; }
5515}
5516"#,
5517        );
5518        let asm = build_application_semantic_model(&parsed);
5519        let component = &asm.components[0];
5520        let count = component.id.state_field("count");
5521        let doubled = component.id.computed("doubled");
5522        let label = component.id.computed("label");
5523        let analysis = &asm.reactive_transitive_analysis;
5524
5525        assert_eq!(analysis.dependencies.len(), 3);
5526        assert_eq!(analysis.dependents.len(), 3);
5527        assert_eq!(
5528            analysis.dependencies_of(count.as_str()),
5529            Vec::<String>::new()
5530        );
5531        assert_eq!(
5532            analysis.dependencies_of(doubled.as_str()),
5533            vec![count.as_str().to_string()]
5534        );
5535        assert_eq!(
5536            analysis.dependencies_of(label.as_str()),
5537            vec![doubled.as_str().to_string(), count.as_str().to_string()]
5538        );
5539        assert_eq!(
5540            analysis.dependents_of(count.as_str()),
5541            vec![doubled.as_str().to_string(), label.as_str().to_string()]
5542        );
5543        assert_eq!(
5544            analysis.dependents_of(doubled.as_str()),
5545            vec![label.as_str().to_string()]
5546        );
5547        assert_eq!(analysis.dependents_of(label.as_str()), Vec::<String>::new());
5548    }
5549
5550    #[test]
5551    fn detects_computed_dependency_cycles_with_stable_diagnostics() {
5552        let parsed = presolve_parser::parse_file(
5553            "src/ComputedDependencyCycles.tsx",
5554            r#"
5555@component("x-computed-dependency-cycles")
5556class ComputedDependencyCycles extends Component {
5557  @computed()
5558  get alpha() { return this.beta; }
5559
5560  @computed()
5561  get beta() { return this.alpha; }
5562
5563  @computed()
5564  get selfLoop() { return this.selfLoop; }
5565}
5566"#,
5567        );
5568        let asm = build_application_semantic_model(&parsed);
5569        let component = &asm.components[0];
5570        let alpha = component.id.computed("alpha");
5571        let beta = component.id.computed("beta");
5572        let self_loop = component.id.computed("selfLoop");
5573
5574        assert_eq!(
5575            asm.reactive_cycle_analysis.cycles,
5576            vec![
5577                crate::IrReactiveCycle {
5578                    nodes: vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5579                },
5580                crate::IrReactiveCycle {
5581                    nodes: vec![self_loop.as_str().to_string()],
5582                },
5583            ]
5584        );
5585        let diagnostics = asm
5586            .diagnostics
5587            .iter()
5588            .filter(|diagnostic| diagnostic.code == "PSC1035")
5589            .collect::<Vec<_>>();
5590        assert_eq!(diagnostics.len(), 2);
5591        assert_eq!(
5592            diagnostics[0].message,
5593            format!("computed dependency cycle detected among: {alpha}, {beta}")
5594        );
5595        assert_eq!(
5596            diagnostics[0].provenance,
5597            Some(
5598                asm.computed_value(&alpha)
5599                    .expect("alpha computed value")
5600                    .provenance
5601                    .clone()
5602            )
5603        );
5604        assert_eq!(
5605            diagnostics[1].message,
5606            format!("computed dependency cycle detected among: {self_loop}")
5607        );
5608        assert_eq!(
5609            diagnostics[1].provenance,
5610            Some(
5611                asm.computed_value(&self_loop)
5612                    .expect("self-loop computed value")
5613                    .provenance
5614                    .clone()
5615            )
5616        );
5617    }
5618
5619    #[test]
5620    fn plans_deterministic_computed_evaluation_order_and_batches() {
5621        let parsed = presolve_parser::parse_file(
5622            "src/ComputedEvaluationPlan.tsx",
5623            r#"
5624@component("x-computed-evaluation-plan")
5625class ComputedEvaluationPlan extends Component {
5626  count = state(1);
5627  other = state(2);
5628
5629  @computed()
5630  get alpha() { return this.count; }
5631
5632  @computed()
5633  get beta() { return this.other; }
5634
5635  @computed()
5636  get gamma() { return this.alpha; }
5637}
5638"#,
5639        );
5640        let asm = build_application_semantic_model(&parsed);
5641        let component = &asm.components[0];
5642        let alpha = component.id.computed("alpha");
5643        let beta = component.id.computed("beta");
5644        let gamma = component.id.computed("gamma");
5645
5646        assert_eq!(
5647            asm.computed_evaluation_plan.evaluation_order,
5648            vec![
5649                alpha.as_str().to_string(),
5650                beta.as_str().to_string(),
5651                gamma.as_str().to_string(),
5652            ]
5653        );
5654        assert_eq!(
5655            asm.computed_evaluation_plan.update_batches,
5656            vec![
5657                vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5658                vec![gamma.as_str().to_string()],
5659            ]
5660        );
5661        assert!(asm.computed_evaluation_plan.unplanned.is_empty());
5662    }
5663
5664    #[test]
5665    fn leaves_cyclic_computed_values_unplanned() {
5666        let parsed = presolve_parser::parse_file(
5667            "src/CyclicComputedEvaluationPlan.tsx",
5668            r#"
5669@component("x-cyclic-computed-evaluation-plan")
5670class CyclicComputedEvaluationPlan extends Component {
5671  @computed()
5672  get alpha() { return this.beta; }
5673
5674  @computed()
5675  get beta() { return this.alpha; }
5676}
5677"#,
5678        );
5679        let asm = build_application_semantic_model(&parsed);
5680        let component = &asm.components[0];
5681        let alpha = component.id.computed("alpha");
5682        let beta = component.id.computed("beta");
5683
5684        assert!(asm.computed_evaluation_plan.evaluation_order.is_empty());
5685        assert!(asm.computed_evaluation_plan.update_batches.is_empty());
5686        assert_eq!(
5687            asm.computed_evaluation_plan.unplanned,
5688            vec![alpha.as_str().to_string(), beta.as_str().to_string()]
5689        );
5690    }
5691
5692    #[test]
5693    fn assigns_inferred_computed_types_and_validates_declared_returns() {
5694        let parsed = presolve_parser::parse_file(
5695            "src/ComputedTypes.tsx",
5696            r#"
5697@component("x-computed-types")
5698class ComputedTypes extends Component {
5699  count: number = state(1);
5700  profile = state({ label: "Presolve" });
5701
5702  @computed()
5703  get doubled(): number { return this.count * 2; }
5704
5705  @computed()
5706  get chained() { return this.doubled + 1; }
5707
5708  @computed()
5709  get label(): string { return this.profile.label; }
5710
5711  @computed()
5712  get invalid(): number { return "wrong"; }
5713
5714  @computed()
5715  get unresolved() { return this.missing; }
5716}
5717"#,
5718        );
5719        let asm = build_application_semantic_model(&parsed);
5720        let component = &asm.components[0];
5721        let doubled = component.id.computed("doubled");
5722        let chained = component.id.computed("chained");
5723        let label = component.id.computed("label");
5724        let invalid = component.id.computed("invalid");
5725        let unresolved = component.id.computed("unresolved");
5726
5727        let types = &asm.semantic_types.computed_values;
5728        assert_eq!(types[&doubled].semantic_type, crate::SemanticType::Number);
5729        assert_eq!(types[&chained].semantic_type, crate::SemanticType::Number);
5730        assert_eq!(types[&label].semantic_type, crate::SemanticType::String);
5731        assert_eq!(
5732            types[&invalid].semantic_type,
5733            crate::SemanticType::StringLiteral("wrong".to_string())
5734        );
5735        assert_eq!(types[&invalid].declared_return_compatible, Some(false));
5736        assert_eq!(types[&doubled].declared_return_compatible, Some(true));
5737        assert_eq!(
5738            types[&unresolved].semantic_type,
5739            crate::SemanticType::Unknown
5740        );
5741        assert_eq!(
5742            types[&unresolved].serialization,
5743            crate::SerializationCompatibility::NotSerializable
5744        );
5745        assert_eq!(
5746            types[&doubled].boundary_compatibility,
5747            crate::BoundaryCompatibility::Compatible
5748        );
5749        assert_eq!(
5750            types[&doubled].execution_boundary,
5751            crate::ExecutionBoundary::Client
5752        );
5753        assert_eq!(
5754            asm.semantic_type_of(&doubled),
5755            Some(&crate::SemanticType::Number)
5756        );
5757        assert_eq!(
5758            asm.serialization_compatibility_of(&chained),
5759            Some(crate::SerializationCompatibility::Serializable)
5760        );
5761    }
5762
5763    #[test]
5764    fn reports_stable_diagnostics_for_computed_semantics() {
5765        let parsed = presolve_parser::parse_file(
5766            "src/ComputedDiagnostics.tsx",
5767            r#"
5768@component("x-computed-diagnostics")
5769class ComputedDiagnostics extends Component {
5770  count = state(1);
5771
5772  @computed()
5773  invalidDeclaration() { return 1; }
5774
5775  @computed()
5776  get unsupportedBody() {
5777    const value = this.count;
5778    return value;
5779  }
5780
5781  @computed()
5782  get unresolvedRead() { return this.missing; }
5783
5784  @computed()
5785  get mismatch(): number { return "wrong"; }
5786
5787  @computed()
5788  get impure() { return helper(); }
5789
5790  @computed()
5791  get cycleA() { return this.cycleB; }
5792
5793  @computed()
5794  get cycleB() { return this.cycleA; }
5795
5796  render() { return <div />; }
5797}
5798"#,
5799        );
5800
5801        let asm = build_application_semantic_model(&parsed);
5802        let repeated = build_application_semantic_model(&parsed);
5803        assert_eq!(asm.diagnostics, repeated.diagnostics);
5804
5805        let component_graph = crate::build_component_graph(&parsed);
5806        let asm_from_component_graph =
5807            super::build_application_semantic_model_from_component_graph(&component_graph);
5808
5809        let codes = asm
5810            .diagnostics
5811            .iter()
5812            .map(|diagnostic| diagnostic.code.as_str())
5813            .collect::<std::collections::BTreeSet<_>>();
5814        assert_eq!(
5815            codes,
5816            std::collections::BTreeSet::from([
5817                "PSC1034", "PSC1035", "PSC1036", "PSC1037", "PSC1038", "PSC1039", "PSC1040",
5818            ])
5819        );
5820        assert_eq!(
5821            asm_from_component_graph
5822                .diagnostics
5823                .iter()
5824                .map(|diagnostic| diagnostic.code.as_str())
5825                .collect::<std::collections::BTreeSet<_>>(),
5826            codes
5827        );
5828        assert!(asm
5829            .diagnostics
5830            .iter()
5831            .all(|diagnostic| diagnostic.provenance.is_some()));
5832        assert!(asm.diagnostics.iter().any(|diagnostic| {
5833            diagnostic.code == crate::ComputedDiagnosticCode::InvalidDeclaration.as_str()
5834                && diagnostic.message
5835                    == "@computed() declaration `invalidDeclaration` must decorate a getter"
5836        }));
5837        assert!(asm.diagnostics.iter().any(|diagnostic| {
5838            diagnostic.code == crate::ComputedDiagnosticCode::UnsupportedBody.as_str()
5839                && diagnostic.message == "computed getter `unsupportedBody` has an unsupported body"
5840        }));
5841        assert!(asm.diagnostics.iter().any(|diagnostic| {
5842            diagnostic.code == crate::ComputedDiagnosticCode::UnresolvedRead.as_str()
5843                && diagnostic.message
5844                    == "computed getter `unresolvedRead` reads unresolved member `this.missing`"
5845        }));
5846        assert!(asm.diagnostics.iter().any(|diagnostic| {
5847            diagnostic.code == crate::ComputedDiagnosticCode::TypeMismatch.as_str()
5848                && diagnostic.message
5849                    == "computed getter `mismatch` returns `\"wrong\"` but declares `number`"
5850        }));
5851        assert!(asm.diagnostics.iter().any(|diagnostic| {
5852            diagnostic.code == crate::ComputedDiagnosticCode::SerializationViolation.as_str()
5853                && diagnostic.message
5854                    == "computed getter `unresolvedRead` has a non-serializable result"
5855        }));
5856    }
5857
5858    #[test]
5859    fn phase_e_audit_preserves_canonical_computed_products() {
5860        let path = "fixtures/0047-computed-diamond/input/ComputedDiamond.tsx";
5861        let parsed = presolve_parser::parse_file(
5862            path,
5863            include_str!("../../../fixtures/0047-computed-diamond/input/ComputedDiamond.tsx"),
5864        );
5865        let asm = build_application_semantic_model(&parsed);
5866        let component_graph = crate::build_component_graph(&parsed);
5867        let asm_from_component_graph =
5868            super::build_application_semantic_model_from_component_graph(&component_graph);
5869        let component = &asm.components[0];
5870        let count = component.id.state_field("count");
5871        let doubled = component.id.computed("doubled");
5872        let tripled = component.id.computed("tripled");
5873        let total = component.id.computed("total");
5874        let expected_owner = crate::SemanticOwner::entity(component.id.clone());
5875
5876        assert!(crate::validate_application_semantic_model(&asm).is_empty());
5877        assert!(crate::validate_application_semantic_model(&asm_from_component_graph).is_empty());
5878        for computed in [&doubled, &tripled, &total] {
5879            assert!(matches!(
5880                asm.entity(computed),
5881                Some(crate::SemanticEntity::Computed(_))
5882            ));
5883            assert_eq!(asm.owner(computed), Some(&expected_owner));
5884            assert_eq!(
5885                asm.semantic_types
5886                    .computed_values
5887                    .get(computed)
5888                    .map(|computed_type| &computed_type.computed),
5889                Some(computed)
5890            );
5891        }
5892
5893        let reactive_references = asm
5894            .references
5895            .iter()
5896            .filter(|reference| {
5897                matches!(
5898                    reference.kind,
5899                    crate::SemanticReferenceKind::ComputedState
5900                        | crate::SemanticReferenceKind::ComputedComputed
5901                )
5902            })
5903            .collect::<Vec<_>>();
5904        assert_eq!(reactive_references.len(), 4);
5905        for reference in reactive_references {
5906            assert!(asm.reactive_graph.edges.iter().any(|edge| {
5907                edge.kind == crate::IrReactiveEdgeKind::Reads
5908                    && edge.source == reference.source.as_str()
5909                    && edge.target == reference.target.as_str()
5910                    && edge.provenance == reference.provenance
5911            }));
5912            assert!(asm.reactive_graph.edges.iter().any(|edge| {
5913                edge.kind == crate::IrReactiveEdgeKind::Invalidates
5914                    && edge.source == reference.target.as_str()
5915                    && edge.target == reference.source.as_str()
5916                    && edge.provenance == reference.provenance
5917            }));
5918        }
5919        assert!(asm.reactive_graph.nodes.contains_key(count.as_str()));
5920        assert_eq!(
5921            asm.computed_evaluation_plan,
5922            crate::plan_computed_evaluation(&asm.reactive_graph)
5923        );
5924        assert_eq!(
5925            asm.computed_evaluation_plan.evaluation_order,
5926            vec![
5927                doubled.as_str().to_string(),
5928                tripled.as_str().to_string(),
5929                total.as_str().to_string(),
5930            ]
5931        );
5932
5933        let ir = crate::lower_components_to_ir(&asm);
5934        assert!(crate::validate_intermediate_representation(&ir).is_empty());
5935        let evaluations = ir
5936            .modules
5937            .iter()
5938            .flat_map(|module| &module.computed_evaluations)
5939            .map(|evaluation| evaluation.computed.clone())
5940            .collect::<Vec<_>>();
5941        assert_eq!(evaluations, vec![doubled, tripled, total]);
5942    }
5943
5944    #[test]
5945    fn classifies_computed_purity_and_reports_unsupported_behavior() {
5946        let mut parsed = presolve_parser::parse_file(
5947            "src/ComputedPurity.tsx",
5948            r#"
5949@component("x-computed-purity")
5950class ComputedPurity extends Component {
5951  count = state(1);
5952
5953  @action()
5954  save() {}
5955
5956  @effect()
5957  sync() {}
5958
5959  @computed()
5960  get pure() { return this.count; }
5961
5962  @computed()
5963  get mutates() { this.count++; return 1; }
5964
5965  @computed()
5966  get actionCall() { return this.save(); }
5967
5968  @computed()
5969  get effectCall() { console.log("effect"); return 1; }
5970
5971  @computed()
5972  get resourceCall() { return resource(); }
5973
5974  @computed()
5975  get arbitraryCall() { return helper(); }
5976
5977  @computed()
5978  get random() { return Math.random(); }
5979
5980  @computed()
5981  get asyncValue() { return 1; }
5982}
5983"#,
5984        );
5985        parsed.classes[0]
5986            .methods
5987            .iter_mut()
5988            .find(|method| method.name == "asyncValue")
5989            .expect("async test getter")
5990            .is_async = true;
5991
5992        let asm = build_application_semantic_model(&parsed);
5993        let component = &asm.components[0];
5994        let pure = component.id.computed("pure");
5995        let mutates = component.id.computed("mutates");
5996        let action_call = component.id.computed("actionCall");
5997        let effect_call = component.id.computed("effectCall");
5998        let resource_call = component.id.computed("resourceCall");
5999        let arbitrary_call = component.id.computed("arbitraryCall");
6000        let random = component.id.computed("random");
6001        let async_value = component.id.computed("asyncValue");
6002
6003        assert_eq!(
6004            asm.computed_value(&pure).expect("pure value").purity,
6005            crate::ComputedPurity::Pure
6006        );
6007        for (computed, kind) in [
6008            (mutates, crate::ComputedPurityViolationKind::StateMutation),
6009            (action_call, crate::ComputedPurityViolationKind::Action),
6010            (effect_call, crate::ComputedPurityViolationKind::Effect),
6011            (resource_call, crate::ComputedPurityViolationKind::Resource),
6012            (
6013                arbitrary_call,
6014                crate::ComputedPurityViolationKind::ArbitraryMethodCall,
6015            ),
6016            (
6017                random,
6018                crate::ComputedPurityViolationKind::NondeterministicOperation,
6019            ),
6020            (async_value, crate::ComputedPurityViolationKind::Async),
6021        ] {
6022            let value = asm.computed_value(&computed).expect("computed value");
6023            assert_eq!(value.purity, crate::ComputedPurity::Impure);
6024            assert!(value
6025                .purity_violations
6026                .iter()
6027                .any(|violation| violation.kind == kind));
6028        }
6029        let diagnostics = asm
6030            .diagnostics
6031            .iter()
6032            .filter(|diagnostic| diagnostic.code == "PSC1034")
6033            .collect::<Vec<_>>();
6034        assert_eq!(diagnostics.len(), 7);
6035        assert!(diagnostics
6036            .iter()
6037            .all(|diagnostic| diagnostic.provenance.is_some()));
6038    }
6039
6040    #[test]
6041    fn establishes_typed_action_input_and_output_contracts() {
6042        let parsed = presolve_parser::parse_file(
6043            "src/Action.tsx",
6044            r#"
6045type ActionResult = number;
6046
6047@component("x-action")
6048class Action extends Component {
6049  @action()
6050  async addTodo(input: string): ActionResult { return 1; }
6051}
6052"#,
6053        );
6054
6055        let asm = build_application_semantic_model(&parsed);
6056        let method = &asm.components[0].methods[0];
6057        let signature = asm
6058            .semantic_types
6059            .action_signatures
6060            .get(&method.id)
6061            .expect("action signature");
6062
6063        assert!(method.is_action());
6064        assert!(signature.is_async);
6065        assert_eq!(signature.input.len(), 1);
6066        assert_eq!(signature.input[0].1, crate::SemanticType::String);
6067        assert_eq!(signature.output, Some(crate::SemanticType::Number));
6068    }
6069
6070    #[test]
6071    fn lowers_supported_literal_state_annotations_into_canonical_types() {
6072        let parsed = presolve_parser::parse_file(
6073            "src/LiteralTypes.tsx",
6074            r#"
6075@component("x-literal-types")
6076class LiteralTypes extends Component {
6077  filter: "all" = state("all");
6078  step: 42 = state(42);
6079  enabled: true = state(true);
6080}
6081"#,
6082        );
6083
6084        let asm = build_application_semantic_model(&parsed);
6085        let types = asm.components[0]
6086            .state_fields
6087            .iter()
6088            .map(|field| {
6089                (
6090                    field.name.as_str(),
6091                    &asm.semantic_types.assignments[&field.id].semantic_type,
6092                )
6093            })
6094            .collect::<Vec<_>>();
6095
6096        assert_eq!(
6097            types,
6098            vec![
6099                (
6100                    "filter",
6101                    &crate::SemanticType::StringLiteral("all".to_string())
6102                ),
6103                (
6104                    "step",
6105                    &crate::SemanticType::NumberLiteral("42".to_string())
6106                ),
6107                ("enabled", &crate::SemanticType::BooleanLiteral(true)),
6108            ]
6109        );
6110    }
6111
6112    #[test]
6113    fn lowers_array_and_tuple_state_annotations_into_canonical_types() {
6114        let parsed = presolve_parser::parse_file(
6115            "src/CollectionTypes.tsx",
6116            r#"
6117@component("x-collection-types")
6118class CollectionTypes extends Component {
6119  names: string[] = state([]);
6120  todos: Todo[] = state([]);
6121  pair: [string, number] = state(["Presolve", 1]);
6122}
6123"#,
6124        );
6125
6126        let asm = build_application_semantic_model(&parsed);
6127        let types = asm.components[0]
6128            .state_fields
6129            .iter()
6130            .map(|field| {
6131                (
6132                    field.name.as_str(),
6133                    &asm.semantic_types.assignments[&field.id].semantic_type,
6134                )
6135            })
6136            .collect::<Vec<_>>();
6137
6138        assert_eq!(
6139            types,
6140            vec![
6141                (
6142                    "names",
6143                    &crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
6144                ),
6145                (
6146                    "todos",
6147                    &crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
6148                ),
6149                (
6150                    "pair",
6151                    &crate::SemanticType::Tuple(vec![
6152                        crate::SemanticType::String,
6153                        crate::SemanticType::Number,
6154                    ]),
6155                ),
6156            ]
6157        );
6158    }
6159
6160    #[test]
6161    fn lowers_structural_object_state_annotations_into_canonical_types() {
6162        let parsed = presolve_parser::parse_file(
6163            "src/ObjectTypes.tsx",
6164            r#"
6165@component("x-object-types")
6166class ObjectTypes extends Component {
6167  todo: { id: string; title: string; completed: boolean } = state({ id: "1", title: "Presolve", completed: false });
6168}
6169"#,
6170        );
6171
6172        let asm = build_application_semantic_model(&parsed);
6173        let field = &asm.components[0].state_fields[0];
6174        let assignment = &asm.semantic_types.assignments[&field.id];
6175        let crate::SemanticType::Object(object) = &assignment.semantic_type else {
6176            panic!("expected structural object type");
6177        };
6178
6179        assert_eq!(
6180            object.properties,
6181            std::collections::BTreeMap::from([
6182                ("completed".to_string(), crate::SemanticType::Boolean),
6183                ("id".to_string(), crate::SemanticType::String),
6184                ("title".to_string(), crate::SemanticType::String),
6185            ])
6186        );
6187    }
6188
6189    #[test]
6190    fn lowers_union_and_nullable_state_annotations_into_canonical_types() {
6191        let parsed = presolve_parser::parse_file(
6192            "src/UnionTypes.tsx",
6193            r#"
6194@component("x-union-types")
6195class UnionTypes extends Component {
6196  filter: "all" | "active" | "completed" = state("all");
6197  user: { id: string } | null = state(null);
6198}
6199"#,
6200        );
6201
6202        let asm = build_application_semantic_model(&parsed);
6203        let fields = &asm.components[0].state_fields;
6204        assert_eq!(
6205            asm.semantic_types.assignments[&fields[0].id].semantic_type,
6206            crate::SemanticType::Union(vec![
6207                crate::SemanticType::StringLiteral("active".to_string()),
6208                crate::SemanticType::StringLiteral("all".to_string()),
6209                crate::SemanticType::StringLiteral("completed".to_string()),
6210            ])
6211        );
6212        assert_eq!(
6213            asm.semantic_types.assignments[&fields[1].id].semantic_type,
6214            crate::SemanticType::Union(vec![
6215                crate::SemanticType::Null,
6216                crate::SemanticType::Object(crate::ObjectType {
6217                    properties: std::collections::BTreeMap::from([(
6218                        "id".to_string(),
6219                        crate::SemanticType::String,
6220                    )]),
6221                }),
6222            ])
6223        );
6224    }
6225
6226    #[test]
6227    fn resolves_local_type_aliases_with_canonical_alias_identity() {
6228        let parsed = presolve_parser::parse_file(
6229            "src/Aliases.tsx",
6230            r#"
6231type TodoId = string;
6232type Filter = "all" | "active" | "completed";
6233
6234@component("x-aliases")
6235class Aliases extends Component {
6236  id: TodoId = state("todo-1");
6237  filter: Filter = state("all");
6238}
6239"#,
6240        );
6241
6242        let asm = build_application_semantic_model(&parsed);
6243        let fields = &asm.components[0].state_fields;
6244        let todo_id = asm
6245            .semantic_types
6246            .aliases
6247            .values()
6248            .find(|alias| alias.name == "TodoId")
6249            .expect("TodoId alias");
6250        let filter = asm
6251            .semantic_types
6252            .aliases
6253            .values()
6254            .find(|alias| alias.name == "Filter")
6255            .expect("Filter alias");
6256
6257        assert_eq!(todo_id.semantic_type, crate::SemanticType::String);
6258        assert_eq!(
6259            filter.semantic_type,
6260            crate::SemanticType::Union(vec![
6261                crate::SemanticType::StringLiteral("active".to_string()),
6262                crate::SemanticType::StringLiteral("all".to_string()),
6263                crate::SemanticType::StringLiteral("completed".to_string()),
6264            ])
6265        );
6266        assert_eq!(
6267            asm.semantic_types.assignments[&fields[0].id].origin,
6268            todo_id.id
6269        );
6270        assert_eq!(
6271            asm.semantic_types.assignments[&fields[1].id].origin,
6272            filter.id
6273        );
6274    }
6275
6276    #[test]
6277    fn resolves_imported_type_aliases_through_named_reexports() {
6278        let unit = CompilationUnit::parse_sources([
6279            (
6280                "src/types.ts",
6281                r#"export type Filter = "all" | "active" | "completed";"#,
6282            ),
6283            ("src/index.ts", r#"export { Filter } from "./types";"#),
6284            (
6285                "src/App.tsx",
6286                r#"
6287import { Filter } from "./index";
6288
6289@component("x-app")
6290class App extends Component {
6291  filter: Filter = state("all");
6292}
6293"#,
6294            ),
6295        ]);
6296
6297        let asm = build_application_semantic_model_for_unit(&unit);
6298        let field = &asm
6299            .components
6300            .iter()
6301            .find(|component| component.class_name == "App")
6302            .expect("App component")
6303            .state_fields[0];
6304        let assignment = &asm.semantic_types.assignments[&field.id];
6305
6306        assert_eq!(
6307            assignment.semantic_type,
6308            crate::SemanticType::Union(vec![
6309                crate::SemanticType::StringLiteral("active".to_string()),
6310                crate::SemanticType::StringLiteral("all".to_string()),
6311                crate::SemanticType::StringLiteral("completed".to_string()),
6312            ])
6313        );
6314        assert_eq!(
6315            assignment.origin,
6316            crate::SemanticId::type_alias_in_module("src/types.ts", "Filter")
6317        );
6318    }
6319
6320    #[test]
6321    fn infers_state_types_from_direct_serializable_initializers() {
6322        let parsed = presolve_parser::parse_file(
6323            "src/InferredState.tsx",
6324            r#"
6325@component("x-inferred-state")
6326class InferredState extends Component {
6327  count = state(0);
6328  todos = state([]);
6329  tags = state(["Presolve"]);
6330  todo = state({ id: "1", completed: false });
6331}
6332"#,
6333        );
6334
6335        let asm = build_application_semantic_model(&parsed);
6336        let fields = &asm.components[0].state_fields;
6337        let types = fields
6338            .iter()
6339            .map(|field| {
6340                let assignment = &asm.semantic_types.assignments[&field.id];
6341                assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6342                (field.name.as_str(), assignment.semantic_type.clone())
6343            })
6344            .collect::<Vec<_>>();
6345
6346        assert_eq!(
6347            types,
6348            vec![
6349                ("count", crate::SemanticType::Number),
6350                (
6351                    "todos",
6352                    crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
6353                ),
6354                (
6355                    "tags",
6356                    crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
6357                ),
6358                (
6359                    "todo",
6360                    crate::SemanticType::Object(crate::ObjectType {
6361                        properties: std::collections::BTreeMap::from([
6362                            ("completed".to_string(), crate::SemanticType::Boolean),
6363                            ("id".to_string(), crate::SemanticType::String),
6364                        ]),
6365                    }),
6366                ),
6367            ]
6368        );
6369    }
6370
6371    #[test]
6372    fn propagates_canonical_types_to_expression_graph_nodes() {
6373        let parsed = presolve_parser::parse_file(
6374            "src/ExpressionTypes.tsx",
6375            r#"
6376@component("x-expression-types")
6377class ExpressionTypes extends Component {
6378  total = state((1 + 2) * 3);
6379  ready = state(1 < 2);
6380}
6381"#,
6382        );
6383
6384        let asm = build_application_semantic_model(&parsed);
6385        let total = &asm.components[0].state_fields[0];
6386        let ready = &asm.components[0].state_fields[1];
6387        let total_root = asm
6388            .expression_root(&total.id)
6389            .expect("total expression root");
6390        let ready_root = asm
6391            .expression_root(&ready.id)
6392            .expect("ready expression root");
6393
6394        assert_eq!(
6395            asm.semantic_types.assignments[total_root].semantic_type,
6396            crate::SemanticType::Number
6397        );
6398        assert_eq!(
6399            asm.semantic_types.assignments[ready_root].semantic_type,
6400            crate::SemanticType::Boolean
6401        );
6402        assert!(asm
6403            .expression_dependencies(total_root)
6404            .iter()
6405            .all(|id| asm.semantic_types.assignments.contains_key(*id)));
6406    }
6407
6408    #[test]
6409    fn derives_component_ownership_without_legacy_owner_fields() {
6410        let parsed = presolve_parser::parse_file(
6411            "src/Counter.tsx",
6412            r#"
6413@component("x-counter")
6414class Counter extends Component {
6415  count = state(0);
6416
6417  increment() {
6418    this.count++;
6419  }
6420
6421  render() {
6422    return <button onClick={() => this.increment()}>{this.count}</button>;
6423  }
6424}
6425"#,
6426        );
6427        let mut component_graph = build_component_graph_for_module(&parsed);
6428        let mut templates = build_template_graph(&component_graph).templates;
6429        let template_entities = build_template_semantic_entities(&templates);
6430        let component = component_graph
6431            .components
6432            .first_mut()
6433            .expect("component graph should contain Counter");
6434        let component_id = component.id.clone();
6435        let method_id = component.methods[0].id.clone();
6436        let action_id = component.actions[0].id.clone();
6437        let state_id = component.state_fields[0].id.clone();
6438        let event_id = component
6439            .render
6440            .as_ref()
6441            .expect("Counter should render")
6442            .event_handlers[0]
6443            .id
6444            .clone();
6445
6446        component.owner = SemanticOwner::entity(component_id.clone());
6447        component.state_fields[0].owner = SemanticOwner::Application;
6448        component.methods[0].owner = SemanticOwner::Application;
6449        component.actions[0].owner = SemanticOwner::Application;
6450        component
6451            .render
6452            .as_mut()
6453            .expect("Counter should render")
6454            .event_handlers[0]
6455            .owner = SemanticOwner::Application;
6456        templates[0].owner = SemanticOwner::Application;
6457
6458        let ownership = collect_ownership(
6459            &component_graph.components,
6460            &std::collections::BTreeMap::new(),
6461            &std::collections::BTreeMap::new(),
6462            &std::collections::BTreeMap::new(),
6463            &std::collections::BTreeMap::new(),
6464            &std::collections::BTreeMap::new(),
6465            &std::collections::BTreeMap::new(),
6466            &std::collections::BTreeMap::new(),
6467            &std::collections::BTreeMap::new(),
6468            &std::collections::BTreeMap::new(),
6469            &std::collections::BTreeMap::new(),
6470            &ComponentInstancePlan::default(),
6471            &std::collections::BTreeMap::new(),
6472            &std::collections::BTreeMap::new(),
6473            &templates,
6474            &template_entities,
6475        );
6476        assert_eq!(ownership[&component_id], SemanticOwner::Application);
6477        assert_eq!(
6478            ownership[&state_id],
6479            SemanticOwner::entity(component_id.clone())
6480        );
6481        assert_eq!(
6482            ownership[&method_id],
6483            SemanticOwner::entity(component_id.clone())
6484        );
6485        assert_eq!(ownership[&action_id], SemanticOwner::entity(method_id));
6486        assert_eq!(
6487            ownership[&event_id],
6488            SemanticOwner::entity(component_id.clone().template())
6489        );
6490        assert_eq!(
6491            ownership[&templates[0].id],
6492            SemanticOwner::entity(component_id)
6493        );
6494    }
6495
6496    #[test]
6497    #[allow(clippy::too_many_lines)]
6498    fn traverses_application_ownership_in_semantic_id_order() {
6499        let parsed = presolve_parser::parse_file(
6500            "src/Counter.tsx",
6501            r#"
6502@component("x-counter")
6503class Counter extends Component {
6504  count = state(0);
6505
6506  increment() {
6507    this.count++;
6508  }
6509
6510  render() {
6511    return <button onClick={this.increment}>{this.count}</button>;
6512  }
6513}
6514"#,
6515        );
6516
6517        let asm = build_application_semantic_model(&parsed);
6518        let component = &asm.components[0];
6519        let roots = asm.application_roots();
6520
6521        assert_eq!(
6522            roots,
6523            vec![
6524                &component.id,
6525                asm.component_instance_plan
6526                    .instances
6527                    .values()
6528                    .find(|instance| instance.depth == 0)
6529                    .expect("build root instance")
6530                    .id
6531                    .as_semantic_id(),
6532            ]
6533        );
6534        assert_eq!(
6535            asm.children_of(&component.id),
6536            vec![
6537                &component.methods[0].id,
6538                &component.methods[1].id,
6539                &component.state_fields[0].id,
6540                &asm.templates[0].id,
6541            ]
6542        );
6543        assert_eq!(
6544            asm.children_of(&component.methods[0].id),
6545            vec![&component.actions[0].id]
6546        );
6547        assert_eq!(asm.parent_of(&component.id), None);
6548        assert_eq!(
6549            asm.parent_of(&component.actions[0].id),
6550            Some(&component.methods[0].id)
6551        );
6552        assert_eq!(
6553            asm.ancestors_of(&component.actions[0].id),
6554            vec![&component.methods[0].id, &component.id]
6555        );
6556        let descendants = asm.descendants_of(&component.id);
6557        assert_eq!(descendants[0], &component.methods[0].id);
6558        assert_eq!(descendants[1], &component.actions[0].id);
6559        assert_eq!(descendants[2], &component.methods[1].id);
6560        assert_eq!(
6561            descendants.len(),
6562            asm.ownership.len() - asm.application_roots().len()
6563        );
6564        assert_eq!(
6565            asm.entities_of_kind(SemanticEntityKind::Method),
6566            vec![&component.methods[0].id, &component.methods[1].id]
6567        );
6568        assert_eq!(
6569            asm.entities_of_kind(SemanticEntityKind::Action),
6570            vec![&component.actions[0].id]
6571        );
6572        assert_eq!(
6573            asm.entities_of_kind(SemanticEntityKind::StateField),
6574            vec![&component.state_fields[0].id]
6575        );
6576        let state_id = &component.state_fields[0].id;
6577        let state_provenance = asm.provenance(state_id).expect("state provenance");
6578        assert_eq!(
6579            asm.entities_in_file(state_provenance.path.as_path()).len(),
6580            asm.ownership.len()
6581        );
6582        let at_state =
6583            asm.entities_at(state_provenance.path.as_path(), state_provenance.span.start);
6584        assert!(at_state.contains(&state_id));
6585        assert!(at_state.iter().all(|id| {
6586            let provenance = asm.provenance(id).expect("entity provenance");
6587            provenance.span.start <= state_provenance.span.start
6588                && state_provenance.span.start < provenance.span.end
6589        }));
6590        let action_references = asm.references_of_kind(SemanticReferenceKind::ActionState);
6591        assert_eq!(action_references.len(), 1);
6592        assert_eq!(action_references[0].source, component.actions[0].id);
6593        assert_eq!(action_references[0].target, component.state_fields[0].id);
6594        let action_provenance = &action_references[0].provenance;
6595        assert_eq!(
6596            asm.references_in_file(action_provenance.path.as_path())
6597                .len(),
6598            asm.references.len()
6599        );
6600        let at_action = asm.references_at(
6601            action_provenance.path.as_path(),
6602            action_provenance.span.start,
6603        );
6604        assert!(at_action.iter().any(|reference| {
6605            reference.source == component.actions[0].id
6606                && reference.target == component.state_fields[0].id
6607        }));
6608    }
6609
6610    #[test]
6611    fn resolves_template_state_dependencies() {
6612        let parsed = presolve_parser::parse_file(
6613            "src/Panel.tsx",
6614            r#"
6615@component("x-panel")
6616class Panel extends Component {
6617  enabled = state(true);
6618
6619  render() {
6620    return <section hidden={this.enabled}>{this.enabled}{this.enabled ? <span>On</span> : <span>Off</span>}</section>;
6621  }
6622}
6623"#,
6624        );
6625
6626        let asm = build_application_semantic_model(&parsed);
6627        let component = &asm.components[0];
6628        let state = &component.state_fields[0];
6629        let state_references = asm.references_to(&state.id);
6630
6631        assert_eq!(state_references.len(), 3);
6632        assert!(state_references.iter().all(|reference| {
6633            reference.kind == SemanticReferenceKind::TemplateState
6634                && reference.target == state.id
6635                && reference.provenance.path.as_path() == std::path::Path::new("src/Panel.tsx")
6636        }));
6637        assert!(state_references.iter().any(|reference| {
6638            asm.template_entity(&reference.source)
6639                .is_some_and(|entity| entity.kind == TemplateSemanticKind::Binding)
6640        }));
6641        assert!(state_references.iter().any(|reference| {
6642            asm.template_entity(&reference.source)
6643                .is_some_and(|entity| entity.kind == TemplateSemanticKind::AttributeBinding)
6644        }));
6645        assert!(state_references.iter().any(|reference| {
6646            asm.template_entity(&reference.source)
6647                .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6648        }));
6649    }
6650
6651    #[test]
6652    fn resolves_template_bindings_to_canonical_computed_entities() {
6653        let parsed = presolve_parser::parse_file(
6654            "src/ComputedTemplate.tsx",
6655            r#"
6656@component("x-computed-template")
6657class ComputedTemplate extends Component {
6658  @computed()
6659  get label(): string { return "Ready"; }
6660
6661  @computed()
6662  get visible(): boolean { return true; }
6663
6664  render() {
6665    return <section title={this.label}>{this.label}{this.visible ? <span>Visible</span> : <span>Hidden</span>}</section>;
6666  }
6667}
6668"#,
6669        );
6670
6671        let asm = build_application_semantic_model(&parsed);
6672        let component = &asm.components[0];
6673        let label = component.id.computed("label");
6674        let visible = component.id.computed("visible");
6675        let references = asm.references_of_kind(SemanticReferenceKind::TemplateComputed);
6676
6677        assert_eq!(references.len(), 3);
6678        assert_eq!(
6679            references
6680                .iter()
6681                .filter(|reference| reference.target == label)
6682                .count(),
6683            2
6684        );
6685        assert!(references.iter().any(|reference| {
6686            reference.target == visible
6687                && asm
6688                    .template_entity(&reference.source)
6689                    .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6690        }));
6691        assert!(references.iter().all(|reference| {
6692            asm.provenance(&reference.source) == Some(&reference.provenance)
6693                && asm.semantic_type_of(&reference.source)
6694                    == asm.semantic_type_of(&reference.target)
6695        }));
6696        assert_eq!(
6697            asm.references_of_kind(SemanticReferenceKind::TemplateState)
6698                .into_iter()
6699                .filter(|reference| reference.target == label || reference.target == visible)
6700                .count(),
6701            0
6702        );
6703
6704        let graph = crate::build_semantic_graph(&asm);
6705        assert_eq!(
6706            graph
6707                .edges
6708                .iter()
6709                .filter(|edge| edge.kind == crate::SemanticGraphEdgeKind::TemplateComputed)
6710                .count(),
6711            3
6712        );
6713    }
6714
6715    #[test]
6716    fn navigates_template_entities_from_canonical_ownership() {
6717        let parsed = presolve_parser::parse_file(
6718            "src/Panel.tsx",
6719            r#"
6720@component("x-panel")
6721class Panel extends Component {
6722  count = state(0);
6723
6724  render() {
6725    return <section>{this.count}</section>;
6726  }
6727}
6728"#,
6729        );
6730        let mut asm = build_application_semantic_model(&parsed);
6731        let template_id = asm.templates[0].id.clone();
6732        let expected = asm.template_entities_for(&template_id).len();
6733
6734        for entity in &mut asm.template_entities {
6735            entity.owner = SemanticOwner::Application;
6736        }
6737
6738        assert_eq!(asm.template_entities_for(&template_id).len(), expected);
6739    }
6740
6741    #[test]
6742    fn leaves_member_expressions_unresolved_without_expression_evaluation() {
6743        let parsed = presolve_parser::parse_file(
6744            "src/Panel.tsx",
6745            r#"
6746@component("x-panel")
6747class Panel extends Component {
6748  enabled = state(true);
6749
6750  render() {
6751    return <section data-value={this.enabled.value}>Panel</section>;
6752  }
6753}
6754"#,
6755        );
6756
6757        let asm = build_application_semantic_model(&parsed);
6758        assert_eq!(
6759            asm.references
6760                .iter()
6761                .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6762                .count(),
6763            0
6764        );
6765    }
6766
6767    #[test]
6768    fn resolves_keyed_list_iterable_to_component_state() {
6769        let parsed = presolve_parser::parse_file(
6770            "src/KeyedList.tsx",
6771            r#"
6772@component("x-keyed-list")
6773class KeyedList extends Component {
6774  items = state([]);
6775
6776  render() {
6777    return <ul>{this.items.map((item) => <li key={item}>{item}</li>)}</ul>;
6778  }
6779}
6780"#,
6781        );
6782
6783        let asm = build_application_semantic_model(&parsed);
6784        let component = &asm.components[0];
6785        let state = &component.state_fields[0];
6786        let reference = asm
6787            .references_to(&state.id)
6788            .into_iter()
6789            .find(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6790            .expect("list iterable state reference");
6791        let list = asm
6792            .template_entity(&reference.source)
6793            .expect("list semantic entity");
6794
6795        assert_eq!(list.kind, TemplateSemanticKind::List);
6796        assert_eq!(list.expression.as_deref(), Some("this.items"));
6797        assert_eq!(reference.provenance, list.provenance);
6798    }
6799
6800    #[test]
6801    fn resolves_template_event_attribute_to_component_method() {
6802        let parsed = presolve_parser::parse_file(
6803            "src/Counter.tsx",
6804            r#"
6805@component("x-counter")
6806class Counter extends Component {
6807  count = state(0);
6808
6809  increment() {
6810    this.count += 1;
6811  }
6812
6813  render() {
6814    return <button onClick={() => this.increment()}>Count</button>;
6815  }
6816}
6817"#,
6818        );
6819
6820        let asm = build_application_semantic_model(&parsed);
6821        let component = &asm.components[0];
6822        let method = component
6823            .methods
6824            .iter()
6825            .find(|method| method.name == "increment")
6826            .expect("increment method");
6827        let reference = asm
6828            .references_to(&method.id)
6829            .into_iter()
6830            .find(|reference| {
6831                reference.kind == SemanticReferenceKind::EventMethod
6832                    && asm
6833                        .template_entity(&reference.source)
6834                        .is_some_and(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
6835            })
6836            .expect("template event method reference");
6837
6838        assert_eq!(
6839            reference.provenance.path,
6840            std::path::Path::new("src/Counter.tsx")
6841        );
6842        assert_eq!(reference.provenance.span.line, 11);
6843    }
6844
6845    #[test]
6846    fn resolves_render_template_bindings_to_unique_method_locals() {
6847        let parsed = presolve_parser::parse_file(
6848            "src/LocalResolution.tsx",
6849            r#"
6850@component("x-local-resolution")
6851class LocalResolution extends Component {
6852  render() {
6853    const title = "Presolve";
6854    return <output title={title}>{title}</output>;
6855  }
6856}
6857"#,
6858        );
6859
6860        let asm = build_application_semantic_model(&parsed);
6861        let component = &asm.components[0];
6862        let render = component
6863            .methods
6864            .iter()
6865            .find(|method| method.name == "render")
6866            .expect("render method");
6867        let local = &render.local_variables[0];
6868        let references = asm.references_of_kind(SemanticReferenceKind::TemplateLocal);
6869        let assignment = asm
6870            .semantic_types
6871            .assignments
6872            .get(&local.id)
6873            .expect("canonical inferred local type");
6874
6875        assert_eq!(
6876            asm.owner(&local.id),
6877            Some(&SemanticOwner::entity(render.id.clone()))
6878        );
6879        assert_eq!(
6880            asm.entities_of_kind(SemanticEntityKind::LocalVariable),
6881            vec![&local.id]
6882        );
6883        assert_eq!(references.len(), 2);
6884        assert!(references.iter().all(|reference| {
6885            reference.target == local.id
6886                && reference.provenance.path == std::path::Path::new("src/LocalResolution.tsx")
6887        }));
6888        assert_eq!(assignment.semantic_type, crate::SemanticType::String);
6889        assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6890        assert_eq!(assignment.provenance.span, local.span);
6891
6892        let semantic_graph = crate::build_semantic_graph(&asm);
6893        assert!(semantic_graph.nodes.iter().any(|node| {
6894            node.kind == crate::SemanticGraphNodeKind::LocalVariable && node.id == local.id
6895        }));
6896        assert_eq!(
6897            semantic_graph
6898                .edges
6899                .iter()
6900                .filter(|edge| {
6901                    edge.kind == crate::SemanticGraphEdgeKind::TemplateLocal
6902                        && edge.target == local.id
6903                })
6904                .count(),
6905            2
6906        );
6907
6908        let template_graph = build_template_graph(&build_component_graph_for_module(&parsed));
6909        assert_eq!(
6910            crate::generate_static_html(&template_graph),
6911            "<output data-presolve-node=\"n0\" title=\"Presolve\" data-presolve-bindings=\"title\"><!-- presolve-binding:n2:title -->Presolve</output>\n"
6912        );
6913    }
6914
6915    #[test]
6916    fn queries_canonical_expression_graphs_by_dependency_provenance_and_owner() {
6917        let parsed = presolve_parser::parse_file(
6918            "src/ExpressionQueries.tsx",
6919            r#"
6920@component("x-expression-queries")
6921class ExpressionQueries extends Component {
6922  total = state((1 + 2) * 3);
6923}
6924"#,
6925        );
6926
6927        let asm = build_application_semantic_model(&parsed);
6928        let field = &asm.components[0].state_fields[0];
6929        let root = asm.expression_root(&field.id).expect("expression root");
6930        let left = field.id.expression("root.0");
6931        let right = field.id.expression("root.1");
6932
6933        assert_eq!(asm.expression(root).map(|node| &node.id), Some(root));
6934        assert_eq!(asm.expression_owner(root), Some(&field.id));
6935        assert_eq!(asm.expression_dependencies(root), vec![&left, &right]);
6936        assert_eq!(
6937            asm.expression_dependents(&left)
6938                .into_iter()
6939                .map(|node| &node.id)
6940                .collect::<Vec<_>>(),
6941            vec![root]
6942        );
6943        assert_eq!(asm.expressions_for(&field.id).len(), 5);
6944
6945        let provenance = asm
6946            .expression_provenance(root)
6947            .expect("expression provenance");
6948        assert_eq!(
6949            provenance.path,
6950            std::path::Path::new("src/ExpressionQueries.tsx")
6951        );
6952        assert_eq!(asm.expressions_in_file(&provenance.path).len(), 5);
6953        assert_eq!(
6954            asm.expressions_at(&provenance.path, provenance.span.start)
6955                .into_iter()
6956                .map(|node| &node.id)
6957                .collect::<Vec<_>>(),
6958            vec![root]
6959        );
6960    }
6961
6962    #[test]
6963    fn file_route_model_composes_layout_default_slots_into_canonical_instances() {
6964        let unit = CompilationUnit::parse_sources([
6965            (
6966                "app/layout.tsx",
6967                r#"
6968@component()
6969class AppLayout extends Component {
6970  @slot() children!: SlotContent;
6971  render() { return <main><slot /></main>; }
6972}
6973"#,
6974            ),
6975            (
6976                "app/routes/index.tsx",
6977                r#"
6978@component()
6979class Home extends Component {
6980  render() { return <article>Home</article>; }
6981}
6982"#,
6983            ),
6984        ]);
6985        let asm = build_file_route_application_semantic_model_for_unit_with_packages(
6986            &unit,
6987            &crate::SemanticPackageResolutionTable::default(),
6988        )
6989        .expect("valid conventional layout");
6990
6991        assert_eq!(asm.component_instance_plan.roots.len(), 1);
6992        let binding = asm
6993            .slot_bindings
6994            .bindings
6995            .values()
6996            .find(|binding| binding.direct_child_instance.is_some())
6997            .expect("compiler-issued layout Slot binding");
6998        assert_eq!(binding.status, crate::SlotBindingStatus::Bound);
6999        assert!(binding.content_fragment.is_none());
7000        assert!(crate::validate_application_semantic_model(&asm).is_empty());
7001
7002        let html = crate::generate_ordinary_instance_html(&asm);
7003        assert!(html.contains("<main"));
7004        assert!(html.contains("<article"));
7005        assert!(html.contains("Home"));
7006    }
7007
7008    #[test]
7009    fn retains_route_loader_source_facts_before_server_capability_resolution() {
7010        let asm = build_application_semantic_model(&presolve_parser::parse_file(
7011            "app/routes/posts/[slug].tsx",
7012            r#"
7013@component()
7014class Post {
7015  @loader("loadPost") post!: Resource<Post, NotFound>;
7016  render() { return <article />; }
7017}
7018"#,
7019        ));
7020
7021        let loader = &asm.components[0].route_loader_declaration_candidates[0];
7022        assert_eq!(loader.field, "post");
7023        assert!(loader.decorator_invoked);
7024        assert_eq!(loader.decorator_argument_count, 1);
7025        assert_eq!(loader.endpoint_designator.as_deref(), Some("loadPost"));
7026        assert_eq!(
7027            loader
7028                .declared_type
7029                .as_ref()
7030                .map(|type_| type_.text.as_str()),
7031            Some("Resource<Post, NotFound>")
7032        );
7033        assert!(asm
7034            .diagnostics
7035            .iter()
7036            .any(|diagnostic| diagnostic.code == "PSC1132"));
7037    }
7038
7039    #[test]
7040    fn retains_server_action_source_facts_before_route_capability_resolution() {
7041        let asm = build_application_semantic_model(&presolve_parser::parse_file(
7042            "app/routes/posts/[slug].tsx",
7043            r#"
7044@component()
7045class Post {
7046  @serverAction("savePost") save(): void {}
7047  render() { return <form />; }
7048}
7049"#,
7050        ));
7051
7052        let action = &asm.components[0].server_action_facts[0];
7053        assert_eq!(action.method_name, "save");
7054        assert!(action.invoked);
7055        assert_eq!(action.argument_count, 1);
7056        assert_eq!(action.endpoint_designator.as_deref(), Some("savePost"));
7057        assert!(!action.is_action);
7058        assert!(!action.action_invoked);
7059        assert!(!action.has_body_effects);
7060        assert!(asm
7061            .diagnostics
7062            .iter()
7063            .any(|diagnostic| diagnostic.code == "PSC1133"));
7064    }
7065}