Skip to main content

presolve_compiler/
asm_validation.rs

1use crate::application_semantic_model::ApplicationSemanticModel;
2use crate::semantic_id::SemanticOwner;
3use crate::{
4    build_ordinary_template_instance_registry, build_runtime_component_artifact,
5    build_template_manifest_from_asm, validate_ordinary_template_instance_registry,
6    validate_runtime_component_artifact, validate_template_manifest, EffectOperationClassification,
7    EffectRenderBoundary, EffectValidation, ManifestEventKind, OrdinaryTemplateIntegrityCode,
8    SemanticTypeId, EFFECT_CAPABILITY_REGISTRY, TEMPLATE_MANIFEST_SCHEMA_VERSION,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AsmValidationDiagnostic {
13    pub code: String,
14    pub message: String,
15}
16
17#[must_use]
18#[allow(clippy::too_many_lines)]
19pub fn validate_application_semantic_model(
20    model: &ApplicationSemanticModel,
21) -> Vec<AsmValidationDiagnostic> {
22    let mut diagnostics = Vec::new();
23
24    for (id, owner) in &model.ownership {
25        if model.entity(id).is_none() {
26            diagnostics.push(AsmValidationDiagnostic {
27                code: "PSASM1001".to_string(),
28                message: format!("ownership references missing semantic entity `{id}`"),
29            });
30        }
31        if !model.provenance.contains_key(id) {
32            diagnostics.push(AsmValidationDiagnostic {
33                code: "PSASM1002".to_string(),
34                message: format!("semantic entity `{id}` is missing source provenance"),
35            });
36        }
37        if let SemanticOwner::Entity(owner_id) = owner {
38            if model.entity(owner_id).is_none() {
39                diagnostics.push(AsmValidationDiagnostic {
40                    code: "PSASM1003".to_string(),
41                    message: format!("semantic entity `{id}` has missing owner `{owner_id}`"),
42                });
43            }
44        }
45    }
46
47    for id in model.provenance.keys() {
48        if !model.ownership.contains_key(id) {
49            diagnostics.push(AsmValidationDiagnostic {
50                code: "PSASM1004".to_string(),
51                message: format!("provenance references unowned semantic entity `{id}`"),
52            });
53        }
54    }
55
56    for reference in &model.references {
57        if model.entity(&reference.source).is_none() || model.entity(&reference.target).is_none() {
58            diagnostics.push(AsmValidationDiagnostic {
59                code: "PSASM1005".to_string(),
60                message: format!(
61                    "reference from `{}` to `{}` has a missing endpoint",
62                    reference.source, reference.target
63                ),
64            });
65        }
66        let source_provenance_matches = model.provenance(&reference.source)
67            == Some(&reference.provenance)
68            || model.form_field_bindings.values().any(|binding| {
69                binding.id.as_semantic_id() == &reference.source
70                    && binding.expression_provenance == reference.provenance
71            })
72            || model.validation_rules.values().any(|rule| {
73                rule.id.as_semantic_id() == &reference.source
74                    && rule.argument_provenance.as_ref() == Some(&reference.provenance)
75            })
76            || model
77                .consumer_for_semantic_id(&reference.source)
78                .is_some_and(|consumer| {
79                    consumer.context_designator.provenance == reference.provenance
80                })
81            || model
82                .expression_graph
83                .nodes_for(&reference.source)
84                .iter()
85                .any(|node| node.provenance == reference.provenance);
86        if !source_provenance_matches {
87            diagnostics.push(AsmValidationDiagnostic {
88                code: "PSASM1006".to_string(),
89                message: format!(
90                    "reference source `{}` has mismatched provenance",
91                    reference.source
92                ),
93            });
94        }
95    }
96
97    validate_semantic_types(model, &mut diagnostics);
98    validate_form_field_bindings(model, &mut diagnostics);
99    validate_form_ownership(model, &mut diagnostics);
100    validate_form_validation(model, &mut diagnostics);
101    validate_form_tracking(model, &mut diagnostics);
102    validate_form_submissions(model, &mut diagnostics);
103    validate_form_serialization(model, &mut diagnostics);
104    validate_form_reset(model, &mut diagnostics);
105    validate_contexts(model, &mut diagnostics);
106    validate_providers(model, &mut diagnostics);
107    validate_consumers(model, &mut diagnostics);
108    validate_context_resolution(model, &mut diagnostics);
109    validate_context_typing(model, &mut diagnostics);
110    validate_context_ownership(model, &mut diagnostics);
111    validate_context_dependency(model, &mut diagnostics);
112    validate_context_lifetime(model, &mut diagnostics);
113    validate_context_evaluation(model, &mut diagnostics);
114    validate_component_instance_scope(model, &mut diagnostics);
115    validate_component_composition(model, &mut diagnostics);
116    validate_component_initialization(model, &mut diagnostics);
117    validate_component_ir(model, &mut diagnostics);
118    validate_optimized_component_ir(model, &mut diagnostics);
119    validate_instance_context(model, &mut diagnostics);
120    validate_slot_bindings(model, &mut diagnostics);
121    validate_composition_types(model, &mut diagnostics);
122    validate_effect_statement_types(model, &mut diagnostics);
123    validate_effect_execution_plan(model, &mut diagnostics);
124    validate_component_diagnostic_metadata(model, &mut diagnostics);
125    validate_template_action_bindings(model, &mut diagnostics);
126    validate_ordinary_template_instance_projection(model, &mut diagnostics);
127
128    diagnostics
129}
130
131fn validate_ordinary_template_instance_projection(
132    model: &ApplicationSemanticModel,
133    diagnostics: &mut Vec<AsmValidationDiagnostic>,
134) {
135    let registry = build_ordinary_template_instance_registry(model);
136    let artifact = build_runtime_component_artifact(model, &model.component_ir_optimization);
137    let manifest = build_template_manifest_from_asm(model);
138    if validate_ordinary_template_instance_registry(model, &registry).is_err() {
139        diagnostics.push(AsmValidationDiagnostic {
140            code: OrdinaryTemplateIntegrityCode::StaleRegistry
141                .as_str()
142                .to_string(),
143            message: "ordinary template registry drifted from canonical products".to_string(),
144        });
145    }
146    if validate_runtime_component_artifact(&artifact).is_err()
147        || validate_template_manifest(&manifest).is_err()
148    {
149        diagnostics.push(AsmValidationDiagnostic {
150            code: OrdinaryTemplateIntegrityCode::ArtifactManifestDrift
151                .as_str()
152                .to_string(),
153            message: "ordinary template artifact or manifest projection is invalid".to_string(),
154        });
155    }
156}
157
158fn validate_form_validation(
159    model: &ApplicationSemanticModel,
160    diagnostics: &mut Vec<AsmValidationDiagnostic>,
161) {
162    let expected_products =
163        crate::collect_validation_products(&model.components, &model.forms, &model.form_fields);
164    if model.validation_rule_candidates != expected_products.candidates
165        || model.validation_rules != expected_products.rules
166    {
167        diagnostics.push(AsmValidationDiagnostic {
168            code: "PSASM1241".to_string(),
169            message: "validation products do not match canonical I6 lowering".to_string(),
170        });
171    }
172    let validation = crate::validate_validation_graph(
173        &model.validation_graph,
174        &model.component_instance_plan.roots,
175        &model.form_ownership,
176        &model.forms,
177        &model.form_fields,
178        &model.validation_rules,
179        &model.validation_rule_candidates,
180    );
181    diagnostics.extend(
182        validation
183            .diagnostics
184            .iter()
185            .map(|diagnostic| AsmValidationDiagnostic {
186                code: diagnostic.code.clone(),
187                message: diagnostic.message.clone(),
188            }),
189    );
190    if model.validation_graph.validation != validation {
191        diagnostics.push(AsmValidationDiagnostic {
192            code: "PSASM1240".to_string(),
193            message: "validation graph retained stale validation facts".to_string(),
194        });
195    }
196    let expected_plans = crate::collect_validation_dependency_plans(
197        &model.forms,
198        &model.form_fields,
199        &model.validation_rules,
200        &model.form_ownership,
201        &model.validation_graph,
202    );
203    if model.validation_dependency_plans != expected_plans {
204        diagnostics.push(AsmValidationDiagnostic {
205            code: "PSASM1272".to_string(),
206            message: "validation dependency plans do not match canonical I7 planning".to_string(),
207        });
208    }
209    let planning = crate::validate_validation_dependency_plans(
210        &model.validation_dependency_plans,
211        &model.forms,
212        &model.form_fields,
213        &model.validation_rules,
214        &model.form_ownership,
215        &model.validation_graph,
216    );
217    diagnostics.extend(
218        planning
219            .diagnostics
220            .iter()
221            .map(|diagnostic| AsmValidationDiagnostic {
222                code: diagnostic.code.clone(),
223                message: diagnostic.message.clone(),
224            }),
225    );
226    if model.validation_dependency_plans.validation != planning {
227        diagnostics.push(AsmValidationDiagnostic {
228            code: "PSASM1271".to_string(),
229            message: "validation dependency plans retained stale validation facts".to_string(),
230        });
231    }
232}
233
234fn validate_form_tracking(
235    model: &ApplicationSemanticModel,
236    diagnostics: &mut Vec<AsmValidationDiagnostic>,
237) {
238    let expected = crate::collect_form_tracking_products(
239        &model.forms,
240        &model.form_fields,
241        &model.form_field_bindings,
242        &model.form_ownership,
243    );
244    if model.form_tracking != expected {
245        diagnostics.push(AsmValidationDiagnostic {
246            code: "PSASM1285".to_string(),
247            message: "I8 form tracking products do not match canonical declaration planning"
248                .to_string(),
249        });
250    }
251    let dirty = crate::validate_dirty_tracking_graph(
252        &model.form_tracking.dirty,
253        &model.forms,
254        &model.form_fields,
255        &model.form_field_bindings,
256        &model.form_ownership,
257    );
258    let touched = crate::validate_touched_tracking_graph(
259        &model.form_tracking.touched,
260        &model.forms,
261        &model.form_fields,
262        &model.form_field_bindings,
263        &model.form_ownership,
264    );
265    diagnostics.extend(
266        dirty
267            .diagnostics
268            .iter()
269            .chain(touched.diagnostics.iter())
270            .map(|diagnostic| AsmValidationDiagnostic {
271                code: diagnostic.code.clone(),
272                message: diagnostic.message.clone(),
273            }),
274    );
275}
276
277fn validate_form_submissions(
278    model: &ApplicationSemanticModel,
279    diagnostics: &mut Vec<AsmValidationDiagnostic>,
280) {
281    let expected = crate::collect_submission_products(
282        &model.components,
283        &model.forms,
284        &model.form_fields,
285        &model.validation_rules,
286        &model.effect_trigger_plan,
287    );
288    if model.submissions != expected {
289        diagnostics.push(AsmValidationDiagnostic {
290            code: "PSASM1286".to_string(),
291            message: "I9 submission products do not match canonical declaration planning"
292                .to_string(),
293        });
294    }
295}
296
297fn validate_form_serialization(
298    model: &ApplicationSemanticModel,
299    diagnostics: &mut Vec<AsmValidationDiagnostic>,
300) {
301    let expected = crate::collect_serialization_products(
302        &model.components,
303        &model.forms,
304        &model.form_fields,
305        &model.submissions.plans,
306    );
307    if model.serialization != expected {
308        diagnostics.push(AsmValidationDiagnostic {
309            code: "PSASM1287".to_string(),
310            message: "I10 serialization products do not match canonical declaration planning"
311                .to_string(),
312        });
313    }
314}
315
316fn validate_form_reset(
317    model: &ApplicationSemanticModel,
318    diagnostics: &mut Vec<AsmValidationDiagnostic>,
319) {
320    let expected = crate::collect_reset_products(
321        &model.forms,
322        &model.form_fields,
323        &model.form_field_bindings,
324        &model.form_tracking,
325    );
326    if model.reset != expected {
327        diagnostics.push(AsmValidationDiagnostic {
328            code: "PSASM1288".to_string(),
329            message: "I11 reset products do not match canonical declaration planning".to_string(),
330        });
331    }
332}
333
334fn validate_form_ownership(
335    model: &ApplicationSemanticModel,
336    diagnostics: &mut Vec<AsmValidationDiagnostic>,
337) {
338    let validation = crate::validate_form_ownership_graph(&model.form_ownership, model);
339    diagnostics.extend(
340        validation
341            .diagnostics
342            .iter()
343            .map(|diagnostic| AsmValidationDiagnostic {
344                code: diagnostic.code.clone(),
345                message: diagnostic.message.clone(),
346            }),
347    );
348    if model.form_ownership.validation != validation {
349        diagnostics.push(AsmValidationDiagnostic {
350            code: "PSASM1220".to_string(),
351            message: "Form ownership graph retained stale validation facts".to_string(),
352        });
353    }
354}
355
356fn validate_form_field_bindings(
357    model: &ApplicationSemanticModel,
358    diagnostics: &mut Vec<AsmValidationDiagnostic>,
359) {
360    let expected = crate::collect_form_field_binding_products(
361        &model.components,
362        &model.templates,
363        &model.forms,
364        &model.form_fields,
365        &model.form_field_declaration_candidates,
366    );
367    if model.form_field_binding_candidates != expected.candidates
368        || model.form_field_bindings != expected.bindings
369    {
370        diagnostics.push(AsmValidationDiagnostic {
371            code: "PSASM1202".to_string(),
372            message: "Form Field bindings do not match canonical I3/template lowering".to_string(),
373        });
374    }
375}
376
377fn validate_component_ir(
378    model: &ApplicationSemanticModel,
379    diagnostics: &mut Vec<AsmValidationDiagnostic>,
380) {
381    if !crate::validate_component_ir(model, &model.component_ir).is_empty() {
382        diagnostics.push(AsmValidationDiagnostic {
383            code: "PSASM1199".to_string(),
384            message: "component IR does not match canonical H10 operations".to_string(),
385        });
386    }
387}
388
389fn validate_optimized_component_ir(
390    model: &ApplicationSemanticModel,
391    diagnostics: &mut Vec<AsmValidationDiagnostic>,
392) {
393    if !crate::validate_optimized_component_ir(&model.component_ir_optimization).is_empty() {
394        diagnostics.push(AsmValidationDiagnostic {
395            code: "PSASM1200".to_string(),
396            message: "optimized component IR does not match the canonical H12 projection"
397                .to_string(),
398        });
399    }
400}
401
402fn validate_component_initialization(
403    model: &ApplicationSemanticModel,
404    diagnostics: &mut Vec<AsmValidationDiagnostic>,
405) {
406    let expected = crate::plan_component_initialization(
407        &model.component_instance_plan,
408        &model.slot_bindings,
409        &model.composition_types,
410        &model.instance_context,
411    );
412    if model.component_initialization != expected {
413        diagnostics.push(AsmValidationDiagnostic {
414            code: "PSASM1198".to_string(),
415            message: "component initialization plan does not match canonical H4/H6/H7/H8 products"
416                .to_string(),
417        });
418    }
419}
420
421fn validate_component_composition(
422    model: &ApplicationSemanticModel,
423    diagnostics: &mut Vec<AsmValidationDiagnostic>,
424) {
425    let components = model
426        .components
427        .iter()
428        .map(|component| component.id.clone())
429        .collect();
430    let expected = crate::analyze_component_composition(
431        &components,
432        &model.component_invocations,
433        &model.component_instance_plan,
434    );
435    if model.component_composition != expected {
436        diagnostics.push(AsmValidationDiagnostic {
437            code: "PSASM1197".to_string(),
438            message: "component composition cycles do not match canonical resolved invocations"
439                .to_string(),
440        });
441    }
442}
443
444fn validate_composition_types(
445    model: &ApplicationSemanticModel,
446    diagnostics: &mut Vec<AsmValidationDiagnostic>,
447) {
448    let component_ids = model
449        .components
450        .iter()
451        .map(|component| component.id.clone())
452        .collect();
453    let expected = crate::collect_composition_type_products(
454        &component_ids,
455        &model.component_invocations,
456        &model.slot_bindings,
457        &model.slots,
458        &model.slot_content_fragments,
459        &model.slot_outlets,
460        &model.instance_context,
461        &model.context_binding_types,
462        &model.context_types,
463        &model.provider_types,
464        &model.context_lifetime,
465        &model.ownership,
466        &model.references,
467    );
468    if model.composition_types != expected {
469        diagnostics.push(AsmValidationDiagnostic {
470            code: "PSASM1196".to_string(),
471            message: "composition typing does not match canonical H2/H6/H7 products".to_string(),
472        });
473    }
474}
475
476fn validate_slot_bindings(
477    model: &ApplicationSemanticModel,
478    diagnostics: &mut Vec<AsmValidationDiagnostic>,
479) {
480    let expected = crate::collect_slot_bindings(
481        &model.component_instance_plan,
482        &model.component_invocations,
483        &model.slots,
484        &model.slot_content_fragments,
485        &model.slot_outlets,
486    );
487    if model.slot_bindings != expected {
488        diagnostics.push(AsmValidationDiagnostic {
489            code: "PSASM1195".to_string(),
490            message: "Slot binding registry does not match canonical H3 facts and H4 instances"
491                .to_string(),
492        });
493    }
494}
495
496fn validate_instance_context(
497    model: &ApplicationSemanticModel,
498    diagnostics: &mut Vec<AsmValidationDiagnostic>,
499) {
500    let expected = crate::collect_instance_context_registry(
501        &model.component_instance_scope,
502        &model.contexts,
503        &model.providers,
504        &model.consumers,
505    );
506    if model.instance_context != expected {
507        diagnostics.push(AsmValidationDiagnostic {
508            code: "PSASM1194".to_string(),
509            message:
510                "instance Context registry does not match canonical declarations and H5 ancestry"
511                    .to_string(),
512        });
513    }
514}
515
516fn validate_component_instance_scope(
517    model: &ApplicationSemanticModel,
518    diagnostics: &mut Vec<AsmValidationDiagnostic>,
519) {
520    let expected = crate::build_component_instance_scope_graph(&model.component_instance_plan);
521    if model.component_instance_scope != expected {
522        diagnostics.push(AsmValidationDiagnostic {
523            code: "PSASM1192".to_string(),
524            message: "component instance scope graph does not match the canonical H4 plan"
525                .to_string(),
526        });
527    }
528    diagnostics.extend(
529        crate::validate_component_instance_scope_graph(&model.component_instance_scope)
530            .into_iter()
531            .map(|diagnostic| AsmValidationDiagnostic {
532                code: "PSASM1193".to_string(),
533                message: format!(
534                    "component instance scope {:?} at `{}`{}",
535                    diagnostic.violation,
536                    diagnostic.instance,
537                    diagnostic.related.map_or_else(String::new, |related| {
538                        format!(" related to `{related}`")
539                    })
540                ),
541            }),
542    );
543}
544
545fn validate_context_dependency(
546    model: &ApplicationSemanticModel,
547    diagnostics: &mut Vec<AsmValidationDiagnostic>,
548) {
549    let expected = crate::collect_context_dependency_graph(
550        &model.components,
551        &model.contexts,
552        &model.providers,
553        &model.consumers,
554        &model.context_resolutions,
555        &model.context_types,
556        &model.provider_types,
557        &model.context_binding_types,
558        &model.computed_values,
559        &model.expression_graph,
560    );
561    if model.context_dependency != expected {
562        diagnostics.push(AsmValidationDiagnostic {
563            code: "PSASM1189".to_string(),
564            message: "Context dependency graph does not match canonical ASM products".to_string(),
565        });
566    }
567}
568
569fn validate_context_lifetime(
570    model: &ApplicationSemanticModel,
571    diagnostics: &mut Vec<AsmValidationDiagnostic>,
572) {
573    let expected = crate::collect_context_lifetime_analysis(
574        &model.components,
575        &model.contexts,
576        &model.providers,
577        &model.consumers,
578        &model.computed_values,
579        &model.context_ownership,
580        &model.component_scope,
581        &model.context_resolutions,
582        &model.context_dependency,
583        &model.provenance,
584    );
585    if model.context_lifetime != expected {
586        diagnostics.push(AsmValidationDiagnostic {
587            code: "PSASM1190".to_string(),
588            message: "Context lifetime analysis does not match canonical ASM products".to_string(),
589        });
590    }
591}
592
593fn validate_context_evaluation(
594    model: &ApplicationSemanticModel,
595    diagnostics: &mut Vec<AsmValidationDiagnostic>,
596) {
597    let expected = crate::collect_context_evaluation_plan(
598        &model.contexts,
599        &model.providers,
600        &model.context_resolutions,
601        &model.context_types,
602        &model.provider_types,
603        &model.context_binding_types,
604        &model.context_lifetime,
605        &model.context_dependency,
606        &model.computed_evaluation_plan,
607        &model.component_scope,
608    );
609    if model.context_evaluation != expected {
610        diagnostics.push(AsmValidationDiagnostic {
611            code: "PSASM1191".to_string(),
612            message: "Context evaluation plan does not match canonical ASM products".to_string(),
613        });
614    }
615}
616
617#[allow(clippy::too_many_lines)]
618fn validate_context_ownership(
619    model: &ApplicationSemanticModel,
620    diagnostics: &mut Vec<AsmValidationDiagnostic>,
621) {
622    let graph = &model.context_ownership;
623    if graph
624        .nodes
625        .iter()
626        .any(|node| matches!(node.id, crate::ContextOwnershipNodeId::Component(ref id) if model.component(id).is_none()))
627    {
628        diagnostics.push(AsmValidationDiagnostic {
629            code: "PSASM1182".to_string(),
630            message: "Context ownership graph references a missing component".to_string(),
631        });
632    }
633    if graph.nodes.len()
634        != model.contexts.len()
635            + model.providers.len()
636            + model.consumers.len()
637            + graph
638                .nodes
639                .iter()
640                .filter(|node| matches!(node.kind, crate::ContextOwnershipNodeKind::Component))
641                .count()
642    {
643        diagnostics.push(AsmValidationDiagnostic {
644            code: "PSASM1183".to_string(),
645            message: "Context ownership graph has an invalid node domain".to_string(),
646        });
647    }
648    for context in model.contexts.values() {
649        let expected_owner = context.owner.entity_id();
650        let default = context.default_expression.as_ref();
651        if graph.owner_of_context(&context.id) != expected_owner
652            || graph.context_default_expression(&context.id) != default
653            || !expected_owner.is_some_and(|owner| {
654                exactly_one_ownership_edge(
655                    graph,
656                    &crate::ContextOwnershipOwnerId::Component(owner.clone()),
657                    &crate::ContextOwnershipTargetId::Context(context.id.clone()),
658                    crate::ContextOwnershipEdgeKind::ComponentOwnsContext,
659                )
660            })
661        {
662            diagnostics.push(AsmValidationDiagnostic {
663                code: "PSASM1184".to_string(),
664                message: format!("Context `{}` has an invalid ownership record", context.id),
665            });
666        }
667        if let Some(default) = default {
668            let default_provenance = model
669                .expression_graph
670                .node(default)
671                .map(|node| &node.provenance);
672            let default_edge = graph.edges.iter().find(|edge| {
673                edge.owner == crate::ContextOwnershipOwnerId::Context(context.id.clone())
674                    && edge.owned == crate::ContextOwnershipTargetId::Expression(default.clone())
675                    && edge.kind == crate::ContextOwnershipEdgeKind::ContextOwnsDefaultExpression
676            });
677            if default_edge.is_none_or(|edge| Some(&edge.provenance) != default_provenance) {
678                diagnostics.push(AsmValidationDiagnostic {
679                    code: "PSASM1188".to_string(),
680                    message: format!(
681                        "Context `{}` has an invalid default ownership edge",
682                        context.id
683                    ),
684                });
685            }
686        }
687    }
688    for provider in model.providers.values() {
689        if graph.owner_of_provider(&provider.id) != provider.owner.entity_id()
690            || !provider.owner.entity_id().is_some_and(|owner| {
691                exactly_one_ownership_edge(
692                    graph,
693                    &crate::ContextOwnershipOwnerId::Component(owner.clone()),
694                    &crate::ContextOwnershipTargetId::Provider(provider.id.clone()),
695                    crate::ContextOwnershipEdgeKind::ComponentOwnsProvider,
696                )
697            })
698        {
699            diagnostics.push(AsmValidationDiagnostic {
700                code: "PSASM1185".to_string(),
701                message: format!("Provider `{}` has an invalid ownership record", provider.id),
702            });
703        }
704    }
705    for consumer in model.consumers.values() {
706        if graph.owner_of_consumer(&consumer.id) != consumer.owner.entity_id()
707            || !consumer.owner.entity_id().is_some_and(|owner| {
708                exactly_one_ownership_edge(
709                    graph,
710                    &crate::ContextOwnershipOwnerId::Component(owner.clone()),
711                    &crate::ContextOwnershipTargetId::Consumer(consumer.id.clone()),
712                    crate::ContextOwnershipEdgeKind::ComponentOwnsConsumer,
713                )
714            })
715        {
716            diagnostics.push(AsmValidationDiagnostic {
717                code: "PSASM1186".to_string(),
718                message: format!("Consumer `{}` has an invalid ownership record", consumer.id),
719            });
720        }
721    }
722    if graph.edges.windows(2).any(|pair| {
723        (&pair[0].owner, pair[0].kind, &pair[0].owned)
724            > (&pair[1].owner, pair[1].kind, &pair[1].owned)
725    }) || graph.edges.iter().any(|edge| {
726        !matches!(
727            (&edge.owner, &edge.owned, edge.kind),
728            (
729                crate::ContextOwnershipOwnerId::Component(_),
730                crate::ContextOwnershipTargetId::Context(_),
731                crate::ContextOwnershipEdgeKind::ComponentOwnsContext
732            ) | (
733                crate::ContextOwnershipOwnerId::Component(_),
734                crate::ContextOwnershipTargetId::Provider(_),
735                crate::ContextOwnershipEdgeKind::ComponentOwnsProvider
736            ) | (
737                crate::ContextOwnershipOwnerId::Component(_),
738                crate::ContextOwnershipTargetId::Consumer(_),
739                crate::ContextOwnershipEdgeKind::ComponentOwnsConsumer
740            ) | (
741                crate::ContextOwnershipOwnerId::Context(_),
742                crate::ContextOwnershipTargetId::Expression(_),
743                crate::ContextOwnershipEdgeKind::ContextOwnsDefaultExpression
744            )
745        )
746    }) {
747        diagnostics.push(AsmValidationDiagnostic {
748            code: "PSASM1187".to_string(),
749            message: "Context ownership graph has an invalid edge domain or order".to_string(),
750        });
751    }
752}
753
754fn exactly_one_ownership_edge(
755    graph: &crate::ContextOwnershipGraph,
756    source: &crate::ContextOwnershipOwnerId,
757    target: &crate::ContextOwnershipTargetId,
758    kind: crate::ContextOwnershipEdgeKind,
759) -> bool {
760    graph
761        .edges
762        .iter()
763        .filter(|edge| &edge.owner == source && &edge.owned == target && edge.kind == kind)
764        .count()
765        == 1
766}
767
768fn validate_context_typing(
769    model: &ApplicationSemanticModel,
770    diagnostics: &mut Vec<AsmValidationDiagnostic>,
771) {
772    if model.context_types.len() != model.contexts.len()
773        || model.provider_types.len() != model.providers.len()
774        || model.consumer_types.len() != model.consumers.len()
775        || model.context_binding_types.len() != model.consumers.len()
776    {
777        diagnostics.push(AsmValidationDiagnostic {
778            code: "PSASM1176".to_string(),
779            message: "Context typing records do not exactly cover canonical entities".to_string(),
780        });
781    }
782    for context in model.contexts.values() {
783        if model.context_type(&context.id).is_none_or(|record| {
784            record.declared_type != context.declared_type_id
785                || record.normalized_type != context.declared_type_id
786        }) {
787            diagnostics.push(AsmValidationDiagnostic {
788                code: "PSASM1177".to_string(),
789                message: format!("Context `{}` has an invalid type record", context.id),
790            });
791        }
792    }
793    for provider in model.providers.values() {
794        if model.provider_type(&provider.id).is_none_or(|record| {
795            record.declared_type != provider.declared_type_id
796                || record.inferred_value_type == provider.declared_type_id
797                || record.context != Some(provider.context.clone())
798        }) {
799            diagnostics.push(AsmValidationDiagnostic {
800                code: "PSASM1178".to_string(),
801                message: format!("Provider `{}` has an invalid type record", provider.id),
802            });
803        }
804    }
805    for consumer in model.consumers.values() {
806        let Some(binding) = model.context_binding_type(&consumer.id) else {
807            continue;
808        };
809        if model.consumer_type(&consumer.id).is_none_or(|record| {
810            record.requested_type != consumer.requested_type_id
811                || record.context != consumer.context().cloned()
812        }) || model
813            .context_resolution(&consumer.id)
814            .is_none_or(|resolution| binding.resolution != resolution.result)
815        {
816            diagnostics.push(AsmValidationDiagnostic {
817                code: "PSASM1179".to_string(),
818                message: format!("Consumer `{}` has an invalid type record", consumer.id),
819            });
820        }
821        if let crate::ContextResolutionResult::Provider { provider, .. } = &binding.resolution {
822            if binding.provider.as_ref() != Some(provider) {
823                diagnostics.push(AsmValidationDiagnostic {
824                    code: "PSASM1180".to_string(),
825                    message: format!("Consumer `{}` does not retain its G4 Provider", consumer.id),
826                });
827            }
828        }
829        if binding.overall == crate::ContextBindingCompatibility::Compatible
830            && (binding.source_to_context != crate::CompatibilityStatus::Compatible
831                || binding.context_to_consumer != crate::CompatibilityStatus::Compatible
832                || binding.boundary_compatibility != crate::CompatibilityStatus::Compatible
833                || binding.serialization != crate::ContextSerializationCompatibility::Serializable)
834        {
835            diagnostics.push(AsmValidationDiagnostic {
836                code: "PSASM1181".to_string(),
837                message: format!(
838                    "Consumer `{}` has a permissively compatible binding",
839                    consumer.id
840                ),
841            });
842        }
843    }
844}
845
846#[allow(clippy::too_many_lines)]
847fn validate_context_resolution(
848    model: &ApplicationSemanticModel,
849    diagnostics: &mut Vec<AsmValidationDiagnostic>,
850) {
851    for scope_diagnostic in model.component_scope.diagnostics() {
852        diagnostics.push(AsmValidationDiagnostic {
853            code: "PSASM1165".to_string(),
854            message: scope_diagnostic.message,
855        });
856    }
857    if model.context_resolutions.len() != model.consumers.len()
858        || model
859            .context_resolutions
860            .keys()
861            .any(|consumer| !model.consumers.contains_key(consumer))
862    {
863        diagnostics.push(AsmValidationDiagnostic {
864            code: "PSASM1166".to_string(),
865            message: "Context resolution records do not exactly cover canonical Consumers"
866                .to_string(),
867        });
868    }
869
870    for (consumer_id, consumer) in &model.consumers {
871        let Some(resolution) = model.context_resolution(consumer_id) else {
872            continue;
873        };
874        if resolution.consumer != *consumer_id
875            || resolution.provenance != consumer.context_designator.provenance
876            || resolution.context != consumer.context().cloned()
877        {
878            diagnostics.push(AsmValidationDiagnostic {
879                code: "PSASM1167".to_string(),
880                message: format!("consumer `{consumer_id}` has a non-canonical resolution record"),
881            });
882        }
883        let expected_scopes = consumer.owner.entity_id().map_or_else(Vec::new, |owner| {
884            model.component_scope.ancestor_chain(owner)
885        });
886        if resolution.searched_scopes != expected_scopes {
887            diagnostics.push(AsmValidationDiagnostic {
888                code: "PSASM1168".to_string(),
889                message: format!(
890                    "consumer `{consumer_id}` searched non-canonical component scopes"
891                ),
892            });
893        }
894
895        match &resolution.result {
896            crate::ContextResolutionResult::Provider {
897                provider,
898                provider_owner,
899                distance,
900            } => {
901                let provider_entity = model.provider(provider);
902                let context_matches = resolution.context.as_ref().is_some_and(|context| {
903                    provider_entity.is_some_and(|provider| &provider.context == context)
904                });
905                let owner_matches = provider_entity.and_then(|provider| provider.owner.entity_id())
906                    == Some(provider_owner);
907                let expected_owner = resolution.searched_scopes.get(*distance as usize);
908                if !context_matches || !owner_matches || expected_owner != Some(provider_owner) {
909                    diagnostics.push(AsmValidationDiagnostic {
910                        code: "PSASM1169".to_string(),
911                        message: format!(
912                            "consumer `{consumer_id}` has an invalid Provider resolution"
913                        ),
914                    });
915                }
916                if (*distance as usize) > resolution.searched_scopes.len()
917                    || resolution.searched_scopes[..*distance as usize]
918                        .iter()
919                        .any(|scope| {
920                            !resolution_candidates(model, scope, resolution.context.as_ref())
921                                .is_empty()
922                        })
923                    || resolution_candidates(model, provider_owner, resolution.context.as_ref())
924                        .len()
925                        != 1
926                {
927                    diagnostics.push(AsmValidationDiagnostic {
928                        code: "PSASM1170".to_string(),
929                        message: format!(
930                            "consumer `{consumer_id}` does not select the nearest Provider"
931                        ),
932                    });
933                }
934            }
935            crate::ContextResolutionResult::ContextDefault {
936                context,
937                expression,
938            } => {
939                let default_is_canonical = model.context(context).is_some_and(|context_entity| {
940                    context_entity.default_expression.is_some()
941                        && model.expression_root(context.as_semantic_id()) == Some(expression)
942                });
943                if resolution.context.as_ref() != Some(context)
944                    || !default_is_canonical
945                    || resolution
946                        .searched_scopes
947                        .iter()
948                        .any(|scope| !resolution_candidates(model, scope, Some(context)).is_empty())
949                {
950                    diagnostics.push(AsmValidationDiagnostic {
951                        code: "PSASM1171".to_string(),
952                        message: format!(
953                            "consumer `{consumer_id}` has an invalid Context default fallback"
954                        ),
955                    });
956                }
957            }
958            crate::ContextResolutionResult::Unresolved => {
959                let has_visible_provider = resolution.searched_scopes.iter().any(|scope| {
960                    !resolution_candidates(model, scope, resolution.context.as_ref()).is_empty()
961                });
962                let has_default = resolution.context.as_ref().is_some_and(|context| {
963                    model
964                        .context(context)
965                        .is_some_and(|entity| entity.default_expression.is_some())
966                });
967                if resolution.context.is_none() || has_visible_provider || has_default {
968                    diagnostics.push(AsmValidationDiagnostic {
969                        code: "PSASM1172".to_string(),
970                        message: format!(
971                            "consumer `{consumer_id}` has an invalid unresolved result"
972                        ),
973                    });
974                }
975            }
976            crate::ContextResolutionResult::Ambiguous {
977                providers,
978                distance,
979            } => {
980                let Some(scope) = resolution.searched_scopes.get(*distance as usize) else {
981                    diagnostics.push(AsmValidationDiagnostic {
982                        code: "PSASM1173".to_string(),
983                        message: format!(
984                            "consumer `{consumer_id}` has an invalid ambiguity distance"
985                        ),
986                    });
987                    continue;
988                };
989                let candidates = resolution_candidates(model, scope, resolution.context.as_ref());
990                if providers.len() < 2
991                    || providers.windows(2).any(|pair| pair[0] >= pair[1])
992                    || *providers != candidates
993                    || resolution.searched_scopes[..*distance as usize]
994                        .iter()
995                        .any(|scope| {
996                            !resolution_candidates(model, scope, resolution.context.as_ref())
997                                .is_empty()
998                        })
999                {
1000                    diagnostics.push(AsmValidationDiagnostic {
1001                        code: "PSASM1174".to_string(),
1002                        message: format!(
1003                            "consumer `{consumer_id}` has an invalid ambiguity result"
1004                        ),
1005                    });
1006                }
1007            }
1008            crate::ContextResolutionResult::InvalidContextReference => {
1009                if resolution.context.is_some() || consumer.context().is_some() {
1010                    diagnostics.push(AsmValidationDiagnostic {
1011                        code: "PSASM1175".to_string(),
1012                        message: format!(
1013                            "consumer `{consumer_id}` has an invalid Context-reference result"
1014                        ),
1015                    });
1016                }
1017            }
1018        }
1019    }
1020}
1021
1022fn resolution_candidates(
1023    model: &ApplicationSemanticModel,
1024    scope: &crate::SemanticId,
1025    context: Option<&crate::ContextId>,
1026) -> Vec<crate::ProviderId> {
1027    let Some(context) = context else {
1028        return Vec::new();
1029    };
1030    model
1031        .providers
1032        .values()
1033        .filter(|provider| {
1034            provider.owner.entity_id() == Some(scope) && provider.context == *context
1035        })
1036        .map(|provider| provider.id.clone())
1037        .collect()
1038}
1039
1040fn validate_contexts(
1041    model: &ApplicationSemanticModel,
1042    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1043) {
1044    for context in model.contexts.values() {
1045        let Some(component_id) = context.owner.entity_id() else {
1046            diagnostics.push(AsmValidationDiagnostic {
1047                code: "PSASM1135".to_string(),
1048                message: format!("context `{}` is not component-owned", context.id),
1049            });
1050            continue;
1051        };
1052        let Some(component) = model.component(component_id) else {
1053            diagnostics.push(AsmValidationDiagnostic {
1054                code: "PSASM1136".to_string(),
1055                message: format!("context `{}` has a missing component owner", context.id),
1056            });
1057            continue;
1058        };
1059        if context.id != crate::ContextId::for_component(component_id, &context.name) {
1060            diagnostics.push(AsmValidationDiagnostic {
1061                code: "PSASM1137".to_string(),
1062                message: format!("context `{}` has a non-canonical identity", context.id),
1063            });
1064        }
1065        if context.authored_field != component.id.context_field(&context.name) {
1066            diagnostics.push(AsmValidationDiagnostic {
1067                code: "PSASM1138".to_string(),
1068                message: format!(
1069                    "context `{}` has a non-canonical authored field",
1070                    context.id
1071                ),
1072            });
1073        }
1074        if context.declared_type.text.is_empty() {
1075            diagnostics.push(AsmValidationDiagnostic {
1076                code: "PSASM1139".to_string(),
1077                message: format!(
1078                    "context `{}` is missing an explicit declared type",
1079                    context.id
1080                ),
1081            });
1082        }
1083        if context.execution_boundary != crate::ExecutionBoundary::Client {
1084            diagnostics.push(AsmValidationDiagnostic {
1085                code: "PSASM1140".to_string(),
1086                message: format!(
1087                    "context `{}` has a non-client execution boundary",
1088                    context.id
1089                ),
1090            });
1091        }
1092        if component
1093            .state_fields
1094            .iter()
1095            .any(|field| field.name == context.name)
1096        {
1097            diagnostics.push(AsmValidationDiagnostic {
1098                code: "PSASM1141".to_string(),
1099                message: format!("context `{}` also lowered as state", context.id),
1100            });
1101        }
1102        let declaration = component
1103            .context_declarations
1104            .iter()
1105            .find(|declaration| declaration.authored_field == context.authored_field);
1106        if declaration.is_none_or(|declaration| declaration.provenance != context.provenance) {
1107            diagnostics.push(AsmValidationDiagnostic {
1108                code: "PSASM1142".to_string(),
1109                message: format!(
1110                    "context `{}` has non-canonical field provenance",
1111                    context.id
1112                ),
1113            });
1114        }
1115        if context.default_expression != model.expression_root(context.id.as_semantic_id()).cloned()
1116        {
1117            diagnostics.push(AsmValidationDiagnostic {
1118                code: "PSASM1143".to_string(),
1119                message: format!("context `{}` has an invalid default expression", context.id),
1120            });
1121        }
1122    }
1123}
1124
1125#[allow(clippy::too_many_lines)]
1126fn validate_providers(
1127    model: &ApplicationSemanticModel,
1128    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1129) {
1130    for provider in model.providers.values() {
1131        let Some(component_id) = provider.owner.entity_id() else {
1132            diagnostics.push(AsmValidationDiagnostic {
1133                code: "PSASM1144".to_string(),
1134                message: format!("provider `{}` is not component-owned", provider.id),
1135            });
1136            continue;
1137        };
1138        let Some(component) = model.component(component_id) else {
1139            diagnostics.push(AsmValidationDiagnostic {
1140                code: "PSASM1145".to_string(),
1141                message: format!("provider `{}` has a missing component owner", provider.id),
1142            });
1143            continue;
1144        };
1145        if provider.id != crate::ProviderId::for_component(component_id, &provider.name) {
1146            diagnostics.push(AsmValidationDiagnostic {
1147                code: "PSASM1146".to_string(),
1148                message: format!("provider `{}` has a non-canonical identity", provider.id),
1149            });
1150        }
1151        if provider.authored_field != component.id.provider_field(&provider.name) {
1152            diagnostics.push(AsmValidationDiagnostic {
1153                code: "PSASM1147".to_string(),
1154                message: format!(
1155                    "provider `{}` has a non-canonical authored field",
1156                    provider.id
1157                ),
1158            });
1159        }
1160        let context = model.context(&provider.context);
1161        if context.is_none() {
1162            diagnostics.push(AsmValidationDiagnostic {
1163                code: "PSASM1148".to_string(),
1164                message: format!("provider `{}` targets a missing Context", provider.id),
1165            });
1166        }
1167        if context.is_some_and(|context| {
1168            context.name != provider.context_designator.context_member
1169                || context
1170                    .owner
1171                    .entity_id()
1172                    .and_then(|owner| model.component(owner))
1173                    .is_none_or(|owner| {
1174                        owner.class_name != provider.context_designator.component_symbol
1175                    })
1176        }) {
1177            diagnostics.push(AsmValidationDiagnostic {
1178                code: "PSASM1149".to_string(),
1179                message: format!(
1180                    "provider `{}` has a mismatched Context designator",
1181                    provider.id
1182                ),
1183            });
1184        }
1185        if provider.declared_type.text.is_empty() {
1186            diagnostics.push(AsmValidationDiagnostic {
1187                code: "PSASM1150".to_string(),
1188                message: format!(
1189                    "provider `{}` is missing an explicit declared type",
1190                    provider.id
1191                ),
1192            });
1193        }
1194        if model.expression_root(provider.id.as_semantic_id()) != Some(&provider.value_expression) {
1195            diagnostics.push(AsmValidationDiagnostic {
1196                code: "PSASM1151".to_string(),
1197                message: format!("provider `{}` has an invalid value expression", provider.id),
1198            });
1199        }
1200        if provider.execution_boundary != crate::ExecutionBoundary::Client {
1201            diagnostics.push(AsmValidationDiagnostic {
1202                code: "PSASM1152".to_string(),
1203                message: format!(
1204                    "provider `{}` has a non-client execution boundary",
1205                    provider.id
1206                ),
1207            });
1208        }
1209        if component
1210            .state_fields
1211            .iter()
1212            .any(|field| field.name == provider.name)
1213            || component
1214                .context_declarations
1215                .iter()
1216                .any(|context| context.name == provider.name)
1217        {
1218            diagnostics.push(AsmValidationDiagnostic {
1219                code: "PSASM1153".to_string(),
1220                message: format!(
1221                    "provider `{}` has a conflicting semantic primitive",
1222                    provider.id
1223                ),
1224            });
1225        }
1226        let declaration = component
1227            .provider_declarations
1228            .iter()
1229            .find(|declaration| declaration.authored_field == provider.authored_field);
1230        if declaration.is_none_or(|declaration| declaration.provenance != provider.provenance) {
1231            diagnostics.push(AsmValidationDiagnostic {
1232                code: "PSASM1154".to_string(),
1233                message: format!(
1234                    "provider `{}` has non-canonical field provenance",
1235                    provider.id
1236                ),
1237            });
1238        }
1239    }
1240}
1241
1242#[allow(clippy::too_many_lines)]
1243fn validate_consumers(
1244    model: &ApplicationSemanticModel,
1245    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1246) {
1247    for consumer in model.consumers.values() {
1248        let Some(component_id) = consumer.owner.entity_id() else {
1249            diagnostics.push(AsmValidationDiagnostic {
1250                code: "PSASM1155".to_string(),
1251                message: format!("consumer `{}` is not component-owned", consumer.id),
1252            });
1253            continue;
1254        };
1255        let Some(component) = model.component(component_id) else {
1256            diagnostics.push(AsmValidationDiagnostic {
1257                code: "PSASM1156".to_string(),
1258                message: format!("consumer `{}` has a missing component owner", consumer.id),
1259            });
1260            continue;
1261        };
1262        if consumer.id != crate::ConsumerId::for_component(component_id, &consumer.name) {
1263            diagnostics.push(AsmValidationDiagnostic {
1264                code: "PSASM1157".to_string(),
1265                message: format!("consumer `{}` has a non-canonical identity", consumer.id),
1266            });
1267        }
1268        if consumer.authored_field != component.id.consumer_field(&consumer.name) {
1269            diagnostics.push(AsmValidationDiagnostic {
1270                code: "PSASM1158".to_string(),
1271                message: format!(
1272                    "consumer `{}` has a non-canonical authored field",
1273                    consumer.id
1274                ),
1275            });
1276        }
1277        if let crate::ContextResolutionState::Resolved(context_id) = &consumer.context_resolution {
1278            let context = model.context(context_id);
1279            if context.is_none() {
1280                diagnostics.push(AsmValidationDiagnostic {
1281                    code: "PSASM1159".to_string(),
1282                    message: format!("consumer `{}` targets a missing Context", consumer.id),
1283                });
1284            }
1285            if context.is_some_and(|context| {
1286                context.name != consumer.context_designator.context_member
1287                    || context
1288                        .owner
1289                        .entity_id()
1290                        .and_then(|owner| model.component(owner))
1291                        .is_none_or(|owner| {
1292                            owner.class_name != consumer.context_designator.component_symbol
1293                        })
1294            }) {
1295                diagnostics.push(AsmValidationDiagnostic {
1296                    code: "PSASM1160".to_string(),
1297                    message: format!(
1298                        "consumer `{}` has a mismatched Context designator",
1299                        consumer.id
1300                    ),
1301                });
1302            }
1303        }
1304        if consumer.requested_type.text.is_empty() {
1305            diagnostics.push(AsmValidationDiagnostic {
1306                code: "PSASM1161".to_string(),
1307                message: format!(
1308                    "consumer `{}` is missing an explicit requested type",
1309                    consumer.id
1310                ),
1311            });
1312        }
1313        if consumer.execution_boundary != crate::ExecutionBoundary::Client {
1314            diagnostics.push(AsmValidationDiagnostic {
1315                code: "PSASM1162".to_string(),
1316                message: format!(
1317                    "consumer `{}` has a non-client execution boundary",
1318                    consumer.id
1319                ),
1320            });
1321        }
1322        if component
1323            .state_fields
1324            .iter()
1325            .any(|field| field.name == consumer.name)
1326            || component
1327                .context_declarations
1328                .iter()
1329                .any(|context| context.name == consumer.name)
1330            || component
1331                .provider_declarations
1332                .iter()
1333                .any(|provider| provider.name == consumer.name)
1334        {
1335            diagnostics.push(AsmValidationDiagnostic {
1336                code: "PSASM1163".to_string(),
1337                message: format!(
1338                    "consumer `{}` has a conflicting semantic primitive",
1339                    consumer.id
1340                ),
1341            });
1342        }
1343        let declaration = component
1344            .consumer_declarations
1345            .iter()
1346            .find(|declaration| declaration.authored_field == consumer.authored_field);
1347        if declaration.is_none_or(|declaration| declaration.provenance != consumer.provenance) {
1348            diagnostics.push(AsmValidationDiagnostic {
1349                code: "PSASM1164".to_string(),
1350                message: format!(
1351                    "consumer `{}` has non-canonical field provenance",
1352                    consumer.id
1353                ),
1354            });
1355        }
1356    }
1357}
1358
1359#[allow(clippy::too_many_lines)]
1360fn validate_component_diagnostic_metadata(
1361    model: &ApplicationSemanticModel,
1362    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1363) {
1364    let actual_component_diagnostics = model
1365        .diagnostics
1366        .iter()
1367        .filter(|item| ("PSC1068"..="PSC1083").contains(&item.code.as_str()))
1368        .cloned()
1369        .collect::<Vec<_>>();
1370    let expected_component_diagnostics = crate::collect_component_diagnostics(model);
1371    if actual_component_diagnostics != expected_component_diagnostics {
1372        diagnostics.push(AsmValidationDiagnostic {
1373            code: "PSASM1201".to_string(),
1374            message: "component diagnostics do not match the canonical H19 projection".to_string(),
1375        });
1376    }
1377    for diagnostic in &model.diagnostics {
1378        let valid_context = diagnostic
1379            .context_id
1380            .as_ref()
1381            .is_none_or(|id| model.context(id).is_some());
1382        let valid_provider = diagnostic
1383            .provider_id
1384            .as_ref()
1385            .is_none_or(|id| model.provider(id).is_some());
1386        let valid_consumer = diagnostic
1387            .consumer_id
1388            .as_ref()
1389            .is_none_or(|id| model.consumer(id).is_some());
1390        let valid_candidate = diagnostic
1391            .context_declaration_candidate_id
1392            .as_ref()
1393            .is_none_or(|id| {
1394                model
1395                    .context_declaration_candidates()
1396                    .candidate(id)
1397                    .is_some()
1398            });
1399        if !(valid_context && valid_provider && valid_consumer && valid_candidate) {
1400            diagnostics.push(AsmValidationDiagnostic {
1401                code: "PSASM1135".to_string(),
1402                message: format!(
1403                    "compiler diagnostic `{}` references an unknown Context diagnostic subject",
1404                    diagnostic.code
1405                ),
1406            });
1407        }
1408        if let (Some(provider), Some(context)) = (
1409            diagnostic
1410                .provider_id
1411                .as_ref()
1412                .and_then(|id| model.provider(id)),
1413            diagnostic.context_id.as_ref(),
1414        ) {
1415            if &provider.context != context {
1416                diagnostics.push(AsmValidationDiagnostic {
1417                    code: "PSASM1138".to_string(),
1418                    message: format!(
1419                        "compiler diagnostic `{}` references a Provider for the wrong Context",
1420                        diagnostic.code
1421                    ),
1422                });
1423            }
1424        }
1425        if let (Some(consumer), Some(context)) = (
1426            diagnostic
1427                .consumer_id
1428                .as_ref()
1429                .and_then(|id| model.consumer(id)),
1430            diagnostic.context_id.as_ref(),
1431        ) {
1432            if consumer.context() != Some(context) {
1433                diagnostics.push(AsmValidationDiagnostic {
1434                    code: "PSASM1139".to_string(),
1435                    message: format!(
1436                        "compiler diagnostic `{}` references a Consumer for the wrong Context",
1437                        diagnostic.code
1438                    ),
1439                });
1440            }
1441        }
1442        if diagnostic.context_declaration_candidate_id.is_some()
1443            && (diagnostic.context_id.is_some()
1444                || diagnostic.provider_id.is_some()
1445                || diagnostic.consumer_id.is_some())
1446        {
1447            diagnostics.push(AsmValidationDiagnostic { code: "PSASM1136".to_string(), message: format!("invalid Context declaration diagnostic `{}` carries a semantic entity identity", diagnostic.code) });
1448        }
1449        if ("PSC1052"..="PSC1067").contains(&diagnostic.code.as_str()) {
1450            if let Some(primary) = diagnostic.provenance.as_ref() {
1451                if !context_diagnostic_primary_is_canonical(model, diagnostic, primary) {
1452                    diagnostics.push(AsmValidationDiagnostic {
1453                        code: "PSASM1140".to_string(),
1454                        message: format!(
1455                            "Context diagnostic `{}` has non-canonical primary provenance",
1456                            diagnostic.code
1457                        ),
1458                    });
1459                }
1460            }
1461            if diagnostic.code == "PSC1058" {
1462                let mut expected = diagnostic
1463                    .consumer_id
1464                    .as_ref()
1465                    .and_then(|consumer| model.context_resolutions.get(consumer))
1466                    .and_then(|resolution| match &resolution.result {
1467                        crate::ContextResolutionResult::Ambiguous { providers, .. } => {
1468                            Some(providers.clone())
1469                        }
1470                        _ => None,
1471                    })
1472                    .unwrap_or_default();
1473                expected.sort();
1474                expected.dedup();
1475                let expected = expected
1476                    .iter()
1477                    .filter_map(|id| model.provider(id))
1478                    .map(|provider| crate::DiagnosticSecondaryLabel {
1479                        provenance: provider.provenance.clone(),
1480                        message: format!("Candidate Provider `{}`.", provider.id),
1481                    })
1482                    .collect::<Vec<_>>();
1483                if diagnostic.secondary_labels != expected {
1484                    diagnostics.push(AsmValidationDiagnostic {
1485                        code: "PSASM1134".to_string(),
1486                        message: "Context ambiguity diagnostic has non-canonical Provider evidence"
1487                            .to_string(),
1488                    });
1489                }
1490            }
1491        }
1492        let effect = diagnostic.effect_id.as_ref().and_then(|effect_id| {
1493            model
1494                .effects
1495                .values()
1496                .find(|effect| effect.id.as_str() == effect_id.as_str())
1497        });
1498        if diagnostic.effect_id.is_some() && effect.is_none() {
1499            diagnostics.push(AsmValidationDiagnostic {
1500                code: "PSASM1129".to_string(),
1501                message: format!(
1502                    "compiler diagnostic `{}` references a missing effect subject",
1503                    diagnostic.code
1504                ),
1505            });
1506        }
1507
1508        let statement = diagnostic.statement_id.as_ref().and_then(|statement_id| {
1509            model
1510                .effect_statements
1511                .values()
1512                .find(|statement| statement.id.as_str() == statement_id.as_str())
1513        });
1514        if diagnostic.statement_id.is_some() && diagnostic.effect_id.is_none() {
1515            diagnostics.push(AsmValidationDiagnostic {
1516                code: "PSASM1130".to_string(),
1517                message: format!(
1518                    "compiler diagnostic `{}` has an effect statement without an effect subject",
1519                    diagnostic.code
1520                ),
1521            });
1522        }
1523        if diagnostic.statement_id.is_some()
1524            && statement
1525                .is_none_or(|statement| effect.is_none_or(|effect| statement.owner != effect.id))
1526        {
1527            diagnostics.push(AsmValidationDiagnostic {
1528                code: "PSASM1131".to_string(),
1529                message: format!(
1530                    "compiler diagnostic `{}` has a statement that does not belong to its effect",
1531                    diagnostic.code
1532                ),
1533            });
1534        }
1535
1536        let primary_subject = statement
1537            .map(|statement| &statement.provenance)
1538            .or_else(|| effect.map(|effect| &effect.provenance));
1539        if let (Some(primary), Some(subject)) = (&diagnostic.provenance, primary_subject) {
1540            if !provenance_contains(subject, primary) {
1541                diagnostics.push(AsmValidationDiagnostic {
1542                    code: "PSASM1132".to_string(),
1543                    message: format!(
1544                        "compiler diagnostic `{}` has non-canonical primary provenance",
1545                        diagnostic.code
1546                    ),
1547                });
1548            }
1549        }
1550
1551        let mut sorted = diagnostic.secondary_labels.clone();
1552        sorted.sort_by(secondary_label_order);
1553        sorted.dedup();
1554        if sorted != diagnostic.secondary_labels {
1555            diagnostics.push(AsmValidationDiagnostic {
1556                code: "PSASM1133".to_string(),
1557                message: format!(
1558                    "compiler diagnostic `{}` has unordered or duplicate secondary labels",
1559                    diagnostic.code
1560                ),
1561            });
1562        }
1563        for label in &diagnostic.secondary_labels {
1564            if diagnostic.provenance.as_ref() == Some(&label.provenance) {
1565                diagnostics.push(AsmValidationDiagnostic {
1566                    code: "PSASM1137".to_string(),
1567                    message: format!(
1568                        "compiler diagnostic `{}` repeats primary provenance as a secondary label",
1569                        diagnostic.code
1570                    ),
1571                });
1572            }
1573            let canonical = model
1574                .provenance
1575                .values()
1576                .any(|provenance| provenance == &label.provenance)
1577                || model
1578                    .expression_graph
1579                    .nodes
1580                    .values()
1581                    .any(|expression| expression.provenance == label.provenance)
1582                || context_diagnostic_secondary_is_canonical(model, diagnostic, &label.provenance);
1583            if !canonical {
1584                diagnostics.push(AsmValidationDiagnostic {
1585                    code: "PSASM1134".to_string(),
1586                    message: format!(
1587                        "compiler diagnostic `{}` has non-canonical secondary-label provenance",
1588                        diagnostic.code
1589                    ),
1590                });
1591            }
1592        }
1593    }
1594}
1595
1596fn context_diagnostic_secondary_is_canonical(
1597    model: &ApplicationSemanticModel,
1598    diagnostic: &crate::ComponentDiagnostic,
1599    provenance: &crate::SourceProvenance,
1600) -> bool {
1601    match diagnostic.code.as_str() {
1602        "PSC1059" => diagnostic
1603            .provider_id
1604            .as_ref()
1605            .and_then(|id| model.provider(id))
1606            .is_some_and(|provider| &provider.declared_type.provenance == provenance),
1607        "PSC1060" | "PSC1061" | "PSC1062" | "PSC1063" | "PSC1064" => diagnostic
1608            .context_id
1609            .as_ref()
1610            .and_then(|id| model.context(id))
1611            .is_some_and(|context| &context.declared_type.provenance == provenance),
1612        _ => false,
1613    }
1614}
1615
1616#[allow(clippy::too_many_lines)]
1617fn context_diagnostic_primary_is_canonical(
1618    model: &ApplicationSemanticModel,
1619    diagnostic: &crate::ComponentDiagnostic,
1620    primary: &crate::SourceProvenance,
1621) -> bool {
1622    match diagnostic.code.as_str() {
1623        "PSC1052" | "PSC1053" | "PSC1054" | "PSC1055" | "PSC1056" => diagnostic
1624            .context_declaration_candidate_id
1625            .as_ref()
1626            .and_then(|id| model.context_declaration_candidates().candidate(id))
1627            .is_some_and(|candidate| {
1628                let crate::ContextDeclarationStatus::Invalid(violations) = &candidate.status else {
1629                    return false;
1630                };
1631                let expected = match violations.first() {
1632                    Some(crate::ContextDeclarationViolation::StaticDeclarationUnsupported) => {
1633                        candidate.authored.static_modifier_provenance.as_ref()
1634                    }
1635                    Some(
1636                        crate::ContextDeclarationViolation::UnsupportedInitializer
1637                        | crate::ContextDeclarationViolation::ForbiddenInitializer
1638                        | crate::ContextDeclarationViolation::MissingInitializer,
1639                    ) => candidate.authored.initializer_provenance.as_ref(),
1640                    Some(
1641                        crate::ContextDeclarationViolation::ContextDesignatorUnsupported
1642                        | crate::ContextDeclarationViolation::UnresolvedContextDesignator,
1643                    ) => candidate
1644                        .authored
1645                        .context_designator
1646                        .as_ref()
1647                        .map(|designator| &designator.provenance),
1648                    Some(_) => Some(&candidate.authored.decorator_provenance),
1649                    None => None,
1650                }
1651                .unwrap_or(&candidate.authored.provenance);
1652                expected == primary
1653            }),
1654        "PSC1057" | "PSC1058" => diagnostic
1655            .consumer_id
1656            .as_ref()
1657            .and_then(|id| model.context_resolutions.get(id))
1658            .is_some_and(|record| &record.provenance == primary),
1659        "PSC1059" => {
1660            diagnostic
1661                .provider_id
1662                .as_ref()
1663                .and_then(|id| model.provider(id))
1664                .and_then(|provider| {
1665                    model
1666                        .expression_graph
1667                        .provenance_of(&provider.value_expression)
1668                })
1669                == Some(primary)
1670        }
1671        "PSC1060" => diagnostic
1672            .provider_id
1673            .as_ref()
1674            .and_then(|id| model.provider(id))
1675            .is_some_and(|provider| &provider.declared_type.provenance == primary),
1676        "PSC1061" => {
1677            diagnostic
1678                .context_id
1679                .as_ref()
1680                .and_then(|id| model.context(id))
1681                .and_then(|context| context.default_expression.as_ref())
1682                .and_then(|expression| model.expression_graph.provenance_of(expression))
1683                == Some(primary)
1684        }
1685        "PSC1062" => diagnostic
1686            .consumer_id
1687            .as_ref()
1688            .and_then(|id| model.consumer(id))
1689            .is_some_and(|consumer| &consumer.requested_type.provenance == primary),
1690        "PSC1063" | "PSC1064" => {
1691            if let Some(provider) = diagnostic
1692                .provider_id
1693                .as_ref()
1694                .and_then(|id| model.provider(id))
1695            {
1696                return model
1697                    .expression_graph
1698                    .provenance_of(&provider.value_expression)
1699                    == Some(primary);
1700            }
1701            diagnostic
1702                .context_id
1703                .as_ref()
1704                .and_then(|id| model.context(id))
1705                .is_some_and(|context| {
1706                    context
1707                        .default_expression
1708                        .as_ref()
1709                        .and_then(|expression| model.expression_graph.provenance_of(expression))
1710                        .unwrap_or(&context.declared_type.provenance)
1711                        == primary
1712                })
1713        }
1714        "PSC1065" => {
1715            diagnostic
1716                .consumer_id
1717                .as_ref()
1718                .and_then(|id| model.context_lifetime.binding_lifetimes.get(id))
1719                .is_some_and(|record| &record.provenance == primary)
1720                || model
1721                    .context_lifetime
1722                    .dependency_lifetimes
1723                    .iter()
1724                    .any(|record| &record.provenance == primary)
1725        }
1726        "PSC1066" | "PSC1067" => model
1727            .context_evaluation
1728            .source_entries
1729            .values()
1730            .filter(|entry| diagnostic.context_id.as_ref() == Some(&entry.context))
1731            .filter(|entry| match &entry.source {
1732                crate::ContextValueSourceId::Provider(provider) => {
1733                    diagnostic.provider_id.as_ref() == Some(provider)
1734                }
1735                crate::ContextValueSourceId::ContextDefault(_) => diagnostic.provider_id.is_none(),
1736            })
1737            .any(|entry| {
1738                if diagnostic.code == "PSC1067" {
1739                    return model
1740                        .expression_graph
1741                        .provenance_of(&entry.expression_root)
1742                        .unwrap_or(&entry.provenance)
1743                        == primary;
1744                }
1745                let dependent = match &entry.source {
1746                    crate::ContextValueSourceId::Provider(provider) => {
1747                        crate::ContextDependencyNodeId::Provider(provider.clone())
1748                    }
1749                    crate::ContextValueSourceId::ContextDefault(context) => {
1750                        crate::ContextDependencyNodeId::ContextDefault(context.clone())
1751                    }
1752                };
1753                entry.reasons.iter().any(|reason| {
1754                    let dependency = match reason {
1755                        crate::ContextSourceBlockReason::MissingStateDependency(id) => {
1756                            crate::ContextDependencyNodeId::State(id.clone())
1757                        }
1758                        crate::ContextSourceBlockReason::UnavailableComputedDependency(id) => {
1759                            crate::ContextDependencyNodeId::Computed(id.clone())
1760                        }
1761                        _ => return false,
1762                    };
1763                    model.context_dependency.edges.iter().any(|edge| {
1764                        edge.dependent == dependent
1765                            && edge.dependency == dependency
1766                            && &edge.provenance == primary
1767                    })
1768                }) || &entry.provenance == primary
1769            }),
1770        _ => false,
1771    }
1772}
1773
1774fn provenance_contains(
1775    subject: &crate::SourceProvenance,
1776    primary: &crate::SourceProvenance,
1777) -> bool {
1778    subject.path == primary.path
1779        && subject.span.start <= primary.span.start
1780        && primary.span.end <= subject.span.end
1781}
1782
1783fn secondary_label_order(
1784    left: &crate::DiagnosticSecondaryLabel,
1785    right: &crate::DiagnosticSecondaryLabel,
1786) -> std::cmp::Ordering {
1787    (
1788        left.provenance.path.as_path(),
1789        left.provenance.span.start,
1790        left.provenance.span.end,
1791        left.message.as_str(),
1792    )
1793        .cmp(&(
1794            right.provenance.path.as_path(),
1795            right.provenance.span.start,
1796            right.provenance.span.end,
1797            right.message.as_str(),
1798        ))
1799}
1800
1801fn validate_template_action_bindings(
1802    model: &ApplicationSemanticModel,
1803    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1804) {
1805    let manifest = build_template_manifest_from_asm(model);
1806    if manifest.schema_version < 2 || manifest.schema_version > TEMPLATE_MANIFEST_SCHEMA_VERSION {
1807        return;
1808    }
1809    if manifest.schema_version == TEMPLATE_MANIFEST_SCHEMA_VERSION {
1810        for event in &manifest.ordinary_events {
1811            let component = model
1812                .components
1813                .iter()
1814                .find(|component| component.id.as_str() == event.component_id);
1815            // The v4 execution table is the authority. Declaration-template
1816            // events remain inspection context and deliberately need not be
1817            // executable for every materialized instance.
1818            let Some(component) = component else {
1819                diagnostics.push(AsmValidationDiagnostic {
1820                    code: "PSASM1126".to_string(),
1821                    message: "ordinary event references a missing component".to_string(),
1822                });
1823                continue;
1824            };
1825            let method = component
1826                .methods
1827                .iter()
1828                .find(|method| method.id.as_str() == event.handler_method_id);
1829            let batch = method.and_then(|method| {
1830                model
1831                    .effect_trigger_plan
1832                    .action_batches
1833                    .values()
1834                    .find(|batch| batch.authored_action_method == method.id)
1835            });
1836            if batch.is_none_or(|batch| Some(batch.id.as_str()) != event.action_batch_id.as_deref())
1837            {
1838                diagnostics.push(AsmValidationDiagnostic {
1839                    code: "PSASM1128".to_string(),
1840                    message: "ordinary event does not resolve to its canonical F8 action batch"
1841                        .to_string(),
1842                });
1843            }
1844        }
1845        return;
1846    }
1847    for component_manifest in &manifest.components {
1848        let Some(component) = model
1849            .components
1850            .iter()
1851            .find(|component| component.class_name == component_manifest.name)
1852        else {
1853            diagnostics.push(AsmValidationDiagnostic {
1854                code: "PSASM1126".to_string(),
1855                message: format!(
1856                    "template manifest references missing component `{}`",
1857                    component_manifest.name
1858                ),
1859            });
1860            continue;
1861        };
1862        for event in &component_manifest.template.events {
1863            if event.kind != Some(ManifestEventKind::Action) {
1864                diagnostics.push(AsmValidationDiagnostic {
1865                    code: "PSASM1127".to_string(),
1866                    message: format!(
1867                        "template event `{}` is missing canonical action binding metadata",
1868                        event.node
1869                    ),
1870                });
1871                continue;
1872            }
1873            let method = component
1874                .methods
1875                .iter()
1876                .find(|method| Some(method.id.as_str()) == event.method_id.as_deref());
1877            let batch = method.and_then(|method| {
1878                model
1879                    .effect_trigger_plan
1880                    .action_batches
1881                    .values()
1882                    .find(|batch| batch.authored_action_method == method.id)
1883            });
1884            if batch.is_none_or(|batch| Some(batch.id.as_str()) != event.action_batch_id.as_deref())
1885            {
1886                diagnostics.push(AsmValidationDiagnostic {
1887                    code: "PSASM1128".to_string(),
1888                    message: format!(
1889                        "template event `{}` does not resolve to its canonical F8 action batch",
1890                        event.node
1891                    ),
1892                });
1893            }
1894        }
1895    }
1896}
1897
1898fn validate_effect_execution_plan(
1899    model: &ApplicationSemanticModel,
1900    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1901) {
1902    let plan = model.effect_execution_plan();
1903    if plan.initial.render_boundary != Some(EffectRenderBoundary::AfterInitialRender) {
1904        diagnostics.push(AsmValidationDiagnostic {
1905            code: "PSASM1120".to_string(),
1906            message: "initial effect execution plan is missing the after-initial-render boundary"
1907                .to_string(),
1908        });
1909    }
1910    validate_effect_execution_entry(
1911        model,
1912        &model.effect_trigger_plan.initial_effects,
1913        &plan.initial.required_computed,
1914        &plan.initial.prerequisite_batches,
1915        &plan.initial.effect_batches,
1916        &plan.initial.unplanned_effects,
1917        "initial",
1918        diagnostics,
1919    );
1920    for action in &plan.actions {
1921        let Some(trigger) = model
1922            .effect_trigger_plan
1923            .action_batch_triggers
1924            .iter()
1925            .find(|trigger| trigger.action_batch == action.action_batch)
1926        else {
1927            diagnostics.push(AsmValidationDiagnostic {
1928                code: "PSASM1121".to_string(),
1929                message: format!(
1930                    "effect execution plan references untriggered action batch `{}`",
1931                    action.action_batch
1932                ),
1933            });
1934            continue;
1935        };
1936        validate_effect_execution_entry(
1937            model,
1938            &trigger.effects,
1939            &action.required_computed,
1940            &action.prerequisite_batches,
1941            &action.effect_batches,
1942            &action.unplanned_effects,
1943            action.action_batch.as_str(),
1944            diagnostics,
1945        );
1946    }
1947}
1948
1949#[allow(clippy::too_many_arguments)]
1950fn validate_effect_execution_entry(
1951    model: &ApplicationSemanticModel,
1952    eligible_effects: &[crate::SemanticId],
1953    required_computed: &[crate::SemanticId],
1954    prerequisite_batches: &[crate::EffectComputedPrerequisiteBatch],
1955    effect_batches: &[crate::EffectExecutionBatch],
1956    unplanned_effects: &[crate::UnplannedEffect],
1957    context: &str,
1958    diagnostics: &mut Vec<AsmValidationDiagnostic>,
1959) {
1960    let required = required_computed
1961        .iter()
1962        .collect::<std::collections::BTreeSet<_>>();
1963    if required.len() != required_computed.len()
1964        || required_computed
1965            .iter()
1966            .any(|computed| !model.computed_values.contains_key(computed))
1967    {
1968        diagnostics.push(AsmValidationDiagnostic {
1969            code: "PSASM1122".to_string(),
1970            message: format!("effect plan `{context}` has invalid required computed membership"),
1971        });
1972    }
1973    let mut batch_membership = std::collections::BTreeSet::new();
1974    let mut prior_source_index = None;
1975    for batch in prerequisite_batches {
1976        let expected = model
1977            .computed_evaluation_plan
1978            .update_batches
1979            .get(batch.source_batch_index as usize)
1980            .map(|source| {
1981                source
1982                    .iter()
1983                    .filter_map(|id| {
1984                        model
1985                            .computed_values
1986                            .keys()
1987                            .find(|computed| computed.as_str() == id)
1988                    })
1989                    .filter(|computed| required.contains(computed))
1990                    .cloned()
1991                    .collect::<Vec<_>>()
1992            });
1993        if expected.as_ref() != Some(&batch.computed)
1994            || prior_source_index.is_some_and(|prior| prior >= batch.source_batch_index)
1995        {
1996            diagnostics.push(AsmValidationDiagnostic {
1997                code: "PSASM1123".to_string(),
1998                message: format!(
1999                    "effect plan `{context}` does not preserve canonical computed batch membership"
2000                ),
2001            });
2002        }
2003        prior_source_index = Some(batch.source_batch_index);
2004        batch_membership.extend(batch.computed.iter());
2005    }
2006    if batch_membership != required {
2007        diagnostics.push(AsmValidationDiagnostic {
2008            code: "PSASM1124".to_string(),
2009            message: format!(
2010                "effect plan `{context}` required computed values do not match prerequisite batches"
2011            ),
2012        });
2013    }
2014
2015    let scheduled = effect_batches
2016        .iter()
2017        .flat_map(|batch| &batch.effects)
2018        .collect::<std::collections::BTreeSet<_>>();
2019    let unplanned = unplanned_effects
2020        .iter()
2021        .map(|record| &record.effect)
2022        .collect::<std::collections::BTreeSet<_>>();
2023    let eligible = eligible_effects
2024        .iter()
2025        .collect::<std::collections::BTreeSet<_>>();
2026    let covered = scheduled
2027        .union(&unplanned)
2028        .copied()
2029        .collect::<std::collections::BTreeSet<_>>();
2030    if covered != eligible
2031        || scheduled.intersection(&unplanned).next().is_some()
2032        || scheduled.iter().any(|effect| {
2033            model
2034                .effects
2035                .get(*effect)
2036                .is_none_or(|effect| effect.validation != EffectValidation::Valid)
2037        })
2038    {
2039        diagnostics.push(AsmValidationDiagnostic {
2040            code: "PSASM1125".to_string(),
2041            message: format!("effect plan `{context}` has invalid effect eligibility membership"),
2042        });
2043    }
2044    if effect_batches.iter().enumerate().any(|(index, batch)| {
2045        batch.index != u32::try_from(index).expect("effect scheduler batch index should fit u32")
2046    }) {
2047        diagnostics.push(AsmValidationDiagnostic {
2048            code: "PSASM1126".to_string(),
2049            message: format!("effect plan `{context}` has non-contiguous terminal batch indexes"),
2050        });
2051    }
2052}
2053
2054fn validate_effect_statement_types(
2055    model: &ApplicationSemanticModel,
2056    diagnostics: &mut Vec<AsmValidationDiagnostic>,
2057) {
2058    for (statement, record) in &model.semantic_types.effect_statements {
2059        if statement != &record.statement {
2060            diagnostics.push(AsmValidationDiagnostic {
2061                code: "PSASM1110".to_string(),
2062                message: format!(
2063                    "effect statement type record key `{statement}` does not match statement `{}`",
2064                    record.statement
2065                ),
2066            });
2067        }
2068        let Some(canonical_statement) = model.effect_statement(statement) else {
2069            diagnostics.push(AsmValidationDiagnostic {
2070                code: "PSASM1111".to_string(),
2071                message: format!(
2072                    "effect statement type record references missing statement `{statement}`"
2073                ),
2074            });
2075            continue;
2076        };
2077        if canonical_statement.provenance != record.provenance {
2078            diagnostics.push(AsmValidationDiagnostic {
2079                code: "PSASM1112".to_string(),
2080                message: format!(
2081                    "effect statement type record for `{statement}` has inconsistent provenance"
2082                ),
2083            });
2084        }
2085        let operation_exists = record.capability_operation.is_some_and(|operation_id| {
2086            EFFECT_CAPABILITY_REGISTRY
2087                .definitions()
2088                .iter()
2089                .flat_map(|definition| definition.operations)
2090                .any(|operation| operation.id == operation_id)
2091        });
2092        if record.operation_classification == EffectOperationClassification::RecognizedCapability
2093            && !operation_exists
2094        {
2095            diagnostics.push(AsmValidationDiagnostic {
2096                code: "PSASM1113".to_string(),
2097                message: format!(
2098                    "recognized effect statement `{statement}` has no registry capability operation"
2099                ),
2100            });
2101        }
2102        if record.operation_classification != EffectOperationClassification::RecognizedCapability
2103            && record.capability_operation.is_some()
2104        {
2105            diagnostics.push(AsmValidationDiagnostic {
2106                code: "PSASM1114".to_string(),
2107                message: format!(
2108                    "non-capability effect statement `{statement}` unexpectedly names a registry operation"
2109                ),
2110            });
2111        }
2112    }
2113}
2114
2115fn validate_semantic_types(
2116    model: &ApplicationSemanticModel,
2117    diagnostics: &mut Vec<AsmValidationDiagnostic>,
2118) {
2119    for (subject, assignment) in &model.semantic_types.assignments {
2120        if subject != &assignment.subject {
2121            diagnostics.push(AsmValidationDiagnostic {
2122                code: "PSASM1101".to_string(),
2123                message: format!(
2124                    "semantic type assignment map key `{subject}` does not match subject `{}`",
2125                    assignment.subject
2126                ),
2127            });
2128        }
2129        if assignment.id != SemanticTypeId::for_subject(&assignment.subject) {
2130            diagnostics.push(AsmValidationDiagnostic {
2131                code: "PSASM1102".to_string(),
2132                message: format!(
2133                    "semantic type assignment for `{subject}` has an invalid canonical type ID `{}`",
2134                    assignment.id
2135                ),
2136            });
2137        }
2138        let subject_provenance = model
2139            .provenance(&assignment.subject)
2140            .or_else(|| model.expression_provenance(&assignment.subject));
2141        if subject_provenance.is_none() {
2142            diagnostics.push(AsmValidationDiagnostic {
2143                code: "PSASM1103".to_string(),
2144                message: format!(
2145                    "semantic type assignment references missing subject `{}`",
2146                    assignment.subject
2147                ),
2148            });
2149        }
2150        if subject_provenance != Some(&assignment.provenance) {
2151            diagnostics.push(AsmValidationDiagnostic {
2152                code: "PSASM1104".to_string(),
2153                message: format!(
2154                    "semantic type assignment for `{subject}` has inconsistent provenance"
2155                ),
2156            });
2157        }
2158        if model.entity(&assignment.origin).is_none()
2159            && !model
2160                .semantic_types
2161                .aliases
2162                .contains_key(&assignment.origin)
2163        {
2164            diagnostics.push(AsmValidationDiagnostic {
2165                code: "PSASM1105".to_string(),
2166                message: format!(
2167                    "semantic type assignment for `{subject}` has unresolved origin `{}`",
2168                    assignment.origin
2169                ),
2170            });
2171        }
2172    }
2173
2174    for alias in model.semantic_types.aliases.values() {
2175        let expected = crate::SemanticId::type_alias_in_module(&alias.provenance.path, &alias.name);
2176        if alias.id != expected {
2177            diagnostics.push(AsmValidationDiagnostic {
2178                code: "PSASM1106".to_string(),
2179                message: format!(
2180                    "type alias `{}` has invalid canonical identity `{}`",
2181                    alias.name, alias.id
2182                ),
2183            });
2184        }
2185    }
2186}