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