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