Skip to main content

presolve_compiler/
lib.rs

1//! Core compiler data structures for the first `Presolve` learning slice.
2//!
3//! This crate deliberately does **not** parse TSX yet. It records a source summary,
4//! spans, obvious declarations, and diagnostics. That gives the project a stable
5//! place to learn compiler fundamentals before choosing a real parser backend.
6
7pub mod action_authority;
8pub mod action_field_lowering;
9pub mod application_publication;
10pub mod application_semantic_model;
11pub mod asm_validation;
12pub mod authored_semantics;
13pub mod binding_table;
14pub mod capability_projection;
15pub mod capture_escape;
16pub mod codec_protocol;
17pub mod compilation_unit;
18pub mod compiler_pass;
19pub mod component_composition;
20pub mod component_diagnostics;
21pub mod component_graph;
22pub mod component_inheritance_lowering;
23pub mod component_initialization;
24pub mod component_instance;
25pub mod component_instance_scope;
26pub mod component_invocation;
27pub mod component_ir;
28pub mod component_ir_optimization;
29pub mod component_scope;
30pub mod composition_typing;
31pub mod computed_getter_lowering;
32pub mod computed_instance_slots;
33pub mod computed_projection;
34pub mod computed_value;
35pub mod consumer;
36pub mod context;
37pub mod context_declaration_candidate;
38pub mod context_dependency;
39mod context_designator;
40pub mod context_diagnostics;
41pub mod context_evaluation;
42pub mod context_inspection;
43pub mod context_lifetime;
44pub mod context_ownership;
45pub mod context_projection;
46pub mod context_resolution;
47pub mod context_resume;
48pub mod context_resume_projection;
49pub mod context_typing;
50pub mod context_update;
51pub mod control_flow;
52pub mod document_template;
53pub mod effect;
54pub mod effect_capability;
55pub mod effect_diagnostics;
56pub mod effect_field_lowering;
57pub mod effect_inspection;
58pub mod effect_projection;
59pub mod effect_resume;
60pub mod environment_input;
61pub mod environment_ownership;
62pub mod environment_publication;
63pub mod environment_read_lowering;
64pub mod environment_read_ownership;
65pub mod explain;
66pub mod expression_graph;
67pub mod file_route_publication;
68pub mod form;
69pub mod form_binding;
70pub mod form_diagnostics;
71pub mod form_field;
72pub mod form_inspection;
73pub mod form_ir;
74pub mod form_ir_optimization;
75pub mod form_ownership;
76pub mod form_projection;
77pub mod form_reset;
78pub mod form_serialization;
79pub mod form_submission;
80pub mod form_submission_host;
81pub mod form_submission_projection;
82pub mod form_tracking;
83pub mod form_validation;
84pub mod form_validation_plan;
85pub mod function_summary;
86pub mod html_codegen;
87pub mod incremental_projection;
88pub mod instance_context;
89pub mod intermediate_representation;
90#[cfg(any(test, all(feature = "wasm", target_arch = "wasm32")))]
91mod language_service;
92pub mod layout_composition;
93pub mod layout_graph;
94pub mod lazy_action_chunks;
95pub mod legacy_decorator_lowering;
96pub mod metaframework_handoff;
97pub mod model;
98pub mod module_graph;
99pub mod opaque;
100pub mod ordinary_html_codegen;
101pub mod ordinary_template_instance;
102pub mod ordinary_template_integrity;
103pub mod page_codegen;
104pub mod persistent_cache;
105pub mod platform;
106pub mod production_audit;
107pub mod production_bootstrap;
108pub mod production_chunk_graph;
109pub mod production_cleanup;
110pub mod production_constant_pool;
111pub mod production_deduplication;
112pub mod production_diagnostics;
113pub mod production_elimination;
114pub mod production_module_emitter;
115pub mod production_optimization;
116pub mod production_reachability;
117pub mod production_reports;
118pub mod production_runtime_artifact;
119pub mod production_scheduler;
120pub mod production_validation;
121pub mod project_discovery;
122pub mod provider;
123pub mod purity_effect;
124pub mod resource;
125pub mod resource_projection;
126pub mod resume_activation;
127pub mod resume_anchor;
128pub mod resume_boot;
129pub mod resume_boundary;
130pub mod resume_capture;
131pub mod resume_chunk;
132pub mod resume_diagnostics;
133pub mod resume_explain;
134pub mod resume_identity;
135pub mod resume_instance;
136pub mod resume_liveness;
137pub mod resume_manifest;
138pub mod resume_plan;
139pub mod resume_restore;
140pub mod resume_schema;
141pub mod route_graph;
142pub mod route_loader;
143pub mod route_loader_projection;
144pub mod route_metadata;
145pub mod route_server_action;
146pub mod runtime_codegen;
147pub mod runtime_component;
148pub mod runtime_component_artifact;
149pub mod runtime_computed;
150pub mod runtime_computed_artifact;
151pub mod runtime_context;
152pub mod runtime_context_artifact;
153pub mod runtime_effect;
154pub mod runtime_effect_artifact;
155pub mod runtime_effect_instance;
156pub mod runtime_form_artifact;
157pub mod runtime_form_registry;
158pub mod runtime_opaque_artifact;
159pub mod runtime_resource_artifact;
160pub mod semantic_capability;
161pub mod semantic_graph;
162pub mod semantic_id;
163pub mod semantic_package;
164pub mod semantic_package_runtime;
165pub mod semantic_provenance;
166pub mod semantic_reference;
167pub mod semantic_type;
168pub mod server_action_projection;
169pub mod service;
170pub mod shared_chunk_candidate;
171pub mod slot;
172pub mod slot_binding;
173pub mod slot_content;
174pub mod slot_field_lowering;
175pub mod slot_projection;
176pub mod state_initializer_lowering;
177pub mod state_instance_storage;
178pub mod state_projection;
179pub mod structural_component;
180pub mod structural_occurrence_identity;
181pub mod summarize;
182pub mod symbol_table;
183pub mod template_graph;
184pub mod template_manifest;
185pub mod template_semantics;
186pub mod tooling_products;
187pub mod tooling_reader;
188pub mod tooling_schema;
189pub mod tsx_binding_projection;
190pub mod v2_authoring_lowering;
191pub mod v2_authority_request;
192pub mod v2_authority_response;
193pub mod validation_projection;
194pub use tooling_products::{
195    build_tooling_artifact_graph_v1, build_tooling_build_trace_v1,
196    build_tooling_compile_cost_report_v1, decode_tooling_artifact_graph_v1,
197    decode_tooling_build_trace_v1, decode_tooling_compile_cost_report_v1,
198    decode_tooling_query_snapshot_v1, tooling_artifact_graph_json_v1, tooling_build_trace_json_v1,
199    tooling_compile_cost_report_json_v1, tooling_query_snapshot_json_v1, ToolingArtifactGraphV1,
200    ToolingBuildTraceStageV1, ToolingBuildTraceV1, ToolingCompileCostReportV1,
201    ToolingProductValidationErrorV1, ToolingQueryDiagnosticSecondaryV1,
202    ToolingQueryDiagnosticSeverityV1, ToolingQueryDiagnosticV1, ToolingQueryRangeV1,
203    ToolingQueryReferenceKindV1, ToolingQueryReferenceV1, ToolingQuerySemanticKindV1,
204    ToolingQuerySemanticRecordV1, ToolingQuerySnapshotSourceUnitV1, ToolingQuerySnapshotV1,
205    ToolingTraceIdentityV1, ToolingTraceOutcomeV1, ToolingTraceStageKindV1,
206    ARTIFACT_GRAPH_TOOLING_SCHEMA_V1, BUILD_TRACE_TOOLING_SCHEMA_V1,
207    COMPILE_COST_TOOLING_SCHEMA_V1, QUERY_SNAPSHOT_TOOLING_SCHEMA_V1,
208};
209pub mod watch;
210pub mod workspace;
211
212#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
213mod wasm_binding;
214
215pub use action_authority::{
216    build_action_authority_v1, ActionAdmissionV1, ActionAuthorityV1, ActionCaptureCoverageV1,
217    ActionEnvironmentV1, ActionFactV1, ActionRecordV1, ACTION_AUTHORITY_SCHEMA_VERSION,
218};
219pub use action_field_lowering::{
220    action_field_sites_v1, lower_action_fields_v1, ActionFieldLoweringErrorV1,
221    ActionFieldLoweringV1, ActionFieldSiteV1, ResolvedActionFieldV1,
222};
223pub use application_publication::{
224    application_publication_manifest_json_v1, build_application_publication_product_from_asm_v1,
225    build_application_publication_product_v1, validate_application_publication_request_v1,
226    ApplicationPublicationArtifactV1, ApplicationPublicationErrorV1,
227    ApplicationPublicationManifestV1, ApplicationPublicationProductV1,
228    ApplicationPublicationProfileV1, ApplicationPublicationRequestErrorV1,
229    ApplicationPublicationRequestV1, ApplicationPublicationSourceV1,
230    ValidatedApplicationPublicationRequestV1, APPLICATION_PUBLICATION_COMPILER_CONTRACT_V1,
231    APPLICATION_PUBLICATION_MANIFEST_SCHEMA_VERSION,
232};
233pub use application_semantic_model::{
234    build_application_semantic_model, build_application_semantic_model_for_unit,
235    build_application_semantic_model_for_unit_with_packages,
236    build_application_semantic_model_from_component_graph,
237    build_file_route_application_semantic_model_for_route_with_packages,
238    build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring,
239    build_file_route_application_semantic_model_for_unit_with_packages,
240    build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring,
241    ApplicationSemanticModel, FileRouteApplicationModelErrorV1, SemanticEntity, SemanticEntityKind,
242};
243pub use asm_validation::{validate_application_semantic_model, AsmValidationDiagnostic};
244pub use authored_semantics::{
245    compose_authored_semantics_v1, normalize_authored_semantics_v1,
246    AuthoredSemanticCandidateKindV1, AuthoredSemanticCompositionErrorV1,
247    AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
248    CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredDeclarationV1,
249    CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1, DerivedAuthoredEvidenceV2,
250    ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
251    CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
252};
253pub use binding_table::{
254    build_binding_table, build_binding_table_with_packages, BindingDiagnostic, BindingTable,
255    ExportBinding, ImportBinding, ImportBindingTarget, ModuleBindingTable,
256};
257pub use capture_escape::{
258    build_capture_escape_graph_v1, CaptureEscapeCoverageV1, CaptureEscapeErrorV1,
259    CaptureEscapeFactV1, CaptureEscapeFactsV1, CaptureEscapeGraphV1, CaptureEscapeKindV1,
260    FunctionCaptureEscapeV1, ResumeCaptureAdmissionV1, CAPTURE_ESCAPE_SCHEMA_VERSION,
261};
262pub use codec_protocol::{
263    build_codec_protocol_v1, CodecBehaviorV1, CodecClassificationV1, CodecClassificationsV1,
264    CodecDeclarationV1, CodecEnvironmentV1, CodecFailureBehaviorV1,
265    CodecProtocolDiagnosticReasonV1, CodecProtocolDiagnosticV1, CodecProtocolRecordV1,
266    CodecProtocolV1, CodecRepresentationV1, CODEC_PROTOCOL_SCHEMA_VERSION,
267};
268pub use compilation_unit::CompilationUnit;
269pub use compiler_pass::{
270    fold_component_graph, AnalysisPass, ConstantEvaluation, ConstantEvaluationPass,
271    ConstantFoldingPass, DependencyAnalysis, DependencyAnalysisPass, ImmutableAsmPass,
272};
273pub use component_composition::{
274    analyze_component_composition, ComponentCompositionAnalysis, ComponentCompositionCycle,
275};
276pub use component_diagnostics::{
277    collect_component_diagnostics, ComponentDiagnosticContract, COMPONENT_DIAGNOSTIC_CONTRACTS,
278};
279pub use component_graph::{
280    build_component_graph, build_component_graph_for_module, build_v2_component_graph_for_module,
281    is_valid_opaque_action_fact, ActionEndpoint, ArithmeticEvaluationError, ArithmeticExpression,
282    ArithmeticExpressionKind, ArithmeticOperator, AuthoredComponentHeritage,
283    AuthoredContextDeclarationCandidate, AuthoredDeclarationKind, AuthoredOpaqueActionFact,
284    AuthoredRouteLoaderDeclarationFact, AuthoredServerActionFact, AuthoredSlotDeclarationCandidate,
285    AuthoredSubmissionDeclarationFact, AuthoredValidationRuleArgument,
286    AuthoredValidationRuleArgumentKind, AuthoredValidationRuleDeclarationFact,
287    AuthoredValidationRuleExpression, AuthoredValidationRuleExpressionKind, ComparisonOperator,
288    ComponentAction, ComponentDiagnostic, ComponentDiagnosticSeverity, ComponentEffectField,
289    ComponentGraph, ComponentMethod, ComponentNode, ComputedExpression, ComputedExpressionKind,
290    ConstantEvaluationError, ConstantExpression, ConstantExpressionKind, ConsumerDeclaration,
291    ContextDeclaration, ContextDeclarationCandidateKind, ContextDeclarationViolation,
292    ContextDesignator, DeclaredStateType, DeclaredStateTypeKind, DiagnosticSecondaryLabel,
293    EffectBodySyntax, EffectCleanupSyntax, EffectExpression, EffectExpressionKind,
294    EffectStatementSyntax, EffectStatementSyntaxKind, FormDeclarationCandidate,
295    FormDeclarationStatus, FormDeclarationViolation, FormDesignatorFact,
296    FormFieldDeclarationCandidate, FormFieldDeclarationViolation, LogicalOperator, MethodCall,
297    MethodLocalVariable, MethodParameter, RenderAttribute, RenderAttributeValue, RenderChild,
298    RenderEventHandler, RenderFragment, RenderList, RenderModel, SerializableValue,
299    SlotDeclaration, SlotDeclarationViolation, SlotKind, StateField, StateOperation,
300    UnsupportedEffectStatementKind, UnsupportedFormDesignatorFact,
301};
302pub use component_inheritance_lowering::{
303    component_inheritance_sites_v1, lower_component_inheritance_v1,
304    ComponentInheritanceLoweringErrorV1, ComponentInheritanceLoweringV1,
305    ComponentInheritanceSiteV1, ResolvedComponentInheritanceV1,
306};
307pub use component_initialization::{
308    plan_component_initialization, ComponentInitializationPlan, ComponentInstanceBatch,
309    SlotBindingBatch,
310};
311pub use component_instance::{
312    plan_component_instances, plan_component_instances_with_virtual_invocations,
313    structural_template_entity_for_region, BlockedComponentInstancePlan,
314    BlockedComponentInstanceReason, ComponentBuildRoot, ComponentBuildRootKind, ComponentInstance,
315    ComponentInstancePlan, ComponentInstanceStatus,
316};
317pub use component_instance_scope::{
318    build_component_instance_scope_graph, validate_component_instance_scope_graph,
319    ComponentInstanceScopeDiagnostic, ComponentInstanceScopeGraph, ComponentInstanceScopeNode,
320    ComponentInstanceScopeViolation,
321};
322pub use component_invocation::{
323    collect_component_invocations, ComponentInvocationEntity, ComponentInvocationResolutionStatus,
324};
325pub use component_ir::{
326    lower_component_ir, validate_component_ir, ComponentIrInstruction, ComponentIrOperation,
327    ComponentIrReport,
328};
329pub use component_ir_optimization::{
330    optimize_component_ir, validate_optimized_component_ir, OptimizedComponentIrReport,
331};
332pub use component_scope::{ComponentScopeDiagnostic, ComponentScopeGraph};
333pub use composition_typing::{
334    collect_composition_type_products, ComponentInvocationTypeRecord, CompositionCompatibility,
335    CompositionTypeProducts, InstanceContextBindingTypeRecord, SlotBindingTypeRecord,
336};
337pub use computed_getter_lowering::{
338    computed_getter_sites_v1, lower_computed_getters_v1, ComputedGetterLoweringErrorV1,
339    ComputedGetterLoweringV1, ComputedGetterSiteV1,
340};
341pub use computed_instance_slots::{
342    build_computed_instance_slot_registry, validate_computed_instance_slot_registry,
343    ComputedInstanceSlotRecord, ComputedInstanceSlotRegistry,
344    COMPUTED_INSTANCE_SLOT_REGISTRY_VERSION,
345};
346pub use computed_projection::{
347    build_computed_projection_v1, ComputedProjectionRecordV1, ComputedProjectionV1,
348    COMPUTED_PROJECTION_SCHEMA_VERSION,
349};
350pub use computed_value::{
351    collect_computed_values, ComputedCachePolicy, ComputedDiagnosticCode, ComputedPurity,
352    ComputedPurityViolation, ComputedPurityViolationKind, ComputedValue,
353};
354pub use consumer::{collect_consumer_entities, ConsumerEntity, ContextResolutionState};
355pub use context::{collect_context_entities, ContextEntity};
356pub use context_declaration_candidate::{
357    collect_context_declaration_candidates, ContextDeclarationCandidate,
358    ContextDeclarationCandidateRegistry, ContextDeclarationStatus, ContextSemanticEntityId,
359};
360pub use context_dependency::{
361    collect_context_dependency_graph, ContextDependencyCompatibility, ContextDependencyEdge,
362    ContextDependencyEdgeKind, ContextDependencyGraph, ContextDependencyNode,
363    ContextDependencyNodeId, ContextDependencyNodeKind,
364};
365pub use context_diagnostics::collect_context_diagnostics;
366pub use context_evaluation::{
367    collect_context_evaluation_plan, ContextConsumerAvailabilityEntry,
368    ContextConsumerAvailabilityStatus, ContextEvaluationBatch, ContextEvaluationBatchId,
369    ContextEvaluationPlan, ContextEvaluationPlanId, ContextSourceBlockReason,
370    ContextSourcePlanEntry, ContextSourcePlanStatus, ContextValueSourceId,
371};
372pub use context_inspection::{
373    build_context_inspection_registry, ContextInspection, ContextInspectionRegistry,
374};
375pub use context_lifetime::{
376    collect_context_lifetime_analysis, ContextBindingLifetimeRecord, ContextBindingLifetimeSource,
377    ContextBindingLifetimeStatus, ContextDefaultLifetimeRecord, ContextDependencyLifetimeRecord,
378    ContextEntityLifetimeRecord, ContextLifetimeAnalysis, ContextLifetimeEntityId,
379    ContextLifetimeId, LifetimeCompatibilityStatus, ProviderLifetimeRecord,
380};
381pub use context_ownership::{
382    collect_context_ownership_graph, ContextOwnedEntities, ContextOwnershipEdge,
383    ContextOwnershipEdgeKind, ContextOwnershipGraph, ContextOwnershipNode, ContextOwnershipNodeId,
384    ContextOwnershipNodeKind, ContextOwnershipOwnerId, ContextOwnershipTargetId,
385};
386pub use context_projection::{
387    build_context_projection_v1, ContextProjectionV1, ContextProviderRecordV1,
388    ContextTokenRecordV1, CONTEXT_PROJECTION_SCHEMA_VERSION,
389};
390pub use context_resolution::{
391    collect_context_resolutions, ContextResolution, ContextResolutionResult,
392};
393pub use context_resume::{
394    build_context_resume_plan, ContextResumePlan, ContextResumeRecord, ContextResumeSlotId,
395    ContextSlotResumeStatus,
396};
397pub use context_resume_projection::{
398    build_context_resume_projection_v1, ContextConsumerResolutionV1,
399    ContextResumeProjectionRecordV1, ContextResumeProjectionV1,
400    CONTEXT_RESUME_PROJECTION_SCHEMA_VERSION,
401};
402pub use context_typing::{
403    collect_context_type_products, CompatibilityStatus, ConsumerTypeRecord,
404    ContextBindingCompatibility, ContextBindingTypeRecord, ContextSerializationCompatibility,
405    ContextTypeProducts, ContextTypeRecord, ProviderTypeRecord,
406};
407pub use context_update::{build_context_update_plan, ContextActionUpdatePlan, ContextUpdatePlan};
408pub use control_flow::{
409    build_control_flow_graph_v1, ControlFlowAccessKindV1, ControlFlowAccessV1, ControlFlowBlockV1,
410    ControlFlowBranchArmV1, ControlFlowBranchEdgeV1, ControlFlowCoverageStatusV1,
411    ControlFlowCoverageV1, ControlFlowFunctionV1, ControlFlowGraphV1, ControlFlowLoopV1,
412    ControlFlowProvenanceV1, CONTROL_FLOW_SCHEMA_VERSION,
413};
414pub use document_template::{apply_document_template_v1, DocumentTemplateErrorV1};
415pub use effect::{
416    analyze_effect_reactivity, collect_effects, derive_effect_trigger_plan, lower_effect_bodies,
417    plan_effect_execution, validate_effects, ActionBatch, ActionBatchEffectTrigger,
418    ActionEffectExecutionPlan, Effect, EffectBody, EffectComputedPrerequisiteBatch,
419    EffectDeclaration, EffectExecutionBatch, EffectExecutionPlan, EffectExecutionPolicy,
420    EffectReactiveAnalysis, EffectRenderBoundary, EffectSemanticViolation,
421    EffectSemanticViolationKind, EffectStatement, EffectStatementKind, EffectTriggerPlan,
422    EffectValidation, InitialEffectExecutionPlan, UnplannedEffect, UnplannedEffectReason,
423};
424pub use effect_capability::{
425    ArgumentSerializationPolicy, BuiltinCapabilityProvenance, CapabilityDefinition, CapabilityId,
426    CapabilityOperation, CapabilityOperationId, CapabilityOperationKind, CapabilityParameters,
427    CapabilityResultPolicy, CapabilitySignature, CapabilityValueContract, EffectCapabilityRegistry,
428    RuntimeCapabilityLowering, StaticCapabilityPath, EFFECT_CAPABILITY_REGISTRY,
429    EFFECT_CAPABILITY_REGISTRY_VERSION,
430};
431pub use effect_diagnostics::{collect_effect_diagnostics, EffectDiagnosticCode};
432pub use effect_field_lowering::{
433    effect_field_sites_v1, lower_effect_fields_v1, EffectFieldLoweringErrorV1,
434    EffectFieldLoweringV1, EffectFieldSiteV1, ResolvedEffectFieldV1,
435};
436pub use effect_inspection::{
437    build_effect_inspection_registry, validate_effect_inspection_registry, EffectInspection,
438    EffectInspectionActionTrigger, EffectInspectionCapability, EffectInspectionDependencies,
439    EffectInspectionInitialTrigger, EffectInspectionIr, EffectInspectionPrerequisiteBatch,
440    EffectInspectionProvenance, EffectInspectionRegistry, EffectInspectionResumability,
441    EffectInspectionRuntime, EffectInspectionSchedule, EffectInspectionScheduledAction,
442    EffectInspectionUnplanned, EffectInspectionValidation, EffectInspectionValidationDiagnostic,
443    EffectInspectionViolation,
444};
445pub use effect_projection::{
446    build_effect_projection_v1, EffectProjectionRecordV1, EffectProjectionV1,
447    EFFECT_PROJECTION_SCHEMA_VERSION,
448};
449pub use effect_resume::{
450    build_effect_resume_plan, validate_effect_resume_plan, EffectActivationSlotId,
451    EffectActivationStatus, EffectInitialResumeMembership, EffectResumePlan, EffectResumeRecord,
452    EffectResumeValidationDiagnostic,
453};
454pub use environment_input::{
455    build_environment_input_manifest_v1, environment_input_manifest_from_json_v1,
456    environment_input_manifest_json_v1, validate_environment_input_manifest_v1,
457    EnvironmentInputErrorV1, EnvironmentInputManifestV1, ENVIRONMENT_INPUT_SCHEMA_VERSION,
458};
459pub use environment_ownership::{
460    build_environment_ownership_graph_v1, EnvironmentClassV1, EnvironmentOwnershipDiagnosticV1,
461    EnvironmentOwnershipEdgeKindV1, EnvironmentOwnershipEdgeV1, EnvironmentOwnershipErrorV1,
462    EnvironmentOwnershipFactsV1, EnvironmentOwnershipGraphV1, EnvironmentOwnershipNodeV1,
463    EnvironmentOwnershipViolationV1, LifetimeClassV1, ENVIRONMENT_OWNERSHIP_SCHEMA_VERSION,
464};
465pub use environment_publication::{
466    build_environment_publication_artifact_v1, environment_publication_artifact_json_v1,
467    validate_environment_publication_artifact_v1, EnvironmentPublicationArtifactV1,
468    EnvironmentPublicationErrorV1, ENVIRONMENT_PUBLICATION_SCHEMA_VERSION,
469};
470pub use environment_read_lowering::{
471    lower_environment_reads_v1, EnvironmentReadDiagnosticCodeV1, EnvironmentReadDiagnosticV1,
472    EnvironmentReadLoweringV1, EnvironmentReadRecordV1, ENVIRONMENT_READ_LOWERING_SCHEMA_VERSION,
473};
474pub use environment_read_ownership::{
475    build_environment_read_ownership_v1, EnvironmentReadOwnershipErrorV1,
476};
477pub use explain::{explain_json, explain_text};
478pub use expression_graph::{ExpressionGraph, ExpressionNode, ExpressionNodeKind};
479pub use file_route_publication::{
480    build_file_route_publication_v1, file_route_artifact_root_v1,
481    file_route_publication_manifest_json_v1, resolve_file_route_request_match_v1,
482    resolve_file_route_request_v1, FileRoutePublicationErrorV1, FileRoutePublicationManifestV1,
483    FileRoutePublicationProductV1, FileRoutePublicationRequestV1, FileRoutePublicationRouteV1,
484    FileRouteRequestMatchV1, FileRouteRequestTargetV1, FILE_ROUTE_ENVIRONMENT_ARTIFACT_PATH_V1,
485    FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1, FILE_ROUTE_PUBLICATION_MANIFEST_SCHEMA_VERSION,
486};
487pub use form::{collect_form_entities, FormEntity};
488pub use form_binding::{
489    collect_form_field_binding_products, FormControlChannel, FormControlCompatibility,
490    FormControlNormalization, FormFieldBinding, FormFieldBindingCandidate,
491    FormFieldBindingEvidence, FormFieldBindingEvidenceKind, FormFieldBindingExpressionFact,
492    FormFieldBindingProducts, FormFieldBindingViolation, FormInputKind,
493};
494pub use form_diagnostics::{
495    collect_form_diagnostics, FormDiagnosticReservation, FORM_DIAGNOSTIC_RESERVATIONS,
496};
497pub use form_field::{collect_form_field_products, FormFieldEntity, FormFieldProducts};
498pub use form_inspection::{build_form_inspection_registry, FormInspection, FormInspectionRegistry};
499pub use form_ir::{
500    lower_form_ir, FormInstanceIr, FormIrOperation, FormIrReport, FormRuntimeStorage,
501};
502pub use form_ir_optimization::{
503    optimize_form_ir, FormIrOptimizationMetrics, OptimizedFormIrReport,
504};
505pub use form_ownership::{
506    collect_form_ownership_graph, validate_form_ownership_graph, FormOwnershipEdge,
507    FormOwnershipEdgeKind, FormOwnershipGraph, FormOwnershipIntegrityDiagnostic,
508    FormOwnershipIntegrityKind, FormOwnershipNode, FormOwnershipNodeKey, FormOwnershipValidation,
509    FormReferenceEdge, FormReferenceKind,
510};
511pub use form_reset::{
512    collect_reset_products, FieldResetOperation, FieldResetStep, FormResetPlan, ResetProducts,
513};
514pub use form_serialization::{
515    collect_serialization_products, FormFieldSerializationConversion, FormSerializationFormat,
516    FormSerializationPlan, SerializationDeclarationFact, SerializationPlanStatus,
517    SerializationProducts, SerializedFieldPlan,
518};
519pub use form_submission::{
520    collect_submission_products, FormSubmissionPlan, SubmissionDeclarationCandidate,
521    SubmissionDeclarationViolation, SubmissionProducts, SubmitResetPolicy,
522};
523pub use form_submission_host::{
524    collect_submission_host_products, SubmissionHost, SubmissionHostCandidate,
525    SubmissionHostProducts, SubmissionHostViolation,
526};
527pub use form_tracking::{
528    collect_form_tracking_products, structurally_equal_serializable_values,
529    validate_dirty_tracking_graph, validate_touched_tracking_graph, DirtyTrackingGraph,
530    DirtyTrackingPlan, DirtyTransitionCause, DirtyTransitionPlan, FieldDirtyTracking,
531    FieldTouchedTracking, FormTrackingIntegrityDiagnostic, FormTrackingIntegrityKind,
532    FormTrackingProducts, FormTrackingValidation, TouchedTrackingGraph, TouchedTrackingPlan,
533    TouchedTransitionCause, TouchedTransitionPlan,
534};
535pub use form_validation::{
536    collect_validation_graph, collect_validation_products, validate_validation_graph,
537    ValidationCompatibility, ValidationDependencyCycle, ValidationDependencyDesignator,
538    ValidationGraph, ValidationGraphEdge, ValidationGraphEdgeKind,
539    ValidationGraphIntegrityDiagnostic, ValidationGraphIntegrityKind, ValidationGraphNode,
540    ValidationGraphNodeKey, ValidationGraphValidation, ValidationProducts, ValidationRule,
541    ValidationRuleArgument, ValidationRuleCandidate, ValidationRuleKind, ValidationRuleViolation,
542};
543pub use form_validation_plan::{
544    collect_validation_dependency_plans, validate_validation_dependency_plans,
545    BlockedFieldValidationDependency, FieldChangeSet, FieldChangeValidationSchedule,
546    FieldDependencyBlockReason, FieldValidationChangePlan, FieldValidationDependency,
547    FieldValidationSourceEntry, FieldValidationTargetEntry, FormValidationDependencyPlan,
548    ValidationDependencyPlanIntegrityDiagnostic, ValidationDependencyPlanIntegrityKind,
549    ValidationDependencyPlanValidation, ValidationDependencyPlans, ValidationPlanningStatus,
550};
551pub use function_summary::{
552    build_function_summary_graph_v1, FunctionCallCoverageV1, FunctionCallFactV1,
553    FunctionCallFactsV1, FunctionSummaryErrorV1, FunctionSummaryGraphV1, FunctionSummaryV1,
554    FUNCTION_SUMMARY_SCHEMA_VERSION,
555};
556pub use html_codegen::generate_static_html;
557pub use instance_context::{
558    collect_instance_context_registry, ConsumerInstanceId, ConsumerInstanceRecord,
559    ContextDefaultSourceInstanceId, ContextSourceInstanceId, ContextSourceInstanceOwner,
560    InstanceContextRegistry, InstanceContextResolution, InstanceContextResolutionStatus,
561    InstanceContextValueSlotId, ProviderInstanceId, ProviderInstanceRecord,
562};
563pub use intermediate_representation::{
564    analyze_constant_propagation, analyze_dead_assignments, analyze_definition_uses,
565    analyze_liveness, analyze_reachability, analyze_reactive_cycles,
566    analyze_reactive_transitive_graph, analyze_use_definitions, build_reactive_graph,
567    compute_dominators, compute_post_dominators, computed_optimization_pipeline, inspect_dom_nodes,
568    lower_components_to_ir, optimize_computed_ir, optimize_context_ir, optimize_effect_ir,
569    plan_computed_evaluation, validate_context_ir, validate_effect_ir,
570    validate_intermediate_representation, validate_optimized_context_ir, ContextConsumerLoadId,
571    ContextIrReport, ContextSourceFunctionId, ContextValueSlotId, IntermediateRepresentation,
572    IrBinaryOperation, IrBlock, IrBlockId, IrBranchArm, IrBranchEdge, IrCfgCleanupPass,
573    IrCommonSubexpressionEliminationPass, IrComputedEvaluation, IrComputedEvaluationPlan,
574    IrConstant, IrConstantFoldingPass, IrConstantPropagationAnalysis, IrContextConsumerBinding,
575    IrContextLoad, IrContextSourceEvaluation, IrCopyPropagationPass, IrDeadAssignmentAnalysis,
576    IrDeadCodeEliminationPass, IrDefinitionUseAnalysis, IrDomAttribute, IrDomAttributeValue,
577    IrDomBinding, IrDomConditional, IrDomEvent, IrDomInspection, IrDomList, IrDomNode, IrDomNodeId,
578    IrDomNodeKind, IrDomText, IrDominatorTree, IrEffectCompletion, IrEffectExecution, IrFunction,
579    IrInstruction, IrInstructionId, IrInstructionKind, IrInstructionSimplificationPass,
580    IrLivenessAnalysis, IrLoop, IrLoopId, IrModule, IrOperand, IrOptimizationMetrics,
581    IrOptimizationPass, IrOptimizationPassReport, IrOptimizationPipeline, IrOptimizationReport,
582    IrPassManager, IrPostDominatorTree, IrReachabilityAnalysis, IrReactiveCycle,
583    IrReactiveCycleAnalysis, IrReactiveEdge, IrReactiveEdgeKind, IrReactiveGraph, IrReactiveNode,
584    IrReactiveNodeKind, IrReactiveTransitiveAnalysis, IrSchedulerInspection, IrStorage,
585    IrStorageId, IrTemplateEntrypoint, IrUnaryOperation, IrUpdateScheduler, IrUse, IrUseDefinition,
586    IrValidationDiagnostic, IrValue, IrValueDefinition, IrValueId, OptimizedContextIrReport,
587    OptimizedIrContextSourceEvaluation,
588};
589pub use layout_composition::{
590    build_layout_composition_plan_from_components_v1, build_layout_composition_plan_v1,
591    layout_composition_virtual_invocations_from_provenance_v1,
592    layout_composition_virtual_invocations_v1, LayoutCompositionEdgeV1, LayoutCompositionErrorV1,
593    LayoutCompositionPlanV1, LayoutCompositionRouteV1,
594};
595pub use legacy_decorator_lowering::{
596    legacy_decorator_sites_v1, lower_legacy_decorators_v1, LegacyDecoratorLoweringErrorV1,
597    LegacyDecoratorLoweringV1, LegacyDecoratorResolutionV1, LegacyDecoratorSiteV1,
598};
599pub use metaframework_handoff::{
600    build_deployable_release_manifest_v1, build_static_request_handoff_v1,
601    deployable_release_manifest_json_v1, static_request_handoff_json_v1, DeployableArtifactV1,
602    DeployableReleaseManifestV1, StaticRequestHandoffV1, StaticRequestRouteV1,
603};
604pub use model::{
605    ClassSummary, DecoratorSummary, Diagnostic, RenderMethodSummary, Severity, SourceSummary, Span,
606};
607pub use module_graph::{
608    build_module_graph, ModuleEdge, ModuleEdgeKind, ModuleGraph, ModuleNode, ModuleTarget,
609};
610pub use opaque::{OpaqueActionResolution, OpaqueActionResolutionOutcome, OpaqueTerminalBinding};
611pub use ordinary_html_codegen::{
612    generate_ordinary_instance_html, generate_ordinary_instance_html_for_component,
613    generate_structural_conditional_host_fragments, generate_structural_keyed_host_fragments,
614    generate_structural_template_instance_html,
615};
616pub use ordinary_template_instance::{
617    build_ordinary_template_instance_registry, validate_ordinary_template_instance_registry,
618    OrdinaryTemplateBindingKind, OrdinaryTemplateInstanceBindingRecord,
619    OrdinaryTemplateInstanceEventRecord, OrdinaryTemplateInstanceRegistry,
620    OrdinaryTemplateInstanceTargetRecord, OrdinaryTemplateTargetKind,
621    ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
622};
623pub use ordinary_template_integrity::{
624    ComputedInstanceSlotIntegrityCode, OrdinaryTemplateIntegrityCode,
625    StateInstanceStorageIntegrityCode,
626};
627pub use page_codegen::{
628    embed_opaque_runtime_artifact, generate_standalone_page,
629    generate_standalone_page_with_component_runtime,
630    generate_standalone_page_with_component_runtime_and_forms,
631    generate_standalone_page_with_computed_runtime, generate_standalone_page_with_context_runtime,
632    generate_standalone_page_with_effect_runtime, generate_standalone_page_with_resume_runtime,
633    generate_standalone_page_with_resume_runtime_and_resources,
634};
635pub use production_audit::{
636    build_production_audit_report_v1, production_audit_report_json_v1, ProductionAuditErrorV1,
637    ProductionAuditReportV1, ProductionRuntimeAuthority, PRODUCTION_AUDIT_REPORT_SCHEMA_VERSION,
638    PRODUCTION_RUNTIME_AUTHORITIES, PRODUCTION_RUNTIME_REFINEMENT_INVARIANTS,
639};
640pub use production_bootstrap::{
641    build_production_bootstrap_plan, ProductionBootstrapBlock, ProductionBootstrapPlan,
642    ProductionRuntimeIndexes,
643};
644pub use production_chunk_graph::{
645    extract_production_chunk_graph, validate_production_chunk_graph, ProductionActivationChunkPlan,
646    ProductionChunkDependency, ProductionChunkExtractionError, ProductionChunkExtractionReport,
647    ProductionChunkGraph, ProductionChunkGraphValidationError, ProductionChunkKind,
648    ProductionChunkRecord, ProductionRootChunkInput, ProductionSharedChunkFailurePolicy,
649};
650pub use production_cleanup::{
651    build_production_destroy_plan, validate_production_cleanup_closure,
652    ProductionCleanupClosureViolation, ProductionCleanupKind, ProductionDestroyPlan,
653    ProductionOwnedRuntimeRecord,
654};
655pub use production_constant_pool::{
656    pool_production_constants, ConstantPoolConsumer, ConstantPoolingDecision,
657    ConstantPoolingReport, ProductionConstantCandidate, ProductionConstantPool,
658    ProductionConstantPoolEntry,
659};
660pub use production_deduplication::{
661    deduplicate_generated_programs, DeduplicatedProgramRegistry, ExecutableProgramCandidate,
662    ExecutableProgramCanonicalStream, ExecutableProgramFingerprintRegistry, ProgramAliasRecord,
663    ProgramDeduplicationReport,
664};
665pub use production_diagnostics::{
666    project_production_diagnostics, ProductionDiagnosticContract, ProductionDiagnosticFact,
667    ProductionDiagnosticKind, ProductionDiagnosticProvenance, ProductionProjectedDiagnostic,
668    PRODUCTION_DIAGNOSTIC_CATALOG,
669};
670pub use production_elimination::{
671    eliminate_unreachable_production_records, DeadProductEliminationReason,
672    DeadProductEliminationReport, OptimizationDecision, ProductionArtifactProjection,
673    ProductionExecutionRecord,
674};
675pub use production_module_emitter::{
676    emit_production_modules, validate_production_module, ProductionModuleLayout,
677    ProductionModuleRecord, ProductionModuleValidationError,
678};
679pub use production_optimization::{
680    BenchmarkFixtureId, ConstantPoolEntryId, ExecutableProgramFingerprint, OptimizationDecisionId,
681    OptimizationPolicyId, OptimizationReportId, PerformanceBudgetId, ProductionArtifactId,
682    ProductionChunkId, ProductionOptimizationDiagnosticReservation,
683    ProductionOptimizationIdentityParseError, ProductionOptimizationPolicy,
684    ProductionOptimizationPolicyV1, RuntimeTableId, SharedChunkCandidateId,
685    PRODUCTION_OPTIMIZATION_DIAGNOSTIC_RESERVATIONS,
686    PRODUCTION_OPTIMIZATION_INTEGRITY_RESERVATION_END,
687    PRODUCTION_OPTIMIZATION_INTEGRITY_RESERVATION_START,
688};
689pub use production_reachability::{
690    build_production_reachability_graph, ProductionExecutableRoot, ProductionProgramEdge,
691    ProductionReachabilityBlock, ProductionReachabilityGraph, ProductionReachabilityReason,
692    ProductionUnreachableRecord,
693};
694pub use production_reports::{
695    build_production_reports, optimization_report_json, runtime_cost_report_json,
696    OptimizationInspectionQuery, OptimizationReportV1, ProductionReportInputs,
697    RuntimeCostInspectionQuery, RuntimeCostReportV1, OPTIMIZATION_REPORT_SCHEMA_VERSION,
698    RUNTIME_COST_REPORT_SCHEMA_VERSION,
699};
700pub use production_runtime_artifact::{
701    build_production_runtime_artifact, build_production_runtime_table,
702    parse_production_runtime_artifact_v1, production_runtime_artifact_json,
703    validate_production_runtime_artifact, ProductionArtifactActivationEntry,
704    ProductionArtifactChunkRecord, ProductionArtifactEntry, ProductionArtifactIntegrity,
705    ProductionArtifactIntegrityViolation, ProductionOrdinalMapping, ProductionOrdinalWidth,
706    ProductionRuntimeArtifactV1, ProductionRuntimeTable, ProductionRuntimeTableRegistry,
707    PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION, PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION,
708};
709pub use production_scheduler::{
710    build_production_patch_schedule, coalesce_production_binding_writes,
711    BindingWriteCoalescingDecision, BindingWriteCoalescingReport, ProductionPatchBatchKind,
712    ProductionPatchOperation, ProductionPatchSchedule,
713};
714pub use production_validation::{
715    validate_production_runtime_pipeline, ProductionRuntimeFailure, ProductionValidationEvidence,
716    ProductionValidationPhase, ProductionValidationResult,
717};
718pub use project_discovery::{
719    discover_project_v1, discover_semantic_packages_v1, DiscoveredProjectSourceV1,
720    DiscoveredProjectV1, ProjectDiscoveryErrorV1,
721};
722pub use provider::{collect_provider_entities, DuplicateProviderDeclaration, ProviderEntity};
723pub use purity_effect::{
724    build_purity_effect_graph_v1, FunctionEffectFactV1, FunctionEffectKindV1,
725    FunctionPurityEffectV1, FunctionPurityV1, PurityEffectErrorV1, PurityEffectGraphV1,
726    PURITY_EFFECT_SCHEMA_VERSION,
727};
728pub use resource::{
729    ResourceActivation, ResourceDeclaration, ResourceDeclarationError, ResourceEndpointBinding,
730    ResourceEndpointResolution, ResourceEndpointResolutionOutcome, ResourceInvalidationPolicy,
731    ResourceLifecycleError, ResourceLifecycleEvent, ResourceLifecycleState, ResourceRetryPolicy,
732};
733pub use resume_activation::{
734    build_resume_activation_plan, validate_resume_activation_plan, ResumeActivationBlock,
735    ResumeActivationBlockReason, ResumeActivationIntegrityCode,
736    ResumeActivationIntegrityDiagnostic, ResumeActivationPlan, ResumeActivationPolicy,
737    ResumeActivationPolicyDecision, ResumeActivationPrerequisite, RESUME_ACTIVATION_PLAN_VERSION,
738};
739pub use resume_anchor::{
740    build_resume_anchor_plan, validate_resume_anchor_plan, validate_resume_marker_html,
741    ResumeAnchorIntegrityCode, ResumeAnchorIntegrityDiagnostic, ResumeAnchorKind,
742    ResumeAnchorPlacement, ResumeAnchorPlan, ResumeAnchorPlanRecord, ResumeEventMarkerRecord,
743    RESUME_ANCHOR_PLAN_VERSION,
744};
745pub use resume_boundary::{
746    build_resume_boundary_graph, validate_resume_boundary_graph, ResumeBoundary,
747    ResumeBoundaryActivationIdentity, ResumeBoundaryActivationProgram,
748    ResumeBoundaryActivationReference, ResumeBoundaryBlock, ResumeBoundaryBlockSource,
749    ResumeBoundaryGraph, ResumeBoundaryIntegrityCode, ResumeBoundaryIntegrityDiagnostic,
750    ResumeBoundaryOwner, ResumeBoundaryOwnershipEdge, RESUME_BOUNDARY_GRAPH_VERSION,
751};
752pub use resume_capture::{
753    build_resume_capture_plan, capture_resume_snapshot, encode_resume_value,
754    resume_snapshot_artifact_json, resume_snapshot_json, validate_resume_capture_plan,
755    ResumeCaptureBlock, ResumeCaptureBlockReason, ResumeCaptureError, ResumeCaptureErrorKind,
756    ResumeCaptureInstruction, ResumeCaptureIntegrityCode, ResumeCaptureIntegrityDiagnostic,
757    ResumeCapturePlan, ResumeCaptureProgram, ResumeEncodedValue, ResumeEnvelopeWriterPlan,
758    ResumeSnapshotBoundaryV1, ResumeSnapshotStructuralOccurrenceV2,
759    ResumeSnapshotStructuralStateV2, ResumeSnapshotV1, ResumeSnapshotV2,
760    ResumeSnapshotValueRecordV1, RuntimeQuiescenceState, RESUME_CAPTURE_MANIFEST_VERSION,
761    RESUME_CAPTURE_PLAN_VERSION, RESUME_SNAPSHOT_SCHEMA_VERSION,
762};
763pub use resume_chunk::{
764    build_resume_chunk_graph, validate_resume_chunk_graph, ResumeChunk, ResumeChunkBlock,
765    ResumeChunkBlockReason, ResumeChunkGraph, ResumeChunkIntegrityCode,
766    ResumeChunkIntegrityDiagnostic, ResumeChunkModulePlan, ResumeChunkProgram,
767    ResumeChunkProgramInclusion, ResumeChunkRootKind, RESUME_CHUNK_GRAPH_VERSION,
768};
769pub use resume_diagnostics::{
770    build_resume_diagnostic_products, project_resume_diagnostics,
771    project_resume_diagnostics_from_products, ResumeDiagnosticProducts,
772    ResumeDiagnosticReservation, ResumeProjectedDiagnostic, RESUME_DIAGNOSTIC_RESERVATIONS,
773    RESUME_INTEGRITY_RESERVATION_END, RESUME_INTEGRITY_RESERVATION_START,
774};
775pub use resume_identity::{
776    ComputedInstanceCacheSlotId, ComputedInstanceDirtySlotId, ResumeActivationId,
777    ResumeActivationRootKind, ResumeAnchorId, ResumeBoundaryId, ResumeBoundaryKind, ResumeBuildId,
778    ResumeCaptureProgramId, ResumeChunkGroupId, ResumeChunkId, ResumeEventId,
779    ResumeIdentityParseError, ResumeRestoreProgramId, ResumeSchemaId, ResumeSlotId,
780    ResumeSnapshotId, ResumeValueRecordId, StateInstanceSlotId, TemplateInstanceBindingId,
781    TemplateInstanceTargetId,
782};
783pub use resume_liveness::{
784    build_resume_liveness_plan, validate_resume_liveness_plan, ResumeExcludedSlot,
785    ResumeExistingSlot, ResumeLivenessBlock, ResumeLivenessBlockReason,
786    ResumeLivenessClassificationKind, ResumeLivenessClassificationRef, ResumeLivenessIntegrityCode,
787    ResumeLivenessIntegrityDiagnostic, ResumeLivenessOwner, ResumeLivenessPlan, ResumeLivenessSlot,
788    ResumeRecomputableSlot, ResumeRecomputationProof, ResumeRetainedSlot, ResumeRetentionReason,
789    RESUME_LIVENESS_PLAN_VERSION,
790};
791pub use resume_manifest::{
792    build_resume_manifest, compute_resume_build_id, parse_resume_manifest_v6,
793    parse_resume_manifest_v7, resume_manifest_json, validate_resume_manifest, ResumeManifest,
794    ResumeManifestActivationPolicy, ResumeManifestActivationRecord, ResumeManifestAnchorRecord,
795    ResumeManifestBoundaryKind, ResumeManifestBoundaryRecord, ResumeManifestCaptureInstruction,
796    ResumeManifestCaptureProgram, ResumeManifestChunkRecord, ResumeManifestContextSlotRecord,
797    ResumeManifestEffectRecord, ResumeManifestEventRecord,
798    ResumeManifestPhaseIComponentResumeRecord, ResumeManifestRestoreInstruction,
799    ResumeManifestRestoreInstructionRecord, ResumeManifestRestoreProgram,
800    ResumeManifestSlotSchemaRecord, ResumeManifestValidationDiagnostic,
801    RESUME_MANIFEST_SCHEMA_VERSION, RESUME_RUNTIME_PROTOCOL_VERSION,
802};
803pub use resume_plan::{
804    build_resume_plan, ComponentInstanceResumePlan, FormFieldResumePlan, FormInstanceResumePlan,
805    ResumeComponentPlan, ResumeComputedPlan, ResumePlan, SlotBindingResumePlan,
806    StructuralRegionResumePlan,
807};
808pub use resume_restore::{
809    build_resume_restore_plan, validate_resume_restore_plan, ResumeRestoreApplicationSchedule,
810    ResumeRestoreBlock, ResumeRestoreBlockReason, ResumeRestoreInstruction,
811    ResumeRestoreInstructionRecord, ResumeRestoreIntegrityCode, ResumeRestoreIntegrityDiagnostic,
812    ResumeRestorePhase, ResumeRestorePlan, ResumeRestoreProgram, ResumeRestoreSchedulePhase,
813    ResumeRestoreSlotAssignment, RESUME_RESTORE_PLAN_VERSION,
814};
815pub use resume_schema::{
816    build_resume_schema_registry, resume_value_codec, validate_resume_schema_registry,
817    ResumeBoundarySchema, ResumeObjectPropertyCodec, ResumeSchemaBlock, ResumeSchemaBlockReason,
818    ResumeSchemaIntegrityCode, ResumeSchemaIntegrityDiagnostic, ResumeSchemaRegistry,
819    ResumeSlotSchema, ResumeValueCodec, RESUME_SCHEMA_REGISTRY_VERSION,
820};
821pub use route_graph::{
822    build_file_route_graph_from_components_v1, build_file_route_graph_v1, build_route_graph,
823    build_static_route_publication_v1, build_validated_file_route_graph_from_components_v1,
824    build_validated_file_route_graph_v1, build_validated_route_graph_v1, route_manifest_json_v1,
825    route_manifest_v1, FileRouteGraphV1, FileRouteNodeV1, RouteGraph, RouteGraphError,
826    RouteManifestEntryV1, RouteManifestV1, RouteNode,
827};
828pub use route_loader::{
829    build_route_loader_plan_v1, route_loader_plan_json_v1, RouteLoaderBindingV1,
830    RouteLoaderPlanErrorV1, RouteLoaderPlanV1, RouteLoaderRouteV1,
831    ROUTE_LOADER_PLAN_SCHEMA_VERSION,
832};
833pub use route_metadata::{
834    build_route_metadata_manifest_v1, route_metadata_manifest_json_v1, RouteMetadataErrorV1,
835    RouteMetadataInputV1, RouteMetadataManifestV1, RouteMetadataRecordV1,
836    ROUTE_METADATA_SCHEMA_VERSION,
837};
838pub use route_server_action::{
839    build_route_server_action_plan_v1, route_server_action_plan_json_v1,
840    RouteServerActionBindingV1, RouteServerActionPlanErrorV1, RouteServerActionPlanV1,
841    RouteServerActionRouteV1, ROUTE_SERVER_ACTION_PLAN_SCHEMA_VERSION,
842};
843pub use runtime_codegen::generate_runtime_stub;
844pub use runtime_component::{
845    build_runtime_component_registry, RuntimeComponentContextBindingRecord,
846    RuntimeComponentDefinitionRecord, RuntimeComponentInitializationBatch,
847    RuntimeComponentInstanceRecord, RuntimeComponentRegistry, RuntimeComponentSlotBindingRecord,
848    RUNTIME_COMPONENT_REGISTRY_SCHEMA_CONTRACT_VERSION,
849};
850pub use runtime_component_artifact::{
851    build_runtime_component_artifact, runtime_component_artifact_json,
852    validate_runtime_component_artifact, RuntimeComponentArtifact,
853    RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION,
854};
855pub use runtime_computed::{
856    build_runtime_computed_registry, ComputedCacheSlotId, ComputedDirtyFlagId,
857    RuntimeComputedCacheSlot, RuntimeComputedDirtyFlag, RuntimeComputedRecord,
858    RuntimeComputedRegistry,
859};
860pub use runtime_computed_artifact::{
861    build_runtime_computed_artifact, runtime_computed_artifact_json, RuntimeComputedArtifact,
862    RuntimeComputedArtifactDirtyFlag, RuntimeComputedArtifactEvaluation,
863    RuntimeComputedArtifactInstruction, RuntimeComputedArtifactInvalidation,
864    RuntimeComputedArtifactResourceInvalidation, RuntimeComputedArtifactSerialization,
865    RUNTIME_COMPUTED_ARTIFACT_SCHEMA_VERSION,
866};
867pub use runtime_context::{
868    build_runtime_context_registry, validate_runtime_context_registry,
869    RuntimeContextConsumerRecord, RuntimeContextEvaluationBatch, RuntimeContextRegistry,
870    RuntimeContextRegistryValidationDiagnostic, RuntimeContextSourceKind,
871    RuntimeContextSourceRecord, RUNTIME_CONTEXT_REGISTRY_SCHEMA_CONTRACT_VERSION,
872};
873pub use runtime_context_artifact::{
874    build_runtime_context_artifact, runtime_context_artifact_json, RuntimeContextArtifact,
875    SerializedContextActionUpdatePlan, SerializedContextBatchId, SerializedContextConsumerBinding,
876    SerializedContextEvaluationBatch, SerializedContextExecutionBoundary,
877    SerializedContextInstruction, SerializedContextInstructionKind, SerializedContextProgram,
878    SerializedContextSource, SerializedContextSourceKind, RUNTIME_CONTEXT_ARTIFACT_SCHEMA_VERSION,
879};
880pub use runtime_effect::{
881    build_runtime_effect_registry, RuntimeActionBatchEffectTrigger, RuntimeEffectRecord,
882    RuntimeEffectRegistry, RuntimeInitialEffectTrigger,
883};
884pub use runtime_effect_artifact::{
885    build_runtime_effect_artifact, runtime_effect_artifact_json, RuntimeEffectArtifact,
886    RuntimeEffectArtifactActionTrigger, RuntimeEffectArtifactCapabilityInstructionKind,
887    RuntimeEffectArtifactCapabilityOperation, RuntimeEffectArtifactEffect,
888    RuntimeEffectArtifactExecutionBoundary, RuntimeEffectArtifactExecutionPolicy,
889    RuntimeEffectArtifactInitialTrigger, RuntimeEffectArtifactInstance,
890    RuntimeEffectArtifactInstruction, RuntimeEffectArtifactPrerequisiteBatch,
891    RuntimeEffectArtifactProgram, RuntimeEffectArtifactRenderBoundary,
892    RuntimeEffectArtifactStructuralTemplate, RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
893};
894pub use runtime_effect_instance::{
895    build_runtime_effect_instance_registry, build_runtime_effect_structural_template_registry,
896    RuntimeEffectInstanceRecord, RuntimeEffectInstanceRegistry,
897    RuntimeEffectStructuralTemplateRecord, RUNTIME_EFFECT_INSTANCE_REGISTRY_VERSION,
898};
899pub use runtime_form_artifact::{
900    build_runtime_forms_artifact, runtime_forms_artifact_json, validate_runtime_forms_artifact,
901    RuntimeFormsArtifact, RuntimeFormsArtifactBinding, RuntimeFormsArtifactDependency,
902    RuntimeFormsArtifactField, RuntimeFormsArtifactFieldProgram, RuntimeFormsArtifactFieldSlots,
903    RuntimeFormsArtifactForm, RuntimeFormsArtifactInstance, RuntimeFormsArtifactPrograms,
904    RuntimeFormsArtifactReset, RuntimeFormsArtifactRule, RuntimeFormsArtifactSerialization,
905    RuntimeFormsArtifactSubmission, RuntimeFormsArtifactValidation,
906    RUNTIME_FORM_ARTIFACT_SCHEMA_VERSION,
907};
908pub use runtime_form_registry::{
909    build_runtime_form_registry, RuntimeFormInstanceRecord, RuntimeFormRecord, RuntimeFormRegistry,
910    RUNTIME_FORM_REGISTRY_VERSION,
911};
912pub use runtime_opaque_artifact::{
913    build_runtime_opaque_artifact, build_runtime_opaque_artifact_with_modules,
914    runtime_opaque_artifact_json, validate_runtime_opaque_artifact, RuntimeOpaqueArtifact,
915    RuntimeOpaqueArtifactActivation, RuntimeOpaqueArtifactBuildError,
916    RuntimeOpaqueArtifactValidationError, RUNTIME_OPAQUE_ARTIFACT_SCHEMA_VERSION,
917};
918pub use runtime_resource_artifact::{
919    build_runtime_resource_artifact, build_runtime_resource_artifact_with_modules,
920    runtime_resource_artifact_json, validate_runtime_resource_artifact, RuntimeResourceArtifact,
921    RuntimeResourceArtifactActivation, RuntimeResourceArtifactBuildError,
922    RuntimeResourceArtifactDeclaration, RuntimeResourceArtifactEndpoint,
923    RuntimeResourceArtifactValidationError, RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION,
924};
925pub use semantic_capability::{
926    build_semantic_capability_registry, semantic_capability_matrix_text,
927    semantic_capability_migration_text, semantic_capability_registry_json, SemanticCapability,
928    SemanticCapabilityClass, SemanticCapabilityRegistry, SemanticCapabilityStatus,
929    SEMANTIC_CAPABILITY_REGISTRY_SCHEMA_VERSION,
930};
931pub use semantic_graph::{
932    build_semantic_graph, semantic_graph_json, SemanticGraph, SemanticGraphConsumer,
933    SemanticGraphContext, SemanticGraphEdge, SemanticGraphEdgeKind, SemanticGraphNode,
934    SemanticGraphNodeKind, SemanticGraphProvenance, SemanticGraphProvider,
935    SEMANTIC_GRAPH_SCHEMA_VERSION,
936};
937pub use semantic_id::{
938    ComponentInstanceId, ComponentInvocationId, ComponentRootId, ComponentStructuralRegionId,
939    ConsumerId, ContextDeclarationCandidateId, ContextId, DirtyTrackingPlanId, EffectId,
940    EffectInstanceId, EffectStatementId, FieldBindingId, FieldDependencyId, FieldId,
941    FieldResetOperationId, FieldTrackingId, FormDeclarationCandidateId,
942    FormFieldBindingCandidateId, FormFieldDeclarationCandidateId, FormFieldDirtySlotId,
943    FormFieldTouchedSlotId, FormFieldValidationSlotId, FormFieldValueSlotId, FormId,
944    FormInstanceId, FormOwnershipGraphId, FormSubmissionStateSlotId, FormValidationAggregateSlotId,
945    ProviderId, ResetPlanId, ResourceActivationId, ResourceId, SemanticId, SemanticOwner,
946    SerializationPlanId, SlotBindingId, SlotContentFragmentId, SlotDeclarationCandidateId, SlotId,
947    SlotOutletId, SubmissionDeclarationCandidateId, SubmissionHostCandidateId, SubmissionHostId,
948    SubmissionPlanId, TemplatePositionId, TouchedTrackingPlanId, ValidationDependencyCycleId,
949    ValidationGraphId, ValidationPlanId, ValidationRuleCandidateId, ValidationRuleId,
950};
951pub use semantic_package::{
952    parse_semantic_package_contract, SemanticPackageContract, SemanticPackageContractError,
953    SemanticPackageExport, SemanticPackageKind, SemanticPackageOpaqueExecutionBoundary,
954    SemanticPackageOpaqueResumePolicy, SemanticPackageOpaqueTerminal, SemanticPackagePureOperation,
955    SemanticPackageResolutionTable, SemanticPackageResourceCancellation,
956    SemanticPackageResourceEndpoint, SemanticPackageResourceExecutionBoundary,
957    SemanticPackageResourceResumePolicy, SemanticPackageRouteLoader,
958    SemanticPackageRouteLoaderFailure, SemanticPackageRouteLoaderInput,
959    SemanticPackageServerAction, SemanticPackageServerActionInput,
960    SemanticPackageServerActionResponse, SemanticPackageServerCachePolicy,
961    SemanticPackageServerCacheScope, SEMANTIC_PACKAGE_CONTRACT_SCHEMA_VERSION,
962};
963pub use semantic_package_runtime::{
964    SemanticPackageRuntimeModuleError, SemanticPackageRuntimeModuleKey,
965    SemanticPackageRuntimeModuleTable,
966};
967pub use semantic_provenance::SourceProvenance;
968pub use semantic_reference::{SemanticReference, SemanticReferenceKind};
969pub use semantic_type::{
970    boundary_compatibility, dom_binding_contract, infer_serializable_value_type, is_assignable,
971    is_state_initializer_assignable, operator_result_type, semantic_type_text,
972    serialization_compatibility, state_initializer_value_type, BoundaryCompatibility,
973    BuiltinTypeAuthority, ComputedValueType, DomBindingContract, DomBindingKind,
974    EffectCompatibility, EffectOperationClassification, EffectStatementTypeRecord,
975    ExecutionBoundary, ObjectType, ResolvedDeclaredSemanticType, ResourceExecutionBoundary,
976    ResourceType, SemanticOperator, SemanticType, SemanticTypeAlias, SemanticTypeAssignment,
977    SemanticTypeId, SemanticTypeModel, SemanticTypeStatus, SerializationCompatibility,
978    TypeDiagnosticCode, TypeDiagnosticFamily,
979};
980pub use shared_chunk_candidate::{
981    plan_shared_lazy_chunk_candidates, SharedChunkCandidate, SharedChunkCandidatePlan,
982    SharedChunkConsumerRoot, SharedChunkProgramOccurrence, SharedChunkRejectionReason,
983    SharedChunkSavingsCalculation,
984};
985pub use slot::{collect_slot_entities, SlotEntity};
986pub use slot_binding::{
987    collect_slot_bindings, collect_slot_bindings_with_virtual_invocations, SlotBinding,
988    SlotBindingRegistry, SlotBindingStatus,
989};
990pub use slot_content::{
991    collect_slot_composition, SlotCompositionRegistry, SlotContentFragment,
992    SlotContentFragmentStatus, SlotContentFragmentViolation, SlotOutlet, SlotOutletStatus,
993    SlotOutletViolation,
994};
995pub use slot_field_lowering::{
996    lower_slot_fields_v1, slot_field_sites_v1, ResolvedSlotFieldV1, SlotFieldLoweringErrorV1,
997    SlotFieldLoweringV1, SlotFieldSiteV1,
998};
999pub use slot_projection::{
1000    build_slot_projection_graph_v1, SlotProjectionGraphV1, SlotProjectionRecordV1,
1001    SlotProjectionStatusV1, SlotResumabilityCoverageV1, SLOT_PROJECTION_SCHEMA_VERSION,
1002};
1003pub use state_initializer_lowering::{
1004    lower_state_initializers_v1, state_initializer_sites_v1, ResolvedStateInitializerV1,
1005    StateInitializerLoweringErrorV1, StateInitializerLoweringV1, StateInitializerSiteV1,
1006};
1007pub use state_instance_storage::{
1008    build_state_instance_storage_registry, validate_state_instance_storage_registry,
1009    StateInstanceStorageRecord, StateInstanceStorageRegistry,
1010    STATE_INSTANCE_STORAGE_REGISTRY_VERSION,
1011};
1012pub use state_projection::{
1013    build_state_projection_v1, StateProjectionRecordV1, StateProjectionV1, StateResumeAdmissionV1,
1014    StateUpdateCoverageV1, STATE_PROJECTION_SCHEMA_VERSION,
1015};
1016pub use structural_component::{
1017    build_structural_component_graph_v1, ComponentInheritanceStatusV1, ComponentPropsResolutionV1,
1018    StructuralComponentDiagnosticReasonV1, StructuralComponentDiagnosticV1,
1019    StructuralComponentFactV1, StructuralComponentGraphV1, StructuralComponentPropV1,
1020    StructuralComponentRecordV1, STRUCTURAL_COMPONENT_SCHEMA_VERSION,
1021};
1022pub use structural_occurrence_identity::{
1023    decode_structural_occurrence_identity, encode_structural_occurrence_identity,
1024    StructuralOccurrenceIdentity,
1025};
1026pub use summarize::summarize_source;
1027pub use symbol_table::{
1028    build_symbol_table, ModuleSymbol, ModuleSymbolTable, SymbolDiagnostic, SymbolKind, SymbolTable,
1029};
1030pub use template_graph::{
1031    build_template_graph, AttributeValue, ConditionalNode, ElementNode, FragmentNode, ListNode,
1032    TemplateAttribute, TemplateChild, TemplateGraph, TemplateNode, TemplateNodeId,
1033};
1034pub use template_manifest::{
1035    build_template_manifest, build_template_manifest_from_asm, template_manifest_json,
1036    validate_template_manifest, ManifestAction, ManifestBindingTarget, ManifestComponent,
1037    ManifestEvent, ManifestEventKind, ManifestFormBinding, ManifestFormHost, ManifestNode,
1038    ManifestOperation, ManifestOrdinaryBinding, ManifestOrdinaryEvent, ManifestOrdinaryTarget,
1039    ManifestTemplate, TemplateManifest, TEMPLATE_MANIFEST_SCHEMA_VERSION,
1040};
1041pub use template_semantics::{
1042    build_template_semantic_entities, TemplateSemanticEntity, TemplateSemanticKind,
1043    TemplateSemanticScope,
1044};
1045pub use v2_authoring_lowering::{
1046    lower_v2_authoring_v1, V2AuthoringLoweringErrorV1, V2AuthoringLoweringV1,
1047    V2AuthoringResolutionsV1,
1048};
1049pub use v2_authority_request::{
1050    build_v2_authority_component_request_v1, build_v2_authority_request_v1,
1051    build_v2_environment_authority_request_v1, V2AuthorityCanonicalV1, V2AuthorityMemberSiteV1,
1052    V2AuthorityPositionV1, V2AuthorityRequestErrorV1, V2AuthorityRequestV1, V2AuthoritySiteV1,
1053    V2_AUTHORITY_REQUEST_SCHEMA_VERSION,
1054};
1055pub use v2_authority_response::{
1056    v2_authoring_resolutions_from_response_v1, v2_environment_public_resolutions_from_response_v1,
1057    validate_v2_authority_response_v1, ResolvedEnvironmentPublicReadV1, V2AuthorityIdentityV1,
1058    V2AuthorityResolutionV1, V2AuthorityResponseErrorV1, V2AuthorityResponseV1,
1059    V2_AUTHORITY_RESPONSE_SCHEMA_VERSION,
1060};
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065    use std::{collections::BTreeMap, path::Path};
1066
1067    #[test]
1068    fn summarizes_component_decorator_class_and_render_method() {
1069        let source = r#"
1070@component("x-counter")
1071class Counter extends Component {
1072  render() {
1073    return <button>Count</button>;
1074  }
1075}
1076"#;
1077
1078        let summary = summarize_source("Counter.tsx", source);
1079
1080        assert_eq!(summary.component_decorators.len(), 1);
1081        assert_eq!(summary.component_classes.len(), 1);
1082        assert_eq!(
1083            summary.component_decorators[0].argument.as_deref(),
1084            Some("x-counter")
1085        );
1086        assert_eq!(summary.class_declarations.len(), 1);
1087        assert_eq!(summary.class_declarations[0].name, "Counter");
1088        assert_eq!(summary.render_methods.len(), 1);
1089        assert!(summary.has_tsx_like_syntax);
1090    }
1091
1092    #[test]
1093    fn recognizes_decorator_free_component_classes_without_ps0100() {
1094        let source = r#"
1095import { Component } from "presolve";
1096
1097export default class Home extends Component {
1098  render() {
1099    return <main>Home</main>;
1100  }
1101}
1102"#;
1103
1104        let summary = summarize_source("app/routes/index.tsx", source);
1105
1106        assert_eq!(summary.component_classes.len(), 1);
1107        assert_eq!(summary.component_classes[0].name, "Home");
1108        assert!(summary.component_decorators.is_empty());
1109        assert!(!summary
1110            .diagnostics
1111            .iter()
1112            .any(|diagnostic| diagnostic.code == "PS0100"));
1113    }
1114
1115    #[test]
1116    fn emits_diagnostics_for_empty_source() {
1117        let summary = summarize_source("Empty.tsx", "");
1118        assert!(summary
1119            .diagnostics
1120            .iter()
1121            .any(|diagnostic| diagnostic.code == "PS0001"));
1122    }
1123
1124    #[test]
1125    fn fixture_0001_source_summary_explain_text_matches_expected() {
1126        let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
1127            .join("../..")
1128            .join("fixtures/0001-source-summary");
1129
1130        let input_path = fixture_root.join("input/Counter.tsx");
1131        let expected_path = fixture_root.join("expected/explain.txt");
1132
1133        let source = std::fs::read_to_string(&input_path).expect("failed to read fixture input");
1134        let expected = std::fs::read_to_string(&expected_path)
1135            .expect("failed to read expected explain output");
1136
1137        let summary = summarize_source("fixtures/0001-source-summary/input/Counter.tsx", &source);
1138
1139        let actual = explain_text(&summary);
1140
1141        assert_eq!(actual, expected);
1142    }
1143
1144    #[test]
1145    fn fixture_0001_source_summary_explain_json_matches_expected() {
1146        let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
1147            .join("../..")
1148            .join("fixtures/0001-source-summary");
1149
1150        let input_path = fixture_root.join("input/Counter.tsx");
1151        let expected_path = fixture_root.join("expected/explain.json");
1152
1153        let source = std::fs::read_to_string(&input_path).expect("failed to read fixture input");
1154        let expected = std::fs::read_to_string(&expected_path)
1155            .expect("failed to read expected JSON explain output");
1156
1157        let summary = summarize_source("fixtures/0001-source-summary/input/Counter.tsx", &source);
1158
1159        let actual = explain_json(&summary);
1160
1161        let actual_json: serde_json::Value =
1162            serde_json::from_str(&actual).expect("actual explain JSON is invalid");
1163        let expected_json: serde_json::Value =
1164            serde_json::from_str(&expected).expect("expected explain JSON fixture is invalid");
1165
1166        assert_eq!(actual_json, expected_json);
1167    }
1168
1169    #[test]
1170    #[allow(clippy::too_many_lines)]
1171    fn builds_component_graph_from_parsed_counter() {
1172        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
1173
1174        let parsed =
1175            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
1176
1177        let graph = build_component_graph(&parsed);
1178
1179        assert!(graph.diagnostics.is_empty());
1180
1181        let component = graph.components.first().expect("expected component");
1182
1183        assert_eq!(component.class_name, "Counter");
1184        assert_eq!(component.id.as_str(), "component:x-counter");
1185        assert_eq!(component.owner, SemanticOwner::Application);
1186        assert_eq!(component.element_name.as_deref(), Some("x-counter"));
1187        assert_eq!(component.route_path.as_deref(), Some("/counter"));
1188
1189        assert_eq!(component.state_fields.len(), 1);
1190        assert_eq!(component.state_fields[0].name, "count");
1191        assert_eq!(
1192            component.state_fields[0].id.as_str(),
1193            "component:x-counter/state:count"
1194        );
1195        assert_eq!(
1196            component.state_fields[0].owner,
1197            SemanticOwner::entity(component.id.clone())
1198        );
1199        assert_eq!(
1200            component.state_fields[0].initial_value,
1201            Some(SerializableValue::Number("0".to_string()))
1202        );
1203
1204        let method_names = component
1205            .methods
1206            .iter()
1207            .map(|method| method.name.as_str())
1208            .collect::<Vec<_>>();
1209
1210        assert_eq!(method_names, vec!["increment", "render"]);
1211        assert_eq!(
1212            component.methods[0].id.as_str(),
1213            "component:x-counter/method:increment"
1214        );
1215        assert_eq!(
1216            component.methods[0].owner,
1217            SemanticOwner::entity(component.id.clone())
1218        );
1219        assert_eq!(
1220            component.actions,
1221            vec![ComponentAction {
1222                id: SemanticId::component(Some("x-counter"), "Counter").action("increment", 0),
1223                owner: SemanticOwner::entity(
1224                    SemanticId::component(Some("x-counter"), "Counter").method("increment"),
1225                ),
1226                method: "increment".to_string(),
1227                operation: StateOperation::AddAssign(SerializableValue::Number("1".to_string())),
1228                field: "count".to_string(),
1229            }]
1230        );
1231
1232        let render = component.render.as_ref().expect("expected render model");
1233
1234        assert_eq!(render.root_element.as_deref(), Some("button"));
1235        assert_eq!(render.attributes.len(), 1);
1236        assert_eq!(render.attributes[0].name, "onClick");
1237        assert!(matches!(
1238            render.attributes[0].value,
1239            RenderAttributeValue::Expression(_)
1240        ));
1241        assert_eq!(render.bindings, vec!["this.count"]);
1242        assert_eq!(render.root_span.expect("expected root span").line, 12);
1243        assert_eq!(render.root_span.expect("expected root span").column, 7);
1244        assert_eq!(render.event_handlers.len(), 1);
1245        assert_eq!(
1246            render.event_handlers[0].id.as_str(),
1247            "component:x-counter/event:click:0"
1248        );
1249        assert_eq!(
1250            render.event_handlers[0].owner,
1251            SemanticOwner::entity(component.id.template())
1252        );
1253        assert_eq!(render.event_handlers[0].event, "click");
1254        assert_eq!(render.event_handlers[0].handler, "this.increment");
1255        assert_eq!(render.event_handlers[0].span.line, 12);
1256        assert_eq!(render.event_handlers[0].span.column, 15);
1257        assert_eq!(render.children.len(), 2);
1258
1259        let RenderChild::Text { value, span } = &render.children[0] else {
1260            panic!("expected text child");
1261        };
1262        assert_eq!(value, "Count:");
1263        assert_eq!(span.line, 13);
1264        assert_eq!(span.column, 9);
1265
1266        let RenderChild::Binding { expression, span } = &render.children[1] else {
1267            panic!("expected binding child");
1268        };
1269        assert_eq!(expression, "this.count");
1270        assert_eq!(span.line, 13);
1271        assert_eq!(span.column, 16);
1272
1273        assert_eq!(graph.references.len(), 2);
1274        assert_eq!(graph.references[0].kind, SemanticReferenceKind::ActionState);
1275        assert_eq!(
1276            graph.references[0].source,
1277            SemanticId::component(Some("x-counter"), "Counter").action("increment", 0)
1278        );
1279        assert_eq!(
1280            graph.references[0].target,
1281            SemanticId::component(Some("x-counter"), "Counter").state_field("count")
1282        );
1283        assert_eq!(
1284            graph.references[0].provenance.path,
1285            Path::new("fixtures/0001-source-summary/input/Counter.tsx")
1286        );
1287        assert_eq!(graph.references[0].provenance.span.line, 7);
1288
1289        assert_eq!(graph.references[1].kind, SemanticReferenceKind::EventMethod);
1290        assert_eq!(
1291            graph.references[1].source,
1292            SemanticId::component(Some("x-counter"), "Counter").event_handler("click", 0)
1293        );
1294        assert_eq!(
1295            graph.references[1].target,
1296            SemanticId::component(Some("x-counter"), "Counter").method("increment")
1297        );
1298        assert_eq!(
1299            graph.references[1].provenance.path,
1300            Path::new("fixtures/0001-source-summary/input/Counter.tsx")
1301        );
1302        assert_eq!(graph.references[1].provenance.span.line, 12);
1303
1304        assert_eq!(graph.provenance[&component.id].span.line, 1);
1305        assert_eq!(graph.provenance[&component.state_fields[0].id].span.line, 4);
1306        assert_eq!(graph.provenance[&component.methods[0].id].span.line, 6);
1307        assert_eq!(graph.provenance[&component.actions[0].id].span.line, 7);
1308        assert_eq!(graph.provenance[&component.id.template()].span.line, 10);
1309        assert_eq!(graph.provenance[&render.event_handlers[0].id].span.line, 12);
1310    }
1311
1312    #[test]
1313    fn lowers_method_parameters_into_canonical_method_metadata() {
1314        let parsed = presolve_parser::parse_file(
1315            "src/Parameters.tsx",
1316            r#"
1317@component("x-parameters")
1318class Parameters extends Component {
1319  save(title: string, retries?: number) {}
1320}
1321"#,
1322        );
1323
1324        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1325        let method = &graph.components[0].methods[0];
1326
1327        assert_eq!(method.name, "save");
1328        assert_eq!(
1329            method
1330                .parameters
1331                .iter()
1332                .map(|parameter| parameter.name.as_str())
1333                .collect::<Vec<_>>(),
1334            vec!["title", "retries"]
1335        );
1336        assert_eq!(method.parameters[0].span.line, 4);
1337        assert_eq!(method.parameters[1].span.line, 4);
1338    }
1339
1340    #[test]
1341    fn assembles_application_semantic_model_from_existing_graphs() {
1342        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
1343        let parsed =
1344            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
1345
1346        let asm = build_application_semantic_model(&parsed);
1347        let component = &asm.components[0];
1348
1349        assert!(asm.diagnostics.is_empty());
1350        assert_eq!(asm.templates.len(), 1);
1351        assert!(asm.template_entities.len() >= 3);
1352        assert_eq!(asm.references.len(), 4);
1353        assert_eq!(asm.ownership.len(), asm.provenance.len());
1354        assert_eq!(asm.ownership[&component.id], SemanticOwner::Application);
1355        assert_eq!(
1356            asm.ownership[&component.state_fields[0].id],
1357            SemanticOwner::entity(component.id.clone())
1358        );
1359        assert_eq!(
1360            asm.ownership[&component.actions[0].id],
1361            SemanticOwner::entity(component.methods[0].id.clone())
1362        );
1363        assert_eq!(
1364            asm.ownership[&asm.templates[0].id],
1365            SemanticOwner::entity(component.id.clone())
1366        );
1367        assert_eq!(asm.provenance[&asm.templates[0].id].span.line, 10);
1368
1369        assert!(matches!(
1370            asm.entity(&component.id),
1371            Some(SemanticEntity::Component(_))
1372        ));
1373        assert!(matches!(
1374            asm.entity(&component.state_fields[0].id),
1375            Some(SemanticEntity::StateField(_))
1376        ));
1377        assert_eq!(asm.component(&component.id), Some(component));
1378        assert_eq!(asm.template(&asm.templates[0].id), Some(&asm.templates[0]));
1379        assert_eq!(
1380            asm.owner(&component.actions[0].id),
1381            Some(&component.actions[0].owner)
1382        );
1383        assert_eq!(asm.provenance(&asm.templates[0].id).unwrap().span.line, 10);
1384        let template_binding = asm
1385            .template_entities
1386            .iter()
1387            .find(|entity| entity.kind == TemplateSemanticKind::Binding)
1388            .expect("template binding entity");
1389        assert!(matches!(
1390            asm.entity(&template_binding.id),
1391            Some(SemanticEntity::TemplateEntity(_))
1392        ));
1393        assert_eq!(
1394            asm.template_entities_for(&asm.templates[0].id).len(),
1395            asm.template_entities.len()
1396        );
1397        assert_eq!(asm.references_from(&component.actions[0].id).len(), 1);
1398        assert_eq!(asm.references_to(&component.state_fields[0].id).len(), 2);
1399        assert_eq!(asm.references_to(&component.methods[0].id).len(), 2);
1400        assert!(validate_application_semantic_model(&asm).is_empty());
1401
1402        let mut invalid = asm.clone();
1403        invalid.provenance.remove(&component.actions[0].id);
1404        let diagnostics = validate_application_semantic_model(&invalid);
1405        let codes = diagnostics
1406            .iter()
1407            .map(|diagnostic| diagnostic.code.as_str())
1408            .collect::<Vec<_>>();
1409        assert!(codes.contains(&"PSASM1002"));
1410        assert!(codes.contains(&"PSASM1006"));
1411
1412        let mut invalid_type = asm.clone();
1413        let state_id = component.state_fields[0].id.clone();
1414        invalid_type
1415            .semantic_types
1416            .assignments
1417            .get_mut(&state_id)
1418            .expect("state type")
1419            .id = SemanticTypeId::for_subject(&component.id);
1420        let diagnostics = validate_application_semantic_model(&invalid_type);
1421        assert!(diagnostics
1422            .iter()
1423            .any(|diagnostic| diagnostic.code == "PSASM1102"));
1424
1425        let dependencies = DependencyAnalysisPass.analyze(&asm);
1426        assert_eq!(dependencies.dependencies[&component.actions[0].id].len(), 1);
1427        assert_eq!(
1428            dependencies.dependents[&component.state_fields[0].id].len(),
1429            2
1430        );
1431    }
1432
1433    #[test]
1434    fn carries_declared_state_types_into_component_and_asm_data() {
1435        let parsed = presolve_parser::parse_file(
1436            "src/Panel.tsx",
1437            r#"
1438@component("x-panel")
1439class Panel extends Component {
1440  count: number = state(0);
1441}
1442"#,
1443        );
1444
1445        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1446        let graph_type = graph.components[0].state_fields[0]
1447            .declared_type
1448            .as_ref()
1449            .expect("declared state type");
1450
1451        assert_eq!(graph_type.text, "number");
1452        assert_eq!(graph_type.provenance.path, Path::new("src/Panel.tsx"));
1453        assert_eq!(graph_type.provenance.span.line, 4);
1454        assert_eq!(graph_type.provenance.span.column, 8);
1455        assert_eq!(graph_type.kind, Some(DeclaredStateTypeKind::Number));
1456
1457        let asm = build_application_semantic_model(&parsed);
1458        assert_eq!(
1459            asm.components[0].state_fields[0].declared_type,
1460            Some(graph_type.clone())
1461        );
1462    }
1463
1464    #[test]
1465    fn lowers_and_evaluates_constant_arithmetic_state_initializers() {
1466        let parsed = presolve_parser::parse_file(
1467            "src/ArithmeticState.tsx",
1468            r#"
1469@component("x-arithmetic-state")
1470class ArithmeticState extends Component {
1471  total: number = state((1 + 2) * 3);
1472  difference: number = state(10 - 3);
1473  quotient: number = state(10 / 2);
1474  remainder: number = state(10 % 3);
1475
1476  render() {
1477    return <output>{this.total}</output>;
1478  }
1479}
1480"#,
1481        );
1482        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1483        let field = &graph.components[0].state_fields[0];
1484
1485        assert_eq!(
1486            field
1487                .initial_expression
1488                .as_ref()
1489                .map(ToString::to_string)
1490                .as_deref(),
1491            Some("((1 + 2) * 3)")
1492        );
1493        assert_eq!(
1494            field.initial_value,
1495            Some(SerializableValue::Number("9".to_string()))
1496        );
1497        assert_eq!(
1498            graph.components[0].state_fields[1].initial_value,
1499            Some(SerializableValue::Number("7".to_string()))
1500        );
1501        assert_eq!(
1502            graph.components[0].state_fields[2].initial_value,
1503            Some(SerializableValue::Number("5".to_string()))
1504        );
1505        assert_eq!(
1506            graph.components[0].state_fields[3].initial_value,
1507            Some(SerializableValue::Number("1".to_string()))
1508        );
1509        assert!(graph.diagnostics.is_empty());
1510    }
1511
1512    #[test]
1513    fn reports_invalid_constant_arithmetic_state_initializers() {
1514        let parsed = presolve_parser::parse_file(
1515            "src/ArithmeticState.tsx",
1516            r#"
1517@component("x-arithmetic-state")
1518class ArithmeticState extends Component {
1519  total: number = state(10 / 0);
1520}
1521"#,
1522        );
1523        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1524        let diagnostic = graph
1525            .diagnostics
1526            .iter()
1527            .find(|diagnostic| diagnostic.code == "PSC1022")
1528            .expect("arithmetic diagnostic");
1529
1530        assert!(diagnostic.message.contains("division or remainder by zero"));
1531        assert_eq!(
1532            diagnostic
1533                .provenance
1534                .as_ref()
1535                .map(|provenance| provenance.span.line),
1536            Some(4)
1537        );
1538    }
1539
1540    #[test]
1541    fn lowers_and_evaluates_constant_comparison_state_initializers() {
1542        let parsed = presolve_parser::parse_file(
1543            "src/ComparisonState.tsx",
1544            r#"
1545@component("x-comparison-state")
1546class ComparisonState extends Component {
1547  equal: boolean = state(3 === 3);
1548  notEqual: boolean = state(3 !== 4);
1549  lessThan: boolean = state(2 < 3);
1550  lessThanOrEqual: boolean = state(3 <= 3);
1551  greaterThan: boolean = state(4 > 3);
1552  ready: boolean = state(((1 + 2) * 3) >= 9);
1553
1554  render() {
1555    return <output>{this.ready}</output>;
1556  }
1557}
1558"#,
1559        );
1560        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1561        let fields = &graph.components[0].state_fields;
1562
1563        assert_eq!(
1564            fields[0]
1565                .initial_expression
1566                .as_ref()
1567                .map(ToString::to_string)
1568                .as_deref(),
1569            Some("(3 === 3)")
1570        );
1571        for field in fields {
1572            assert_eq!(field.initial_value, Some(SerializableValue::Boolean(true)));
1573        }
1574        assert_eq!(
1575            fields[5]
1576                .initial_expression
1577                .as_ref()
1578                .map(ToString::to_string)
1579                .as_deref(),
1580            Some("(((1 + 2) * 3) >= 9)")
1581        );
1582        assert!(graph.diagnostics.is_empty());
1583    }
1584
1585    #[test]
1586    fn reports_invalid_constant_comparison_state_initializers() {
1587        let parsed = presolve_parser::parse_file(
1588            "src/ComparisonState.tsx",
1589            r#"
1590@component("x-comparison-state")
1591class ComparisonState extends Component {
1592  ready: boolean = state((10 / 0) >= 1);
1593}
1594"#,
1595        );
1596        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1597        let diagnostic = graph
1598            .diagnostics
1599            .iter()
1600            .find(|diagnostic| diagnostic.code == "PSC1023")
1601            .expect("comparison diagnostic");
1602
1603        assert!(diagnostic.message.contains("division or remainder by zero"));
1604        assert_eq!(
1605            diagnostic
1606                .provenance
1607                .as_ref()
1608                .map(|provenance| provenance.span.line),
1609            Some(4)
1610        );
1611    }
1612
1613    #[test]
1614    fn lowers_and_evaluates_constant_logical_state_initializers() {
1615        let parsed = presolve_parser::parse_file(
1616            "src/LogicalState.tsx",
1617            r#"
1618@component("x-logical-state")
1619class LogicalState extends Component {
1620  both: boolean = state((1 < 2) && (3 >= 3));
1621  either: boolean = state(false || (10 !== 4));
1622  shortAnd: boolean = state(false && ((10 / 0) > 1));
1623  shortOr: boolean = state(true || ((10 / 0) > 1));
1624
1625  render() {
1626    return <output>{this.both}</output>;
1627  }
1628}
1629"#,
1630        );
1631        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1632        let fields = &graph.components[0].state_fields;
1633
1634        assert_eq!(
1635            fields[0]
1636                .initial_expression
1637                .as_ref()
1638                .map(ToString::to_string)
1639                .as_deref(),
1640            Some("((1 < 2) && (3 >= 3))")
1641        );
1642        assert_eq!(
1643            fields
1644                .iter()
1645                .map(|field| field.initial_value.clone())
1646                .collect::<Vec<_>>(),
1647            vec![
1648                Some(SerializableValue::Boolean(true)),
1649                Some(SerializableValue::Boolean(true)),
1650                Some(SerializableValue::Boolean(false)),
1651                Some(SerializableValue::Boolean(true)),
1652            ]
1653        );
1654        assert!(graph.diagnostics.is_empty());
1655    }
1656
1657    #[test]
1658    fn reports_evaluated_invalid_constant_logical_state_initializers() {
1659        let parsed = presolve_parser::parse_file(
1660            "src/LogicalState.tsx",
1661            r#"
1662@component("x-logical-state")
1663class LogicalState extends Component {
1664  ready: boolean = state(true && ((10 / 0) > 1));
1665}
1666"#,
1667        );
1668        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1669        let diagnostic = graph
1670            .diagnostics
1671            .iter()
1672            .find(|diagnostic| diagnostic.code == "PSC1024")
1673            .expect("logical diagnostic");
1674
1675        assert!(diagnostic.message.contains("division or remainder by zero"));
1676        assert_eq!(
1677            diagnostic
1678                .provenance
1679                .as_ref()
1680                .map(|provenance| provenance.span.line),
1681            Some(4)
1682        );
1683    }
1684
1685    #[test]
1686    fn lowers_and_evaluates_constant_nullish_state_initializers() {
1687        let parsed = presolve_parser::parse_file(
1688            "src/NullishState.tsx",
1689            r#"
1690@component("x-nullish-state")
1691class NullishState extends Component {
1692  label: string = state(null ?? "fallback");
1693  total: number = state(5 ?? (10 / 0));
1694
1695  render() {
1696    return <output>{this.label}</output>;
1697  }
1698}
1699"#,
1700        );
1701        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1702        let fields = &graph.components[0].state_fields;
1703
1704        assert_eq!(
1705            fields[0]
1706                .initial_expression
1707                .as_ref()
1708                .map(ToString::to_string)
1709                .as_deref(),
1710            Some("(null ?? \"fallback\")")
1711        );
1712        assert_eq!(
1713            fields[0].initial_value,
1714            Some(SerializableValue::String("fallback".to_string()))
1715        );
1716        assert_eq!(
1717            fields[1].initial_value,
1718            Some(SerializableValue::Number("5".to_string()))
1719        );
1720        assert!(graph.diagnostics.is_empty());
1721    }
1722
1723    #[test]
1724    fn lowers_and_evaluates_constant_unary_state_initializers() {
1725        let parsed = presolve_parser::parse_file(
1726            "src/UnaryState.tsx",
1727            r#"
1728@component("x-unary-state")
1729class UnaryState extends Component {
1730  negated: boolean = state(!(1 < 2));
1731  signed: number = state(-(1 + 2));
1732  render() { return <output>{this.signed}</output>; }
1733}
1734"#,
1735        );
1736        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1737        assert_eq!(
1738            graph.components[0].state_fields[0].initial_value,
1739            Some(SerializableValue::Boolean(false))
1740        );
1741        assert_eq!(
1742            graph.components[0].state_fields[1].initial_value,
1743            Some(SerializableValue::Number("-3".to_string()))
1744        );
1745    }
1746
1747    #[test]
1748    fn reports_reached_invalid_constant_nullish_state_initializers() {
1749        let parsed = presolve_parser::parse_file(
1750            "src/NullishState.tsx",
1751            r#"
1752@component("x-nullish-state")
1753class NullishState extends Component {
1754  total: number = state(null ?? (10 / 0));
1755}
1756"#,
1757        );
1758        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1759        let diagnostic = graph
1760            .diagnostics
1761            .iter()
1762            .find(|diagnostic| diagnostic.code == "PSC1025")
1763            .expect("nullish diagnostic");
1764        assert!(diagnostic.message.contains("division or remainder by zero"));
1765    }
1766
1767    #[test]
1768    fn classifies_exact_primitive_declared_state_types() {
1769        let source =
1770            include_str!("../../../fixtures/0025-typed-state-annotations/input/TypedState.tsx");
1771        let parsed = presolve_parser::parse_file(
1772            "fixtures/0025-typed-state-annotations/input/TypedState.tsx",
1773            source,
1774        );
1775        let graph = build_component_graph_for_module(&parsed);
1776        let kinds = graph.components[0]
1777            .state_fields
1778            .iter()
1779            .map(|field| {
1780                (
1781                    field.name.as_str(),
1782                    field
1783                        .declared_type
1784                        .as_ref()
1785                        .and_then(|declared_type| declared_type.kind),
1786                )
1787            })
1788            .collect::<Vec<_>>();
1789
1790        assert_eq!(
1791            kinds,
1792            vec![
1793                ("count", Some(DeclaredStateTypeKind::Number)),
1794                ("status", None),
1795                ("title", Some(DeclaredStateTypeKind::String)),
1796                ("enabled", Some(DeclaredStateTypeKind::Boolean)),
1797                ("empty", Some(DeclaredStateTypeKind::Null)),
1798            ]
1799        );
1800    }
1801
1802    #[test]
1803    fn reports_primitive_declared_state_initializer_mismatches() {
1804        let source = include_str!(
1805            "../../../fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx"
1806        );
1807        let parsed = presolve_parser::parse_file(
1808            "fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx",
1809            source,
1810        );
1811        let graph = build_component_graph_for_module(&parsed);
1812        let folded = ConstantFoldingPass.transform(
1813            &build_application_semantic_model_from_component_graph(&graph),
1814        );
1815        let diagnostics = folded
1816            .diagnostics
1817            .iter()
1818            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1819            .collect::<Vec<_>>();
1820
1821        assert_eq!(diagnostics.len(), 6);
1822        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1016"));
1823
1824        let provenance = folded.diagnostics[0]
1825            .provenance
1826            .as_ref()
1827            .expect("mismatch diagnostic provenance");
1828        assert_eq!(
1829            provenance.path,
1830            Path::new("fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx")
1831        );
1832        assert_eq!(provenance.span.line, 3);
1833        assert_eq!(provenance.span.column, 8);
1834    }
1835
1836    #[test]
1837    fn reports_primitive_declared_state_action_assignment_mismatches() {
1838        let source = include_str!(
1839            "../../../fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx"
1840        );
1841        let parsed = presolve_parser::parse_file(
1842            "fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx",
1843            source,
1844        );
1845        let graph = build_component_graph_for_module(&parsed);
1846        let folded = ConstantFoldingPass.transform(
1847            &build_application_semantic_model_from_component_graph(&graph),
1848        );
1849        let diagnostics = folded
1850            .diagnostics
1851            .iter()
1852            .filter(|diagnostic| diagnostic.code == "PSC1017")
1853            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1854            .collect::<Vec<_>>();
1855
1856        assert_eq!(diagnostics.len(), 6);
1857        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1017"));
1858        assert!(diagnostics.iter().any(|(_, message)| {
1859            message.contains("state field `status`") && message.contains("assigns `number`")
1860        }));
1861        assert!(diagnostics.iter().any(|(_, message)| {
1862            message.contains("state field `collection`") && message.contains("assigns `tuple`")
1863        }));
1864
1865        let provenance = folded
1866            .diagnostics
1867            .iter()
1868            .find(|diagnostic| diagnostic.code == "PSC1017")
1869            .expect("action mismatch diagnostic")
1870            .provenance
1871            .as_ref()
1872            .expect("action mismatch diagnostic provenance");
1873        assert_eq!(
1874            provenance.path,
1875            Path::new(
1876                "fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx"
1877            )
1878        );
1879        assert_eq!(provenance.span.line, 11);
1880        assert_eq!(provenance.span.column, 5);
1881    }
1882
1883    #[test]
1884    fn reports_non_boolean_primitive_toggle_actions() {
1885        let source = include_str!(
1886            "../../../fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx"
1887        );
1888        let parsed = presolve_parser::parse_file(
1889            "fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx",
1890            source,
1891        );
1892        let graph = build_component_graph_for_module(&parsed);
1893        let folded = ConstantFoldingPass.transform(
1894            &build_application_semantic_model_from_component_graph(&graph),
1895        );
1896        let diagnostics = folded
1897            .diagnostics
1898            .iter()
1899            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1900            .collect::<Vec<_>>();
1901
1902        assert_eq!(diagnostics.len(), 4);
1903        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1018"));
1904
1905        let provenance = folded.diagnostics[0]
1906            .provenance
1907            .as_ref()
1908            .expect("toggle diagnostic provenance");
1909        assert_eq!(
1910            provenance.path,
1911            Path::new(
1912                "fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx"
1913            )
1914        );
1915        assert_eq!(provenance.span.line, 10);
1916        assert_eq!(provenance.span.column, 5);
1917    }
1918
1919    #[test]
1920    fn reports_non_numeric_primitive_increment_and_decrement_actions() {
1921        let source = include_str!(
1922            "../../../fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx"
1923        );
1924        let parsed = presolve_parser::parse_file(
1925            "fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx",
1926            source,
1927        );
1928        let graph = build_component_graph_for_module(&parsed);
1929        let folded = ConstantFoldingPass.transform(
1930            &build_application_semantic_model_from_component_graph(&graph),
1931        );
1932        let diagnostics = folded
1933            .diagnostics
1934            .iter()
1935            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1936            .collect::<Vec<_>>();
1937
1938        assert_eq!(diagnostics.len(), 4);
1939        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1019"));
1940
1941        let provenance = folded.diagnostics[0]
1942            .provenance
1943            .as_ref()
1944            .expect("numeric action diagnostic provenance");
1945        assert_eq!(
1946            provenance.path,
1947            Path::new(
1948                "fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx"
1949            )
1950        );
1951        assert_eq!(provenance.span.line, 10);
1952        assert_eq!(provenance.span.column, 5);
1953    }
1954
1955    #[test]
1956    fn reports_compound_numeric_action_target_and_operand_mismatches() {
1957        let source = include_str!(
1958            "../../../fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx"
1959        );
1960        let parsed = presolve_parser::parse_file(
1961            "fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx",
1962            source,
1963        );
1964        let graph = build_component_graph_for_module(&parsed);
1965        let folded = ConstantFoldingPass.transform(
1966            &build_application_semantic_model_from_component_graph(&graph),
1967        );
1968        let diagnostics = folded
1969            .diagnostics
1970            .iter()
1971            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1972            .collect::<Vec<_>>();
1973
1974        assert_eq!(diagnostics.len(), 7);
1975        assert_eq!(
1976            diagnostics
1977                .iter()
1978                .filter(|(code, _)| *code == "PSC1020")
1979                .count(),
1980            3
1981        );
1982        assert_eq!(
1983            diagnostics
1984                .iter()
1985                .filter(|(code, _)| *code == "PSC1021")
1986                .count(),
1987            4
1988        );
1989
1990        let provenance = folded.diagnostics[0]
1991            .provenance
1992            .as_ref()
1993            .expect("compound action diagnostic provenance");
1994        assert_eq!(
1995            provenance.path,
1996            Path::new(
1997                "fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx"
1998            )
1999        );
2000        assert_eq!(provenance.span.line, 9);
2001        assert_eq!(provenance.span.column, 5);
2002    }
2003
2004    #[test]
2005    fn assembles_application_semantic_model_from_multiple_files() {
2006        let unit = CompilationUnit::parse_sources([
2007            (
2008                "src/Zeta.tsx",
2009                r#"
2010@component("x-zeta")
2011class Zeta extends Component {
2012  render() {
2013    return <div>Zeta</div>;
2014  }
2015}
2016"#,
2017            ),
2018            (
2019                "src/Alpha.tsx",
2020                r#"
2021@component("x-alpha")
2022class Alpha extends Component {
2023  render() {
2024    return <div>Alpha</div>;
2025  }
2026}
2027"#,
2028            ),
2029        ]);
2030
2031        let asm = build_application_semantic_model_for_unit(&unit);
2032
2033        assert_eq!(
2034            unit.files()
2035                .iter()
2036                .map(|file| file.path.to_string_lossy().into_owned())
2037                .collect::<Vec<_>>(),
2038            vec!["src/Alpha.tsx", "src/Zeta.tsx"]
2039        );
2040        assert_eq!(
2041            asm.components
2042                .iter()
2043                .map(|component| component.id.as_str())
2044                .collect::<Vec<_>>(),
2045            vec![
2046                "module:src/Alpha.tsx/component:x-alpha",
2047                "module:src/Zeta.tsx/component:x-zeta"
2048            ]
2049        );
2050        assert!(asm.diagnostics.is_empty());
2051        assert!(validate_application_semantic_model(&asm).is_empty());
2052    }
2053
2054    #[test]
2055    fn component_graph_reports_semantic_errors() {
2056        let source =
2057            include_str!("../../../fixtures/0003-semantic-errors/input/BrokenSemantics.tsx");
2058
2059        let parsed = presolve_parser::parse_file(
2060            "fixtures/0003-semantic-errors/input/BrokenSemantics.tsx",
2061            source,
2062        );
2063
2064        let graph = build_component_graph(&parsed);
2065
2066        let codes = graph
2067            .diagnostics
2068            .iter()
2069            .map(|diagnostic| diagnostic.code.as_str())
2070            .collect::<Vec<_>>();
2071
2072        assert!(codes.contains(&"PSC1001"));
2073        assert!(codes.contains(&"PSC1003"));
2074        assert!(codes.contains(&"PSC1004"));
2075        assert!(graph.references.is_empty());
2076    }
2077
2078    #[test]
2079    fn component_graph_reports_unsupported_event_errors() {
2080        let source = r#"
2081@component("x-counter")
2082class Counter extends Component {
2083  count = state(0);
2084
2085  increment() {
2086    this.count++;
2087  }
2088
2089  render() {
2090    return <button onMouseover={() => this.increment()}>Count: {this.count}</button>;
2091  }
2092}
2093"#;
2094
2095        let parsed = presolve_parser::parse_file("UnsupportedEvent.tsx", source);
2096
2097        let graph = build_component_graph(&parsed);
2098
2099        assert!(graph
2100            .diagnostics
2101            .iter()
2102            .any(|diagnostic| diagnostic.code == "PSC1005"));
2103    }
2104
2105    #[test]
2106    fn component_graph_validates_static_action_parameter_bindings() {
2107        let source = r#"
2108@component("x-parameterized")
2109class Parameterized extends Component {
2110  label = state("Ready");
2111
2112  @action() setLabel(value: string) {
2113    this.label = value;
2114  }
2115
2116  render() {
2117    return <button onClick={() => this.setLabel(1)}>{this.label}</button>;
2118  }
2119}
2120"#;
2121
2122        let parsed = presolve_parser::parse_file("Parameterized.tsx", source);
2123        let graph = build_component_graph(&parsed);
2124        let codes = graph
2125            .diagnostics
2126            .iter()
2127            .map(|diagnostic| diagnostic.code.as_str())
2128            .collect::<Vec<_>>();
2129
2130        assert!(codes.contains(&"PSC1043"));
2131        assert!(!codes.contains(&"PSC1041"));
2132        assert!(!codes.contains(&"PSC1042"));
2133    }
2134
2135    #[test]
2136    fn component_graph_rejects_unbound_action_parameters() {
2137        let source = r#"
2138@component("x-parameterized")
2139class Parameterized extends Component {
2140  label = state("Ready");
2141
2142  @action() setLabel(value: string) {
2143    this.label = value;
2144  }
2145
2146  render() {
2147    return <button onClick={this.setLabel}>{this.label}</button>;
2148  }
2149}
2150"#;
2151
2152        let parsed = presolve_parser::parse_file("UnboundParameterized.tsx", source);
2153        let graph = build_component_graph(&parsed);
2154
2155        assert!(graph
2156            .diagnostics
2157            .iter()
2158            .any(|diagnostic| diagnostic.code == "PSC1042"));
2159    }
2160
2161    #[test]
2162    fn component_graph_requires_action_decorator_for_parameter_state_assignment() {
2163        let source = r#"
2164@component("x-parameterized")
2165class Parameterized extends Component {
2166  label = state("Ready");
2167
2168  setLabel(value: string) {
2169    this.label = value;
2170  }
2171
2172  render() {
2173    return <button onClick={() => this.setLabel("Locked")}>{this.label}</button>;
2174  }
2175}
2176"#;
2177
2178        let parsed = presolve_parser::parse_file("UndecoratedParameterized.tsx", source);
2179        let graph = build_component_graph(&parsed);
2180
2181        assert!(graph.diagnostics.iter().any(|diagnostic| {
2182            diagnostic.code == "PSC1041" && diagnostic.message.contains("requires @action()")
2183        }));
2184    }
2185
2186    #[test]
2187    fn component_graph_rejects_action_parameter_state_type_mismatch() {
2188        let source = r#"
2189@component("x-parameterized")
2190class Parameterized extends Component {
2191  count = state(0);
2192
2193  @action() setCount(value: string) {
2194    this.count = value;
2195  }
2196
2197  render() {
2198    return <button onClick={() => this.setCount("Locked")}>{this.count}</button>;
2199  }
2200}
2201"#;
2202
2203        let parsed = presolve_parser::parse_file("MismatchedParameterized.tsx", source);
2204        let graph = build_component_graph(&parsed);
2205
2206        assert!(graph
2207            .diagnostics
2208            .iter()
2209            .any(|diagnostic| diagnostic.code == "PSC1044"));
2210    }
2211
2212    #[test]
2213    fn component_graph_lowers_serializable_action_local_to_state_assignment() {
2214        let source = r#"
2215@component("x-action-local")
2216class ActionLocal extends Component {
2217  label = state("Ready");
2218
2219  @action() lock() {
2220    const next = "Locked";
2221    this.label = next;
2222  }
2223
2224  render() {
2225    return <button onClick={this.lock}>{this.label}</button>;
2226  }
2227}
2228"#;
2229
2230        let parsed = presolve_parser::parse_file("ActionLocal.tsx", source);
2231        let graph = build_component_graph(&parsed);
2232
2233        assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2234        assert!(matches!(
2235            graph.components[0].actions[0].operation,
2236            StateOperation::Assign(SerializableValue::String(ref value)) if value == "Locked"
2237        ));
2238    }
2239
2240    #[test]
2241    fn component_graph_lowers_structured_serializable_action_local_to_state_assignment() {
2242        let source = r#"
2243@component("x-action-local-record")
2244class ActionLocalRecord extends Component {
2245  profile = state({ name: "Ready", roles: ["reader"] });
2246
2247  @action() promote() {
2248    const next = { name: "Locked", roles: ["writer", "admin"] };
2249    this.profile = next;
2250  }
2251
2252  render() {
2253    return <button onClick={this.promote}>Promote</button>;
2254  }
2255}
2256"#;
2257
2258        let parsed = presolve_parser::parse_file("ActionLocalRecord.tsx", source);
2259        let graph = build_component_graph(&parsed);
2260
2261        assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2262        assert!(matches!(
2263            graph.components[0].actions[0].operation,
2264            StateOperation::Assign(SerializableValue::Object(_))
2265        ));
2266    }
2267
2268    #[test]
2269    fn component_graph_retains_resource_declaration_facts_before_package_resolution() {
2270        let parsed = presolve_parser::parse_file(
2271            "ResourceFact.tsx",
2272            r#"
2273@component("x-resource-fact")
2274class ResourceFact extends Component {
2275  @resource("profile") profile!: string;
2276  render() { return <div>Profile</div>; }
2277}
2278"#,
2279        );
2280        let graph = build_component_graph(&parsed);
2281        let facts = &graph.components[0].resource_declaration_candidates;
2282        assert_eq!(facts.len(), 1);
2283        assert_eq!(facts[0].field, "profile");
2284        assert!(facts[0].decorator_invoked);
2285        assert_eq!(facts[0].decorator_argument_count, 1);
2286        assert_eq!(facts[0].endpoint_designator.as_deref(), Some("profile"));
2287        assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2288    }
2289
2290    #[test]
2291    fn component_graph_retains_and_validates_opaque_action_declarations() {
2292        let parsed = presolve_parser::parse_file(
2293            "OpaqueActions.tsx",
2294            r#"
2295@component("x-opaque-actions")
2296class OpaqueActions extends Component {
2297  @action() @opaque("@acme/analytics", "trackPurchase")
2298  track(): void {}
2299
2300  @action() @opaque("@acme/analytics")
2301  missingExport(): void {}
2302
2303  @opaque("@acme/analytics", "trackPurchase")
2304  missingAction(): void {}
2305
2306  @action() @opaque("@acme/analytics", "trackPurchase")
2307  writesState(): void { this.count++; }
2308
2309  count = state(0);
2310
2311  render() { return <button onClick={this.track}>Buy</button>; }
2312}
2313"#,
2314        );
2315
2316        let graph = build_component_graph(&parsed);
2317        let component = &graph.components[0];
2318        assert_eq!(component.opaque_action_facts.len(), 4);
2319
2320        let track = component
2321            .opaque_action_facts
2322            .iter()
2323            .find(|fact| fact.method_name == "track")
2324            .expect("retained track declaration");
2325        assert_eq!(
2326            track.id.as_str(),
2327            "component:x-opaque-actions/opaque-activation:track"
2328        );
2329        assert_eq!(track.package.as_deref(), Some("@acme/analytics"));
2330        assert_eq!(track.export.as_deref(), Some("trackPurchase"));
2331        assert!(track.is_action);
2332        assert!(track.action_invoked);
2333        assert!(!track.has_body_effects);
2334
2335        assert_eq!(
2336            graph
2337                .diagnostics
2338                .iter()
2339                .filter(|diagnostic| diagnostic.code == "PSC1130")
2340                .count(),
2341            3,
2342            "{:?}",
2343            graph.diagnostics
2344        );
2345    }
2346
2347    #[test]
2348    fn resolves_opaque_action_through_an_integrity_checked_terminal_package_contract() {
2349        let unit = CompilationUnit::parse_sources([(
2350            "src/Checkout.tsx",
2351            r#"
2352import { trackPurchase } from "@acme/analytics";
2353
2354@component("x-checkout")
2355class Checkout extends Component {
2356  @action() @opaque("@acme/analytics", "trackPurchase")
2357  track(): void {}
2358
2359  render() { return <button onClick={this.track}>Buy</button>; }
2360}
2361"#,
2362        )]);
2363        let contract = parse_semantic_package_contract(
2364            r#"{"schema_version":1,"package":"@acme/analytics","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"trackPurchase":{"kind":"opaque","type_signature":"() -> void","runtime_module":"dist/track.js","resume_policy":"cold_fallback","opaque_terminal":{"execution_boundary":"client","resume":"cold_fallback"}}}}"#,
2365        )
2366        .expect("opaque terminal contract");
2367        let mut packages = SemanticPackageResolutionTable::default();
2368        packages
2369            .insert("@acme/analytics".into(), contract)
2370            .expect("unique contract");
2371
2372        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2373        assert_eq!(model.opaque_action_resolutions.len(), 1);
2374        let resolution = &model.opaque_action_resolutions[0];
2375        assert_eq!(
2376            resolution.activation.as_str(),
2377            "module:src/Checkout.tsx/component:x-checkout/opaque-activation:track"
2378        );
2379        let OpaqueActionResolutionOutcome::Resolved(binding) = &resolution.outcome else {
2380            panic!(
2381                "expected resolved opaque terminal: {:?}",
2382                resolution.outcome
2383            );
2384        };
2385        assert_eq!(binding.package, "@acme/analytics");
2386        assert_eq!(binding.version, "1.2.3");
2387        assert_eq!(binding.export, "trackPurchase");
2388        assert_eq!(binding.runtime_module, "dist/track.js");
2389        assert!(model
2390            .diagnostics
2391            .iter()
2392            .all(|diagnostic| diagnostic.code != "PSC1130" && diagnostic.code != "PSC1131"));
2393    }
2394
2395    #[test]
2396    fn opaque_action_rejects_a_nonopaque_package_export() {
2397        let unit = CompilationUnit::parse_sources([(
2398            "src/Checkout.tsx",
2399            r#"
2400import { format } from "date-kit";
2401
2402@component("x-checkout")
2403class Checkout extends Component {
2404  @action() @opaque("date-kit", "format")
2405  track(): void {}
2406
2407  render() { return <button onClick={this.track}>Buy</button>; }
2408}
2409"#,
2410        )]);
2411        let contract = parse_semantic_package_contract(
2412            r#"{"schema_version":1,"package":"date-kit","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date) -> string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#,
2413        )
2414        .expect("pure contract");
2415        let mut packages = SemanticPackageResolutionTable::default();
2416        packages.insert("date-kit".into(), contract).unwrap();
2417
2418        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2419        assert!(matches!(
2420            model.opaque_action_resolutions[0].outcome,
2421            OpaqueActionResolutionOutcome::NonOpaqueBinding {
2422                kind: SemanticPackageKind::Pure,
2423                ..
2424            }
2425        ));
2426        assert!(model
2427            .diagnostics
2428            .iter()
2429            .any(|diagnostic| diagnostic.code == "PSC1131"));
2430    }
2431
2432    #[test]
2433    fn resolves_resource_source_designator_through_integrity_checked_package_contract() {
2434        let unit = CompilationUnit::parse_sources([(
2435            "src/Profile.tsx",
2436            r#"
2437import { loadProfile } from "profile-service";
2438
2439@component("x-profile")
2440class Profile extends Component {
2441  @resource("loadProfile") profile!: Resource<string, string>;
2442  render() { return <div>Profile</div>; }
2443}
2444"#,
2445        )]);
2446        let contract = parse_semantic_package_contract(
2447            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
2448        )
2449        .expect("resource contract");
2450        let mut packages = SemanticPackageResolutionTable::default();
2451        packages
2452            .insert("profile-service".into(), contract)
2453            .expect("unique package contract");
2454
2455        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2456        assert_eq!(model.resource_endpoint_resolutions.len(), 1);
2457        let ResourceEndpointResolutionOutcome::Resolved(endpoint) =
2458            &model.resource_endpoint_resolutions[0].outcome
2459        else {
2460            panic!("expected resolved resource endpoint");
2461        };
2462        assert_eq!(endpoint.local_name, "loadProfile");
2463        assert_eq!(endpoint.package, "profile-service");
2464        assert_eq!(endpoint.version, "1.2.3");
2465        assert_eq!(endpoint.export, "loadProfile");
2466        assert_eq!(
2467            endpoint.endpoint.execution_boundary,
2468            SemanticPackageResourceExecutionBoundary::Shared
2469        );
2470        assert_eq!(model.resource_declarations.len(), 1);
2471        let declaration = model
2472            .resource_declarations
2473            .values()
2474            .next()
2475            .expect("resource declaration");
2476        assert_eq!(declaration.name, "profile");
2477        assert_eq!(declaration.data_type, SemanticType::String);
2478        assert_eq!(declaration.error_type, SemanticType::String);
2479        assert_eq!(
2480            declaration.execution_boundary,
2481            ResourceExecutionBoundary::Shared
2482        );
2483        assert_eq!(model.resource_activations.len(), 1);
2484        assert!(model.resource_activations.values().all(|activation| {
2485            activation.declaration == declaration.id
2486                && activation.state == ResourceLifecycleState::Idle
2487        }));
2488        assert!(
2489            model
2490                .diagnostics
2491                .iter()
2492                .all(|diagnostic| { diagnostic.code != "PSC1046" && diagnostic.code != "PSC1128" }),
2493            "{:?}",
2494            model.diagnostics
2495        );
2496        assert!(matches!(
2497            model
2498                .semantic_types
2499                .assignments
2500                .get(declaration.id.as_semantic_id())
2501                .map(|assignment| &assignment.semantic_type),
2502            Some(SemanticType::Resource(resource))
2503                if resource.data.as_ref() == &SemanticType::String
2504                    && resource.error.as_ref() == &SemanticType::String
2505        ));
2506    }
2507
2508    #[test]
2509    fn lowers_direct_computed_resource_data_projection_with_exact_declaration_identity() {
2510        let unit = CompilationUnit::parse_sources([(
2511            "src/Profile.tsx",
2512            r#"
2513import { loadProfile } from "profile-service";
2514
2515@component("x-profile")
2516class Profile extends Component {
2517  @resource("loadProfile") profile!: Resource<string, string>;
2518  @computed() get profileName(): string | null { return this.profile.data; }
2519  render() { return <div>{this.profileName}</div>; }
2520}
2521"#,
2522        )]);
2523        let contract = parse_semantic_package_contract(
2524            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
2525        )
2526        .expect("resource contract");
2527        let mut packages = SemanticPackageResolutionTable::default();
2528        packages
2529            .insert("profile-service".into(), contract)
2530            .expect("unique package contract");
2531
2532        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2533        assert!(model.diagnostics.is_empty(), "{:?}", model.diagnostics);
2534        let declaration = model
2535            .resource_declarations
2536            .values()
2537            .next()
2538            .expect("resource declaration");
2539        let computed = model.components[0].id.computed("profileName");
2540        assert!(model.references.iter().any(|reference| {
2541            reference.kind == SemanticReferenceKind::ComputedResource
2542                && reference.source == computed
2543                && reference.target == *declaration.id.as_semantic_id()
2544        }));
2545        assert_eq!(
2546            model.semantic_types.computed_values[&computed].semantic_type,
2547            SemanticType::Union(vec![SemanticType::Null, SemanticType::String])
2548        );
2549
2550        let ir = lower_components_to_ir(&model);
2551        assert!(ir
2552            .modules
2553            .iter()
2554            .flat_map(|module| &module.functions)
2555            .any(|function| {
2556                function
2557                    .blocks
2558                    .iter()
2559                    .flat_map(|block| &block.instructions)
2560                    .any(|instruction| {
2561                        matches!(
2562                            &instruction.kind,
2563                            IrInstructionKind::LoadResource { declaration: loaded }
2564                                if loaded == declaration.id.as_semantic_id()
2565                        )
2566                    })
2567            }));
2568        let artifact = build_runtime_computed_artifact(&model, &ir);
2569        assert!(artifact.resource_invalidations.iter().any(|invalidation| {
2570            invalidation.declaration == declaration.id.as_str()
2571                && invalidation.dependents == vec![computed.to_string()]
2572        }));
2573        assert!(artifact.evaluations.iter().any(|evaluation| {
2574            evaluation.computed == computed.as_str()
2575                && evaluation.program.instructions.iter().any(|instruction| {
2576                    matches!(
2577                        instruction,
2578                        RuntimeComputedArtifactInstruction::LoadResource { declaration: loaded, .. }
2579                            if loaded == declaration.id.as_str()
2580                    )
2581                })
2582        }));
2583
2584        let chained_unit = CompilationUnit::parse_sources([(
2585            "src/ChainedProfile.tsx",
2586            r#"
2587import { loadProfile } from "profile-service";
2588
2589@component("x-chained-profile")
2590class ChainedProfile extends Component {
2591  @resource("loadProfile") profile!: Resource<string, string>;
2592  @computed() get invalid() { return this.profile.data.length; }
2593  render() { return <div>Profile</div>; }
2594}
2595"#,
2596        )]);
2597        let chained =
2598            build_application_semantic_model_for_unit_with_packages(&chained_unit, &packages);
2599        assert!(chained.diagnostics.iter().any(|diagnostic| {
2600            diagnostic.code == ComputedDiagnosticCode::UnsupportedBody.as_str()
2601                && diagnostic.message.contains("may only directly project")
2602        }));
2603    }
2604
2605    #[test]
2606    fn component_graph_reports_duplicate_event_errors() {
2607        let parsed = presolve_parser::ParsedFile {
2608            path: "DuplicateEvent.tsx".into(),
2609            syntax: presolve_parser::ParsedSourceAst {
2610                source: String::new(),
2611                estree_json: "{}".to_owned(),
2612                span: test_span(),
2613            },
2614            diagnostics: Vec::new(),
2615            imports: Vec::new(),
2616            exports: Vec::new(),
2617            call_expressions: Vec::new(),
2618            type_aliases: Vec::new(),
2619            local_type_bindings: Vec::new(),
2620            local_value_bindings: Vec::new(),
2621            classes: vec![presolve_parser::ParsedClass {
2622                name: "DuplicateEvent".to_string(),
2623                span: test_span(),
2624                heritage: None,
2625                decorators: vec![presolve_parser::ParsedDecorator {
2626                    name: "component".to_string(),
2627                    is_invoked: true,
2628                    arguments: vec![Some("x-duplicate-event".to_string())],
2629                    argument: Some("x-duplicate-event".to_string()),
2630                    argument_count: 1,
2631                    argument_spans: vec![test_span()],
2632                    static_member_argument: None,
2633                    this_member_argument: None,
2634                    validation_rule_expression: None,
2635                    span: test_span(),
2636                }],
2637                properties: Vec::new(),
2638                methods: vec![presolve_parser::ParsedMethod {
2639                    name: "render".to_string(),
2640                    span: test_span(),
2641                    decorators: Vec::new(),
2642                    is_getter: false,
2643                    is_setter: false,
2644                    is_async: false,
2645                    is_static: false,
2646                    jsx_roots: vec![presolve_parser::ParsedJsxNode::Element(
2647                        presolve_parser::ParsedJsxElement {
2648                            name: "button".to_string(),
2649                            name_span: test_span(),
2650                            span: test_span(),
2651                            attributes: Vec::new(),
2652                            event_handlers: vec![
2653                                presolve_parser::ParsedEventHandler {
2654                                    event: "click".to_string(),
2655                                    handler: "this.render".to_string(),
2656                                    arguments: Vec::new(),
2657                                    span: test_span(),
2658                                },
2659                                presolve_parser::ParsedEventHandler {
2660                                    event: "click".to_string(),
2661                                    handler: "this.render".to_string(),
2662                                    arguments: Vec::new(),
2663                                    span: test_span(),
2664                                },
2665                            ],
2666                            children: Vec::new(),
2667                        },
2668                    )],
2669                    bindings: Vec::new(),
2670                    state_updates: Vec::new(),
2671                    local_variables: Vec::new(),
2672                    parameters: Vec::new(),
2673                    return_type_annotation: None,
2674                    return_values: Vec::new(),
2675                    computed_expression: None,
2676                    effect_body: None,
2677                    calls: Vec::new(),
2678                }],
2679            }],
2680        };
2681
2682        let graph = build_component_graph(&parsed);
2683
2684        assert!(graph
2685            .diagnostics
2686            .iter()
2687            .any(|diagnostic| diagnostic.code == "PSC1006"));
2688    }
2689
2690    fn test_span() -> presolve_parser::SourceSpan {
2691        presolve_parser::SourceSpan {
2692            start: 0,
2693            end: 0,
2694            line: 1,
2695            column: 1,
2696        }
2697    }
2698
2699    #[test]
2700    fn builds_increment_action_from_parsed_method_update() {
2701        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
2702
2703        let parsed =
2704            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
2705
2706        let graph = build_component_graph(&parsed);
2707        let component = graph.components.first().expect("expected component");
2708
2709        assert_eq!(
2710            component.actions,
2711            vec![ComponentAction {
2712                id: SemanticId::component(Some("x-nested-counter"), "NestedCounter")
2713                    .action("increment", 0),
2714                owner: SemanticOwner::entity(
2715                    SemanticId::component(Some("x-nested-counter"), "NestedCounter")
2716                        .method("increment"),
2717                ),
2718                method: "increment".to_string(),
2719                operation: StateOperation::Increment,
2720                field: "count".to_string(),
2721            }]
2722        );
2723    }
2724
2725    #[test]
2726    fn builds_decrement_action_from_parsed_method_update() {
2727        let source =
2728            include_str!("../../../fixtures/0009-decrement-counter/input/DecrementCounter.tsx");
2729
2730        let parsed = presolve_parser::parse_file(
2731            "fixtures/0009-decrement-counter/input/DecrementCounter.tsx",
2732            source,
2733        );
2734
2735        let graph = build_component_graph(&parsed);
2736        let component = graph.components.first().expect("expected component");
2737
2738        assert_eq!(
2739            component.actions,
2740            vec![ComponentAction {
2741                id: SemanticId::component(Some("x-decrement-counter"), "DecrementCounter")
2742                    .action("decrement", 0),
2743                owner: SemanticOwner::entity(
2744                    SemanticId::component(Some("x-decrement-counter"), "DecrementCounter")
2745                        .method("decrement"),
2746                ),
2747                method: "decrement".to_string(),
2748                operation: StateOperation::Decrement,
2749                field: "count".to_string(),
2750            }]
2751        );
2752    }
2753
2754    #[test]
2755    fn builds_add_and_subtract_assign_actions_from_parsed_method_updates() {
2756        let source =
2757            include_str!("../../../fixtures/0010-add-subtract-assign/input/StepCounter.tsx");
2758
2759        let parsed = presolve_parser::parse_file(
2760            "fixtures/0010-add-subtract-assign/input/StepCounter.tsx",
2761            source,
2762        );
2763
2764        let graph = build_component_graph(&parsed);
2765        let component = graph.components.first().expect("expected component");
2766
2767        assert_eq!(
2768            component.actions,
2769            vec![
2770                ComponentAction {
2771                    id: SemanticId::component(Some("x-step-counter"), "StepCounter")
2772                        .action("addTwo", 0),
2773                    owner: SemanticOwner::entity(
2774                        SemanticId::component(Some("x-step-counter"), "StepCounter")
2775                            .method("addTwo"),
2776                    ),
2777                    method: "addTwo".to_string(),
2778                    operation: StateOperation::AddAssign(SerializableValue::Number(
2779                        "2".to_string()
2780                    )),
2781                    field: "count".to_string(),
2782                },
2783                ComponentAction {
2784                    id: SemanticId::component(Some("x-step-counter"), "StepCounter")
2785                        .action("subtractThree", 0),
2786                    owner: SemanticOwner::entity(
2787                        SemanticId::component(Some("x-step-counter"), "StepCounter")
2788                            .method("subtractThree"),
2789                    ),
2790                    method: "subtractThree".to_string(),
2791                    operation: StateOperation::SubtractAssign(SerializableValue::Number(
2792                        "3".to_string()
2793                    )),
2794                    field: "count".to_string(),
2795                }
2796            ]
2797        );
2798    }
2799
2800    #[test]
2801    fn builds_direct_assignment_action_from_parsed_method_update() {
2802        let source =
2803            include_str!("../../../fixtures/0011-direct-assignment/input/ResetCounter.tsx");
2804
2805        let parsed = presolve_parser::parse_file(
2806            "fixtures/0011-direct-assignment/input/ResetCounter.tsx",
2807            source,
2808        );
2809
2810        let graph = build_component_graph(&parsed);
2811        let component = graph.components.first().expect("expected component");
2812
2813        assert_eq!(
2814            component.actions,
2815            vec![ComponentAction {
2816                id: SemanticId::component(Some("x-reset-counter"), "ResetCounter")
2817                    .action("reset", 0),
2818                owner: SemanticOwner::entity(
2819                    SemanticId::component(Some("x-reset-counter"), "ResetCounter").method("reset"),
2820                ),
2821                method: "reset".to_string(),
2822                operation: StateOperation::Assign(SerializableValue::Number("0".to_string())),
2823                field: "count".to_string(),
2824            }]
2825        );
2826    }
2827
2828    #[test]
2829    fn builds_boolean_toggle_action_from_parsed_method_update() {
2830        let source = include_str!("../../../fixtures/0012-boolean-toggle/input/ToggleFlag.tsx");
2831
2832        let parsed = presolve_parser::parse_file(
2833            "fixtures/0012-boolean-toggle/input/ToggleFlag.tsx",
2834            source,
2835        );
2836
2837        let graph = build_component_graph(&parsed);
2838        let component = graph.components.first().expect("expected component");
2839
2840        assert_eq!(
2841            component.actions,
2842            vec![ComponentAction {
2843                id: SemanticId::component(Some("x-toggle-flag"), "ToggleFlag").action("toggle", 0),
2844                owner: SemanticOwner::entity(
2845                    SemanticId::component(Some("x-toggle-flag"), "ToggleFlag").method("toggle"),
2846                ),
2847                method: "toggle".to_string(),
2848                operation: StateOperation::Toggle,
2849                field: "enabled".to_string(),
2850            }]
2851        );
2852    }
2853
2854    #[test]
2855    fn builds_multi_step_actions_from_parsed_method_updates_in_source_order() {
2856        let source =
2857            include_str!("../../../fixtures/0013-multi-step-action/input/BatchActionCounter.tsx");
2858
2859        let parsed = presolve_parser::parse_file(
2860            "fixtures/0013-multi-step-action/input/BatchActionCounter.tsx",
2861            source,
2862        );
2863
2864        let graph = build_component_graph(&parsed);
2865        let component = graph.components.first().expect("expected component");
2866
2867        assert_eq!(
2868            component.actions,
2869            vec![
2870                ComponentAction {
2871                    id:
2872                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2873                            .action("apply", 0),
2874                    owner:
2875                        SemanticOwner::entity(
2876                            SemanticId::component(
2877                                Some("x-batch-action-counter"),
2878                                "BatchActionCounter",
2879                            )
2880                            .method("apply"),
2881                        ),
2882                    method: "apply".to_string(),
2883                    operation: StateOperation::AddAssign(SerializableValue::Number(
2884                        "2".to_string()
2885                    )),
2886                    field: "count".to_string(),
2887                },
2888                ComponentAction {
2889                    id:
2890                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2891                            .action("apply", 1),
2892                    owner:
2893                        SemanticOwner::entity(
2894                            SemanticId::component(
2895                                Some("x-batch-action-counter"),
2896                                "BatchActionCounter",
2897                            )
2898                            .method("apply"),
2899                        ),
2900                    method: "apply".to_string(),
2901                    operation: StateOperation::Decrement,
2902                    field: "count".to_string(),
2903                },
2904                ComponentAction {
2905                    id:
2906                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2907                            .action("apply", 2),
2908                    owner:
2909                        SemanticOwner::entity(
2910                            SemanticId::component(
2911                                Some("x-batch-action-counter"),
2912                                "BatchActionCounter",
2913                            )
2914                            .method("apply"),
2915                        ),
2916                    method: "apply".to_string(),
2917                    operation: StateOperation::Assign(SerializableValue::Number("8".to_string())),
2918                    field: "count".to_string(),
2919                },
2920                ComponentAction {
2921                    id:
2922                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2923                            .action("apply", 3),
2924                    owner:
2925                        SemanticOwner::entity(
2926                            SemanticId::component(
2927                                Some("x-batch-action-counter"),
2928                                "BatchActionCounter",
2929                            )
2930                            .method("apply"),
2931                        ),
2932                    method: "apply".to_string(),
2933                    operation: StateOperation::Increment,
2934                    field: "count".to_string(),
2935                },
2936                ComponentAction {
2937                    id:
2938                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2939                            .action("apply", 4),
2940                    owner:
2941                        SemanticOwner::entity(
2942                            SemanticId::component(
2943                                Some("x-batch-action-counter"),
2944                                "BatchActionCounter",
2945                            )
2946                            .method("apply"),
2947                        ),
2948                    method: "apply".to_string(),
2949                    operation: StateOperation::Toggle,
2950                    field: "enabled".to_string(),
2951                }
2952            ]
2953        );
2954    }
2955
2956    #[test]
2957    fn generates_static_html_from_template_graph() {
2958        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
2959
2960        let parsed =
2961            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
2962
2963        let component_graph = build_component_graph(&parsed);
2964        let template_graph = build_template_graph(&component_graph);
2965        let html = generate_static_html(&template_graph);
2966
2967        assert_eq!(
2968            html,
2969            "<button data-presolve-node=\"n0\" data-presolve-on-click=\"this.increment\" data-presolve-bindings=\"this.count\">Count:<!-- presolve-binding:n1:this.count -->0</button>\n"
2970        );
2971    }
2972
2973    #[test]
2974    fn lowers_jsx_html_attribute_aliases_before_html_and_manifest_generation() {
2975        let source = r#"
2976@component("x-attribute-alias")
2977class AttributeAlias extends Component {
2978  target = state("profile-name");
2979  render() {
2980    return <label className={this.target} htmlFor={this.target}>Profile</label>;
2981  }
2982}
2983"#;
2984        let parsed = presolve_parser::parse_file("AttributeAlias.tsx", source);
2985        let component_graph = build_component_graph(&parsed);
2986        let template_graph = build_template_graph(&component_graph);
2987        let html = generate_static_html(&template_graph);
2988        let manifest = build_template_manifest(&component_graph, &template_graph);
2989        let model = build_application_semantic_model_from_component_graph(&component_graph);
2990        let runtime = build_runtime_component_artifact(&model, &model.component_ir_optimization);
2991
2992        assert!(html.contains("class=\"profile-name\""));
2993        assert!(html.contains("for=\"profile-name\""));
2994        assert!(!html.contains("className="));
2995        assert!(!html.contains("htmlFor="));
2996        assert_eq!(
2997            manifest.components[0]
2998                .template
2999                .nodes
3000                .iter()
3001                .filter_map(|node| match node {
3002                    ManifestNode::Binding { attribute, .. } => attribute.as_deref(),
3003                    _ => None,
3004                })
3005                .collect::<Vec<_>>(),
3006            vec!["class", "for"]
3007        );
3008        assert_eq!(
3009            runtime
3010                .ordinary_template_bindings
3011                .iter()
3012                .filter_map(|binding| binding.attribute_name.as_deref())
3013                .collect::<Vec<_>>(),
3014            vec!["class", "for"]
3015        );
3016    }
3017
3018    #[test]
3019    fn preserves_string_state_literals_in_template_outputs() {
3020        let source = include_str!("../../../fixtures/0006-string-state/input/StringGreeting.tsx");
3021
3022        let parsed = presolve_parser::parse_file(
3023            "fixtures/0006-string-state/input/StringGreeting.tsx",
3024            source,
3025        );
3026
3027        let component_graph = build_component_graph(&parsed);
3028        let component = component_graph
3029            .components
3030            .first()
3031            .expect("expected component");
3032
3033        assert_eq!(
3034            component.state_fields[0].initial_value,
3035            Some(SerializableValue::String("Austin & <Zero>".to_string()))
3036        );
3037
3038        let template_graph = build_template_graph(&component_graph);
3039        let html = generate_static_html(&template_graph);
3040
3041        assert_eq!(
3042            html,
3043            "<p data-presolve-node=\"n0\" data-presolve-bindings=\"this.name\">Name:<!-- presolve-binding:n1:this.name -->Austin &amp; &lt;Zero&gt;</p>\n"
3044        );
3045
3046        let manifest = build_template_manifest(&component_graph, &template_graph);
3047
3048        assert_eq!(
3049            manifest.components[0].template.nodes,
3050            vec![
3051                ManifestNode::Element {
3052                    id: "n0".to_string(),
3053                    tag: "p".to_string(),
3054                },
3055                ManifestNode::Binding {
3056                    id: "n1".to_string(),
3057                    expression: "this.name".to_string(),
3058                    initial_value: Some(SerializableValue::String("Austin & <Zero>".to_string())),
3059                    target: None,
3060                    element: None,
3061                    attribute: None,
3062                }
3063            ]
3064        );
3065
3066        let manifest_json = template_manifest_json(&manifest);
3067        let manifest_value: serde_json::Value =
3068            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3069
3070        assert_eq!(
3071            manifest_value["components"][0]["template"]["nodes"][1]["initial_value"],
3072            serde_json::json!("Austin & <Zero>")
3073        );
3074    }
3075
3076    #[test]
3077    fn decodes_jsx_text_character_references_before_static_html_escaping() {
3078        let parsed = presolve_parser::parse_file(
3079            "CodeExample.tsx",
3080            r#"
3081@component("x-code-example")
3082class CodeExample extends Component {
3083  render() {
3084    return <pre><code>&lt;button&gt;A &amp; B&#123;ok&#125;&lt;/button&gt;</code></pre>;
3085  }
3086}
3087"#,
3088        );
3089
3090        let component_graph = build_component_graph(&parsed);
3091        assert!(component_graph.diagnostics.is_empty());
3092
3093        let html = generate_static_html(&build_template_graph(&component_graph));
3094
3095        assert!(html.contains("&lt;button&gt;A &amp; B{ok}&lt;/button&gt;"));
3096        assert!(!html.contains("&amp;lt;button&amp;gt;"));
3097        assert!(!html.contains("&amp;#123;"));
3098    }
3099
3100    #[test]
3101    fn preserves_inline_jsx_text_boundaries_in_static_html() {
3102        let parsed = presolve_parser::parse_file(
3103            "CodeExample.tsx",
3104            r#"
3105@component("x-code-example")
3106class CodeExample extends Component {
3107  render() {
3108    return <pre><code><span>import &#123; </span><span className="syntax-type">Component</span><span> &#125; from </span><span>&quot;presolve&quot;</span>;</code></pre>;
3109  }
3110}
3111"#,
3112        );
3113
3114        let component_graph = build_component_graph(&parsed);
3115        assert!(component_graph.diagnostics.is_empty());
3116
3117        let html = generate_static_html(&build_template_graph(&component_graph));
3118
3119        assert!(html.contains("import { </span><span data-presolve-node"));
3120        assert!(html.contains("class=\"syntax-type\">Component</span><span"));
3121        assert!(html.contains("> } from </span><span"));
3122        assert!(html.contains(">\"presolve\"</span>;"));
3123        assert!(!html.contains("data-presolve-bindings"));
3124    }
3125
3126    #[test]
3127    fn preserves_static_jsx_attributes_in_template_outputs() {
3128        let source =
3129            include_str!("../../../fixtures/0014-static-attributes/input/StaticAttributePanel.tsx");
3130
3131        let parsed = presolve_parser::parse_file(
3132            "fixtures/0014-static-attributes/input/StaticAttributePanel.tsx",
3133            source,
3134        );
3135
3136        let component_graph = build_component_graph(&parsed);
3137        assert!(component_graph.diagnostics.is_empty());
3138
3139        let template_graph = build_template_graph(&component_graph);
3140        let root = template_graph.templates[0]
3141            .root
3142            .as_ref()
3143            .expect("expected root");
3144
3145        assert_eq!(root.attributes.len(), 3);
3146        assert_eq!(root.attributes[0].name, "id");
3147        assert_eq!(
3148            root.attributes[0].value,
3149            AttributeValue::Static("panel-root".to_string())
3150        );
3151        assert_eq!(root.attributes[1].name, "aria-label");
3152        assert_eq!(
3153            root.attributes[1].value,
3154            AttributeValue::Static("Status \"Panel\"".to_string())
3155        );
3156        assert_eq!(root.attributes[2].name, "hidden");
3157        assert_eq!(root.attributes[2].value, AttributeValue::Boolean);
3158
3159        let TemplateChild::Element(button) = &root.children[0] else {
3160            panic!("expected button child");
3161        };
3162
3163        assert_eq!(button.attributes.len(), 4);
3164        assert_eq!(button.attributes[0].name, "type");
3165        assert_eq!(
3166            button.attributes[0].value,
3167            AttributeValue::Static("button".to_string())
3168        );
3169        assert_eq!(button.attributes[1].name, "data-mode");
3170        assert_eq!(
3171            button.attributes[1].value,
3172            AttributeValue::Static("safe & sound".to_string())
3173        );
3174        assert_eq!(button.attributes[2].name, "title");
3175        assert_eq!(
3176            button.attributes[2].value,
3177            AttributeValue::Static("Use <carefully>".to_string())
3178        );
3179        assert_eq!(button.attributes[3].name, "data-presolve-bindings");
3180
3181        let html = generate_static_html(&template_graph);
3182
3183        assert_eq!(
3184            html,
3185            "<section data-presolve-node=\"n0\" id=\"panel-root\" aria-label=\"Status &quot;Panel&quot;\" hidden><button data-presolve-node=\"n1\" type=\"button\" data-mode=\"safe &amp; sound\" title=\"Use &lt;carefully&gt;\" data-presolve-bindings=\"this.label\">Label:<!-- presolve-binding:n2:this.label -->Ready</button></section>\n"
3186        );
3187    }
3188
3189    #[test]
3190    fn reports_static_attribute_semantic_errors() {
3191        let source = r#"
3192@component("x-bad-attrs")
3193class BadAttrs extends Component {
3194  disabled = state(false);
3195
3196  render() {
3197    return <button type="button" type="submit" title={label} {...props}>Go</button>;
3198  }
3199}
3200"#;
3201
3202        let parsed = presolve_parser::parse_file("BadAttrs.tsx", source);
3203        let graph = build_component_graph(&parsed);
3204        let codes = graph
3205            .diagnostics
3206            .iter()
3207            .map(|diagnostic| diagnostic.code.as_str())
3208            .collect::<Vec<_>>();
3209
3210        assert!(codes.contains(&"PSC1007"));
3211        assert!(codes.contains(&"PSC1008"));
3212        assert!(codes.contains(&"PSC1009"));
3213    }
3214
3215    #[test]
3216    fn builds_dynamic_attribute_bindings_in_template_outputs() {
3217        let source = include_str!(
3218            "../../../fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx"
3219        );
3220
3221        let parsed = presolve_parser::parse_file(
3222            "fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx",
3223            source,
3224        );
3225
3226        let component_graph = build_component_graph(&parsed);
3227        assert!(component_graph.diagnostics.is_empty());
3228
3229        let template_graph = build_template_graph(&component_graph);
3230        let root = template_graph.templates[0]
3231            .root
3232            .as_ref()
3233            .expect("expected root");
3234
3235        assert_eq!(root.attributes[0].name, "disabled");
3236        assert_eq!(
3237            root.attributes[0].value,
3238            AttributeValue::Binding {
3239                id: TemplateNodeId("n1".to_string()),
3240                expression: "this.disabled".to_string(),
3241                initial_value: Some(SerializableValue::Boolean(false)),
3242            }
3243        );
3244        assert_eq!(root.attributes[1].name, "title");
3245        assert_eq!(
3246            root.attributes[1].value,
3247            AttributeValue::Binding {
3248                id: TemplateNodeId("n2".to_string()),
3249                expression: "this.label".to_string(),
3250                initial_value: Some(SerializableValue::String("Ready".to_string())),
3251            }
3252        );
3253
3254        let html = generate_static_html(&template_graph);
3255
3256        assert_eq!(
3257            html,
3258            "<button data-presolve-node=\"n0\" title=\"Ready\" data-presolve-on-click=\"this.lock\" data-presolve-bindings=\"this.label\">Status:<!-- presolve-binding:n3:this.label -->Ready</button>\n"
3259        );
3260    }
3261
3262    #[test]
3263    fn preserves_boolean_state_literals_in_template_outputs() {
3264        let source = include_str!("../../../fixtures/0007-boolean-state/input/BooleanFlags.tsx");
3265
3266        let parsed = presolve_parser::parse_file(
3267            "fixtures/0007-boolean-state/input/BooleanFlags.tsx",
3268            source,
3269        );
3270
3271        let component_graph = build_component_graph(&parsed);
3272        let component = component_graph
3273            .components
3274            .first()
3275            .expect("expected component");
3276
3277        assert_eq!(
3278            component.state_fields[0].initial_value,
3279            Some(SerializableValue::Boolean(true))
3280        );
3281        assert_eq!(
3282            component.state_fields[1].initial_value,
3283            Some(SerializableValue::Boolean(false))
3284        );
3285
3286        let template_graph = build_template_graph(&component_graph);
3287        let html = generate_static_html(&template_graph);
3288
3289        assert_eq!(
3290            html,
3291            "<section data-presolve-node=\"n0\"><p data-presolve-node=\"n1\" data-presolve-bindings=\"this.enabled\">Enabled:<!-- presolve-binding:n2:this.enabled -->true</p><p data-presolve-node=\"n3\" data-presolve-bindings=\"this.disabled\">Disabled:<!-- presolve-binding:n4:this.disabled -->false</p></section>\n"
3292        );
3293
3294        let manifest = build_template_manifest(&component_graph, &template_graph);
3295
3296        assert_eq!(
3297            manifest.components[0].template.nodes,
3298            vec![
3299                ManifestNode::Element {
3300                    id: "n0".to_string(),
3301                    tag: "section".to_string(),
3302                },
3303                ManifestNode::Element {
3304                    id: "n1".to_string(),
3305                    tag: "p".to_string(),
3306                },
3307                ManifestNode::Binding {
3308                    id: "n2".to_string(),
3309                    expression: "this.enabled".to_string(),
3310                    initial_value: Some(SerializableValue::Boolean(true)),
3311                    target: None,
3312                    element: None,
3313                    attribute: None,
3314                },
3315                ManifestNode::Element {
3316                    id: "n3".to_string(),
3317                    tag: "p".to_string(),
3318                },
3319                ManifestNode::Binding {
3320                    id: "n4".to_string(),
3321                    expression: "this.disabled".to_string(),
3322                    initial_value: Some(SerializableValue::Boolean(false)),
3323                    target: None,
3324                    element: None,
3325                    attribute: None,
3326                }
3327            ]
3328        );
3329
3330        let manifest_json = template_manifest_json(&manifest);
3331        let manifest_value: serde_json::Value =
3332            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3333
3334        assert_eq!(
3335            manifest_value["components"][0]["template"]["nodes"][2]["initial_value"],
3336            serde_json::json!(true)
3337        );
3338        assert_eq!(
3339            manifest_value["components"][0]["template"]["nodes"][4]["initial_value"],
3340            serde_json::json!(false)
3341        );
3342    }
3343
3344    #[test]
3345    fn preserves_null_state_literals_in_template_outputs() {
3346        let source = include_str!("../../../fixtures/0008-null-state/input/NullSelection.tsx");
3347
3348        let parsed =
3349            presolve_parser::parse_file("fixtures/0008-null-state/input/NullSelection.tsx", source);
3350
3351        let component_graph = build_component_graph(&parsed);
3352        let component = component_graph
3353            .components
3354            .first()
3355            .expect("expected component");
3356
3357        assert_eq!(
3358            component.state_fields[0].initial_value,
3359            Some(SerializableValue::Null)
3360        );
3361
3362        let template_graph = build_template_graph(&component_graph);
3363        let html = generate_static_html(&template_graph);
3364
3365        assert_eq!(
3366            html,
3367            "<p data-presolve-node=\"n0\" data-presolve-bindings=\"this.selection\">Selection:<!-- presolve-binding:n1:this.selection --></p>\n"
3368        );
3369
3370        let manifest = build_template_manifest(&component_graph, &template_graph);
3371
3372        assert_eq!(
3373            manifest.components[0].template.nodes,
3374            vec![
3375                ManifestNode::Element {
3376                    id: "n0".to_string(),
3377                    tag: "p".to_string(),
3378                },
3379                ManifestNode::Binding {
3380                    id: "n1".to_string(),
3381                    expression: "this.selection".to_string(),
3382                    initial_value: Some(SerializableValue::Null),
3383                    target: None,
3384                    element: None,
3385                    attribute: None,
3386                }
3387            ]
3388        );
3389
3390        let manifest_json = template_manifest_json(&manifest);
3391        let manifest_value: serde_json::Value =
3392            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3393
3394        assert_eq!(
3395            manifest_value["components"][0]["template"]["nodes"][1]["initial_value"],
3396            serde_json::Value::Null
3397        );
3398    }
3399
3400    #[test]
3401    fn builds_template_graph_from_component_graph() {
3402        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
3403
3404        let parsed =
3405            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
3406
3407        let component_graph = build_component_graph(&parsed);
3408        let template_graph = build_template_graph(&component_graph);
3409
3410        assert_eq!(template_graph.templates.len(), 1);
3411
3412        let template = &template_graph.templates[0];
3413        assert_eq!(template.component_name, "Counter");
3414        assert_eq!(template.id.as_str(), "component:x-counter/template:render");
3415        assert_eq!(
3416            template.provenance.path,
3417            Path::new("fixtures/0001-source-summary/input/Counter.tsx")
3418        );
3419        assert_eq!(template.provenance.span.line, 10);
3420        assert_eq!(
3421            template.owner,
3422            SemanticOwner::entity(SemanticId::component(Some("x-counter"), "Counter"))
3423        );
3424
3425        let root = template.root.as_ref().expect("expected template root");
3426
3427        assert_eq!(root.id.0, "n0");
3428        assert_eq!(root.tag_name, "button");
3429        assert_eq!(root.span.line, 12);
3430        assert_eq!(root.span.column, 7);
3431
3432        assert_eq!(root.attributes.len(), 2);
3433        assert_eq!(root.attributes[0].name, "data-presolve-on-click");
3434        assert_eq!(
3435            root.attributes[0].span.expect("expected event span").line,
3436            12
3437        );
3438        assert_eq!(
3439            root.attributes[0].span.expect("expected event span").column,
3440            15
3441        );
3442        assert_eq!(
3443            root.attributes[0].value,
3444            AttributeValue::EventHandler {
3445                event: "click".to_string(),
3446                handler: "this.increment".to_string(),
3447                arguments: Vec::new(),
3448            }
3449        );
3450
3451        assert_eq!(root.attributes[1].name, "data-presolve-bindings");
3452        assert_eq!(root.attributes[1].span, None);
3453        assert_eq!(
3454            root.attributes[1].value,
3455            AttributeValue::BindingList(vec!["this.count".to_string()])
3456        );
3457
3458        assert_eq!(root.children.len(), 2);
3459        let TemplateChild::Text { value, span } = &root.children[0] else {
3460            panic!("expected text child");
3461        };
3462        assert_eq!(value, "Count:");
3463        assert_eq!(span.line, 13);
3464        assert_eq!(span.column, 9);
3465
3466        let TemplateChild::Binding {
3467            id,
3468            expression,
3469            initial_value,
3470            span,
3471        } = &root.children[1]
3472        else {
3473            panic!("expected binding child");
3474        };
3475        assert_eq!(id.0, "n1");
3476        assert_eq!(expression, "this.count");
3477        assert_eq!(
3478            initial_value,
3479            &Some(SerializableValue::Number("0".to_string()))
3480        );
3481        assert_eq!(span.line, 13);
3482        assert_eq!(span.column, 16);
3483    }
3484
3485    #[test]
3486    fn semantic_ids_are_stable_when_component_declaration_order_changes() {
3487        let alpha = r#"
3488@component("x-alpha")
3489class Alpha extends Component {
3490  count = state(0);
3491
3492  increment() {
3493    this.count++;
3494  }
3495
3496  render() {
3497    return <p>{this.count}</p>;
3498  }
3499}
3500"#;
3501        let beta = r#"
3502@component("x-beta")
3503class Beta extends Component {
3504  enabled = state(false);
3505
3506  toggle() {
3507    this.enabled = !this.enabled;
3508  }
3509
3510  render() {
3511    return <p>{this.enabled}</p>;
3512  }
3513}
3514"#;
3515
3516        let ids_for = |source: &str| {
3517            let parsed = presolve_parser::parse_file("App.tsx", source);
3518            let component_graph = build_component_graph(&parsed);
3519            let template_graph = build_template_graph(&component_graph);
3520            let template_ids = template_graph
3521                .templates
3522                .iter()
3523                .map(|template| (template.component_name.as_str(), template.id.to_string()))
3524                .collect::<BTreeMap<_, _>>();
3525
3526            component_graph
3527                .components
3528                .iter()
3529                .map(|component| {
3530                    let ids = component
3531                        .state_fields
3532                        .iter()
3533                        .map(|field| field.id.to_string())
3534                        .chain(component.methods.iter().map(|method| method.id.to_string()))
3535                        .chain(component.actions.iter().map(|action| action.id.to_string()))
3536                        .chain(std::iter::once(
3537                            template_ids
3538                                .get(component.class_name.as_str())
3539                                .expect("expected component template")
3540                                .clone(),
3541                        ))
3542                        .collect::<Vec<_>>();
3543
3544                    (
3545                        component.class_name.clone(),
3546                        (component.id.to_string(), ids),
3547                    )
3548                })
3549                .collect::<BTreeMap<_, _>>()
3550        };
3551
3552        assert_eq!(
3553            ids_for(&format!("{alpha}\n{beta}")),
3554            ids_for(&format!("{beta}\n{alpha}"))
3555        );
3556    }
3557
3558    #[test]
3559    fn carries_source_spans_into_nested_template_nodes() {
3560        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
3561
3562        let parsed =
3563            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
3564
3565        let component_graph = build_component_graph(&parsed);
3566        let template_graph = build_template_graph(&component_graph);
3567        let root = template_graph.templates[0]
3568            .root
3569            .as_ref()
3570            .expect("expected root");
3571
3572        assert_eq!(root.tag_name, "section");
3573        assert_eq!(root.span.line, 12);
3574        assert_eq!(root.span.column, 7);
3575
3576        let TemplateChild::Element(button) = &root.children[0] else {
3577            panic!("expected nested button");
3578        };
3579
3580        assert_eq!(button.tag_name, "button");
3581        assert_eq!(button.span.line, 13);
3582        assert_eq!(button.span.column, 9);
3583        assert_eq!(button.attributes[0].name, "data-presolve-on-click");
3584        assert_eq!(
3585            button.attributes[0].span.expect("expected event span").line,
3586            13
3587        );
3588        assert_eq!(
3589            button.attributes[0]
3590                .span
3591                .expect("expected event span")
3592                .column,
3593            17
3594        );
3595        assert_eq!(button.attributes[1].name, "data-presolve-bindings");
3596        assert_eq!(button.attributes[1].span, None);
3597
3598        let TemplateChild::Text { value, span } = &button.children[0] else {
3599            panic!("expected nested text");
3600        };
3601        assert_eq!(value, "Count: ");
3602        assert_eq!(span.line, 13);
3603        assert_eq!(span.column, 50);
3604
3605        let TemplateChild::Binding {
3606            expression, span, ..
3607        } = &button.children[1]
3608        else {
3609            panic!("expected nested binding");
3610        };
3611        assert_eq!(expression, "this.count");
3612        assert_eq!(span.line, 13);
3613        assert_eq!(span.column, 57);
3614    }
3615
3616    #[test]
3617    fn preserves_fragment_siblings_without_wrapper_elements() {
3618        let source = include_str!("../../../fixtures/0016-fragments/input/FragmentPanel.tsx");
3619
3620        let parsed =
3621            presolve_parser::parse_file("fixtures/0016-fragments/input/FragmentPanel.tsx", source);
3622
3623        let component_graph = build_component_graph(&parsed);
3624        assert!(component_graph.diagnostics.is_empty());
3625
3626        let template_graph = build_template_graph(&component_graph);
3627        let template = &template_graph.templates[0];
3628        let fragment = template
3629            .root_fragment
3630            .as_ref()
3631            .expect("expected fragment root");
3632
3633        assert!(template.root.is_none());
3634        assert_eq!(fragment.id.0, "n0");
3635        assert_eq!(fragment.children.len(), 2);
3636
3637        let TemplateChild::Element(heading) = &fragment.children[0] else {
3638            panic!("expected heading child");
3639        };
3640        assert_eq!(heading.id.0, "n1");
3641        assert_eq!(heading.tag_name, "h1");
3642
3643        let TemplateChild::Fragment(nested) = &fragment.children[1] else {
3644            panic!("expected nested fragment child");
3645        };
3646        assert_eq!(nested.id.0, "n2");
3647
3648        let TemplateChild::Element(paragraph) = &nested.children[0] else {
3649            panic!("expected paragraph child");
3650        };
3651        assert_eq!(paragraph.id.0, "n3");
3652        assert_eq!(paragraph.tag_name, "p");
3653
3654        let html = generate_static_html(&template_graph);
3655        assert_eq!(
3656            html,
3657            "<h1 data-presolve-node=\"n1\">Title</h1><p data-presolve-node=\"n3\" data-presolve-bindings=\"this.label\">Status: <!-- presolve-binding:n4:this.label -->Ready</p><span data-presolve-node=\"n5\">Done</span>\n"
3658        );
3659
3660        let manifest = build_template_manifest(&component_graph, &template_graph);
3661        assert_eq!(
3662            manifest.components[0].template.nodes,
3663            vec![
3664                ManifestNode::Element {
3665                    id: "n1".to_string(),
3666                    tag: "h1".to_string(),
3667                },
3668                ManifestNode::Element {
3669                    id: "n3".to_string(),
3670                    tag: "p".to_string(),
3671                },
3672                ManifestNode::Binding {
3673                    id: "n4".to_string(),
3674                    expression: "this.label".to_string(),
3675                    initial_value: Some(SerializableValue::String("Ready".to_string())),
3676                    target: None,
3677                    element: None,
3678                    attribute: None,
3679                },
3680                ManifestNode::Element {
3681                    id: "n5".to_string(),
3682                    tag: "span".to_string(),
3683                },
3684            ]
3685        );
3686    }
3687
3688    #[test]
3689    fn builds_conditional_template_boundaries_and_manifest_branch_html() {
3690        let source = include_str!(
3691            "../../../fixtures/0017-conditional-rendering/input/ConditionalStatus.tsx"
3692        );
3693
3694        let parsed = presolve_parser::parse_file(
3695            "fixtures/0017-conditional-rendering/input/ConditionalStatus.tsx",
3696            source,
3697        );
3698
3699        let component_graph = build_component_graph(&parsed);
3700        assert!(component_graph.diagnostics.is_empty());
3701
3702        let template_graph = build_template_graph(&component_graph);
3703        let root = template_graph.templates[0]
3704            .root
3705            .as_ref()
3706            .expect("expected root element");
3707
3708        assert_eq!(root.id.0, "n0");
3709        assert_eq!(root.attributes[1].name, "data-presolve-bindings");
3710
3711        let TemplateChild::Conditional(conditional) = &root.children[0] else {
3712            panic!("expected conditional child");
3713        };
3714
3715        assert_eq!(conditional.id.0, "n1");
3716        assert_eq!(conditional.start_id.0, "n2");
3717        assert_eq!(conditional.end_id.0, "n3");
3718        assert_eq!(conditional.condition, "this.enabled");
3719        assert_eq!(
3720            conditional.initial_value,
3721            Some(SerializableValue::Boolean(true))
3722        );
3723
3724        let TemplateChild::Element(when_true) = &conditional.when_true[0] else {
3725            panic!("expected true branch element");
3726        };
3727        assert_eq!(when_true.id.0, "n4");
3728        assert_eq!(when_true.tag_name, "span");
3729
3730        let TemplateChild::Element(when_false) = &conditional.when_false[0] else {
3731            panic!("expected false branch element");
3732        };
3733        assert_eq!(when_false.id.0, "n5");
3734        assert_eq!(when_false.tag_name, "span");
3735
3736        let html = generate_static_html(&template_graph);
3737        assert_eq!(
3738            html,
3739            "<button data-presolve-node=\"n0\" data-presolve-on-click=\"this.toggle\" data-presolve-bindings=\"this.enabled\"><!-- presolve-conditional-start:n2:this.enabled --><span data-presolve-node=\"n4\">On</span><!-- presolve-conditional-end:n3 --></button>\n"
3740        );
3741
3742        let manifest = build_template_manifest(&component_graph, &template_graph);
3743        assert_eq!(
3744            manifest.components[0].template.nodes,
3745            vec![
3746                ManifestNode::Element {
3747                    id: "n0".to_string(),
3748                    tag: "button".to_string(),
3749                },
3750                ManifestNode::Conditional {
3751                    id: "n1".to_string(),
3752                    start: "n2".to_string(),
3753                    end: "n3".to_string(),
3754                    condition: "this.enabled".to_string(),
3755                    initial_value: Some(SerializableValue::Boolean(true)),
3756                    when_true_html: "<span data-presolve-node=\"n4\">On</span>".to_string(),
3757                    when_false_html: "<span data-presolve-node=\"n5\">Off</span>".to_string(),
3758                },
3759            ]
3760        );
3761    }
3762
3763    #[test]
3764    fn builds_logical_and_conditional_with_empty_false_branch() {
3765        let source = include_str!(
3766            "../../../fixtures/0018-logical-and-conditional/input/LogicalAndStatus.tsx"
3767        );
3768
3769        let parsed = presolve_parser::parse_file(
3770            "fixtures/0018-logical-and-conditional/input/LogicalAndStatus.tsx",
3771            source,
3772        );
3773
3774        let component_graph = build_component_graph(&parsed);
3775        assert!(component_graph.diagnostics.is_empty());
3776
3777        let template_graph = build_template_graph(&component_graph);
3778        let root = template_graph.templates[0]
3779            .root
3780            .as_ref()
3781            .expect("expected root element");
3782
3783        let TemplateChild::Conditional(conditional) = &root.children[0] else {
3784            panic!("expected conditional child");
3785        };
3786
3787        assert_eq!(conditional.when_true.len(), 1);
3788        assert!(conditional.when_false.is_empty());
3789
3790        let manifest = build_template_manifest(&component_graph, &template_graph);
3791        assert_eq!(
3792            manifest.components[0].template.nodes[1],
3793            ManifestNode::Conditional {
3794                id: "n1".to_string(),
3795                start: "n2".to_string(),
3796                end: "n3".to_string(),
3797                condition: "this.enabled".to_string(),
3798                initial_value: Some(SerializableValue::Boolean(true)),
3799                when_true_html: "<span data-presolve-node=\"n4\">On</span>".to_string(),
3800                when_false_html: String::new(),
3801            }
3802        );
3803    }
3804
3805    #[test]
3806    fn builds_empty_keyed_list_from_a_serializable_array() {
3807        let source =
3808            include_str!("../../../fixtures/0019-keyed-list-semantics/input/KeyedList.tsx");
3809
3810        let parsed = presolve_parser::parse_file(
3811            "fixtures/0019-keyed-list-semantics/input/KeyedList.tsx",
3812            source,
3813        );
3814
3815        let component_graph = build_component_graph(&parsed);
3816        assert!(component_graph.diagnostics.is_empty());
3817
3818        let template_graph = build_template_graph(&component_graph);
3819        let root = template_graph.templates[0]
3820            .root
3821            .as_ref()
3822            .expect("expected root element");
3823
3824        assert_eq!(root.id.0, "n0");
3825        assert_eq!(root.attributes[0].name, "data-presolve-bindings");
3826
3827        let TemplateChild::List(list) = &root.children[0] else {
3828            panic!("expected keyed list child");
3829        };
3830
3831        assert_eq!(list.id.0, "n1");
3832        assert_eq!(list.start_id.0, "n2");
3833        assert_eq!(list.end_id.0, "n3");
3834        assert_eq!(list.iterable, "this.items");
3835        assert_eq!(
3836            list.initial_value,
3837            Some(SerializableValue::Array(Vec::new()))
3838        );
3839        assert_eq!(list.item_variable, "item");
3840        assert_eq!(list.index_variable.as_deref(), Some("index"));
3841        assert_eq!(list.key_expression, "item.id");
3842
3843        let TemplateChild::Element(item_template) = &list.item_template[0] else {
3844            panic!("expected list item element");
3845        };
3846        assert_eq!(item_template.id.0, "n4");
3847        assert_eq!(item_template.tag_name, "li");
3848
3849        assert_eq!(
3850            generate_static_html(&template_graph),
3851            "<ul data-presolve-node=\"n0\" data-presolve-bindings=\"this.items\"><!-- presolve-list-start:n2:this.items --><!-- presolve-list-end:n3 --></ul>\n"
3852        );
3853
3854        let manifest = build_template_manifest(&component_graph, &template_graph);
3855        assert_eq!(
3856            manifest.components[0].template.nodes,
3857            vec![
3858                ManifestNode::Element {
3859                    id: "n0".to_string(),
3860                    tag: "ul".to_string(),
3861                },
3862                ManifestNode::List {
3863                    id: "n1".to_string(),
3864                    start: "n2".to_string(),
3865                    end: "n3".to_string(),
3866                    iterable: "this.items".to_string(),
3867                    initial_value: Some(SerializableValue::Array(Vec::new())),
3868                    item_variable: "item".to_string(),
3869                    index_variable: Some("index".to_string()),
3870                    key_expression: "item.id".to_string(),
3871                    item_root: "n4".to_string(),
3872                    item_template_html: "<li data-presolve-node=\"n4:__ez_list_key__\" data-presolve-bindings=\"index,item.label\"><!-- presolve-binding:n5:__ez_list_key__:index -->__ez_list_index__<!-- presolve-list-binding-end:n5:__ez_list_key__ -->: <!-- presolve-binding:n6:__ez_list_key__:item.label --><!-- presolve-list-binding-end:n6:__ez_list_key__ --></li>".to_string(),
3873                },
3874            ]
3875        );
3876    }
3877
3878    #[test]
3879    fn preserves_recursive_object_values_in_component_and_manifest_models() {
3880        let source = include_str!(
3881            "../../../fixtures/0023-recursive-object-values/input/RecursiveObjectValues.tsx"
3882        );
3883        let parsed = presolve_parser::parse_file(
3884            "fixtures/0023-recursive-object-values/input/RecursiveObjectValues.tsx",
3885            source,
3886        );
3887
3888        let component_graph = build_component_graph(&parsed);
3889        assert!(component_graph.diagnostics.is_empty());
3890
3891        let profile = SerializableValue::Object(BTreeMap::from([
3892            (
3893                "name".to_string(),
3894                SerializableValue::String("North".to_string()),
3895            ),
3896            (
3897                "settings".to_string(),
3898                SerializableValue::Object(BTreeMap::from([
3899                    ("enabled".to_string(), SerializableValue::Boolean(true)),
3900                    (
3901                        "tags".to_string(),
3902                        SerializableValue::Array(vec![
3903                            SerializableValue::String("compiler".to_string()),
3904                            SerializableValue::Object(BTreeMap::from([
3905                                (
3906                                    "name".to_string(),
3907                                    SerializableValue::String("runtime".to_string()),
3908                                ),
3909                                (
3910                                    "rank".to_string(),
3911                                    SerializableValue::Number("2".to_string()),
3912                                ),
3913                            ])),
3914                        ]),
3915                    ),
3916                ])),
3917            ),
3918        ]));
3919        assert_eq!(
3920            component_graph.components[0].state_fields[0].initial_value,
3921            Some(profile.clone())
3922        );
3923
3924        let template_graph = build_template_graph(&component_graph);
3925        let manifest = build_template_manifest(&component_graph, &template_graph);
3926        assert_eq!(
3927            manifest.components[0].template.nodes[3],
3928            ManifestNode::Binding {
3929                id: "n3".to_string(),
3930                expression: "this.profile".to_string(),
3931                initial_value: Some(profile),
3932                target: None,
3933                element: None,
3934                attribute: None,
3935            }
3936        );
3937        assert!(matches!(
3938            manifest.components[0].actions[0].operand,
3939            Some(SerializableValue::Object(_))
3940        ));
3941    }
3942
3943    #[test]
3944    fn renders_static_object_list_members_and_keys() {
3945        let source = include_str!(
3946            "../../../fixtures/0024-static-object-keyed-list/input/StaticObjectKeyedList.tsx"
3947        );
3948        let parsed = presolve_parser::parse_file(
3949            "fixtures/0024-static-object-keyed-list/input/StaticObjectKeyedList.tsx",
3950            source,
3951        );
3952        let component_graph = build_component_graph(&parsed);
3953        assert!(component_graph.diagnostics.is_empty());
3954
3955        let template_graph = build_template_graph(&component_graph);
3956        assert_eq!(
3957            generate_static_html(&template_graph),
3958            "<ol data-presolve-node=\"n0\" data-presolve-bindings=\"this.items\"><!-- presolve-list-start:n2:this.items --><li data-presolve-node=\"n4:north\" data-presolve-bindings=\"index,item.label,item.details.region\"><!-- presolve-binding:n5:north:index -->0<!-- presolve-list-binding-end:n5:north -->: <!-- presolve-binding:n6:north:item.label -->North<!-- presolve-list-binding-end:n6:north --> (<!-- presolve-binding:n7:north:item.details.region -->west<!-- presolve-list-binding-end:n7:north -->)</li><li data-presolve-node=\"n4:south\" data-presolve-bindings=\"index,item.label,item.details.region\"><!-- presolve-binding:n5:south:index -->1<!-- presolve-list-binding-end:n5:south -->: <!-- presolve-binding:n6:south:item.label -->South<!-- presolve-list-binding-end:n6:south --> (<!-- presolve-binding:n7:south:item.details.region -->east<!-- presolve-list-binding-end:n7:south -->)</li><!-- presolve-list-end:n3 --></ol>\n"
3959        );
3960    }
3961
3962    #[test]
3963    fn renders_initial_keyed_list_items_from_a_serializable_array() {
3964        let source =
3965            include_str!("../../../fixtures/0020-static-keyed-list/input/StaticKeyedList.tsx");
3966
3967        let parsed = presolve_parser::parse_file(
3968            "fixtures/0020-static-keyed-list/input/StaticKeyedList.tsx",
3969            source,
3970        );
3971        let component_graph = build_component_graph(&parsed);
3972        assert!(component_graph.diagnostics.is_empty());
3973
3974        let template_graph = build_template_graph(&component_graph);
3975        let root = template_graph.templates[0]
3976            .root
3977            .as_ref()
3978            .expect("expected root element");
3979        let TemplateChild::List(list) = &root.children[0] else {
3980            panic!("expected keyed list child");
3981        };
3982
3983        assert_eq!(
3984            list.initial_value,
3985            Some(SerializableValue::Array(vec![
3986                SerializableValue::String("North".to_string()),
3987                SerializableValue::String("South".to_string()),
3988            ]))
3989        );
3990
3991        assert_eq!(
3992            generate_static_html(&template_graph),
3993            "<ol data-presolve-node=\"n0\" data-presolve-bindings=\"this.labels\"><!-- presolve-list-start:n2:this.labels --><li data-presolve-node=\"n4:North\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:North:index -->0<!-- presolve-list-binding-end:n5:North -->: <!-- presolve-binding:n6:North:label -->North<!-- presolve-list-binding-end:n6:North --></li><li data-presolve-node=\"n4:South\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:South:index -->1<!-- presolve-list-binding-end:n5:South -->: <!-- presolve-binding:n6:South:label -->South<!-- presolve-list-binding-end:n6:South --></li><!-- presolve-list-end:n3 --></ol>\n"
3994        );
3995
3996        let manifest = build_template_manifest(&component_graph, &template_graph);
3997        assert_eq!(
3998            manifest.components[0].template.nodes,
3999            vec![
4000                ManifestNode::Element {
4001                    id: "n0".to_string(),
4002                    tag: "ol".to_string(),
4003                },
4004                ManifestNode::List {
4005                    id: "n1".to_string(),
4006                    start: "n2".to_string(),
4007                    end: "n3".to_string(),
4008                    iterable: "this.labels".to_string(),
4009                    initial_value: Some(SerializableValue::Array(vec![
4010                        SerializableValue::String("North".to_string()),
4011                        SerializableValue::String("South".to_string()),
4012                    ])),
4013                    item_variable: "label".to_string(),
4014                    index_variable: Some("index".to_string()),
4015                    key_expression: "label".to_string(),
4016                    item_root: "n4".to_string(),
4017                    item_template_html: "<li data-presolve-node=\"n4:__ez_list_key__\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:__ez_list_key__:index -->__ez_list_index__<!-- presolve-list-binding-end:n5:__ez_list_key__ -->: <!-- presolve-binding:n6:__ez_list_key__:label -->__ez_list_item__<!-- presolve-list-binding-end:n6:__ez_list_key__ --></li>".to_string(),
4018                },
4019            ]
4020        );
4021    }
4022
4023    #[test]
4024    fn builds_template_manifest_for_nested_jsx() {
4025        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
4026
4027        let parsed =
4028            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
4029
4030        let component_graph = build_component_graph(&parsed);
4031        let template_graph = build_template_graph(&component_graph);
4032        let manifest = build_template_manifest(&component_graph, &template_graph);
4033
4034        assert_eq!(manifest.schema_version, 1);
4035        assert_eq!(manifest.components.len(), 1);
4036
4037        let component = &manifest.components[0];
4038        assert_eq!(component.name, "NestedCounter");
4039
4040        assert_eq!(
4041            component.template.nodes,
4042            vec![
4043                ManifestNode::Element {
4044                    id: "n0".to_string(),
4045                    tag: "section".to_string(),
4046                },
4047                ManifestNode::Element {
4048                    id: "n1".to_string(),
4049                    tag: "button".to_string(),
4050                },
4051                ManifestNode::Binding {
4052                    id: "n2".to_string(),
4053                    expression: "this.count".to_string(),
4054                    initial_value: Some(SerializableValue::Number("0".to_string())),
4055                    target: None,
4056                    element: None,
4057                    attribute: None,
4058                }
4059            ]
4060        );
4061
4062        assert_eq!(
4063            component.template.events,
4064            vec![ManifestEvent {
4065                node: "n1".to_string(),
4066                kind: None,
4067                event: "click".to_string(),
4068                handler: "this.increment".to_string(),
4069                arguments: Vec::new(),
4070                method_id: None,
4071                action_batch_id: None,
4072            }]
4073        );
4074
4075        assert_eq!(
4076            component.actions,
4077            vec![ManifestAction {
4078                method: "increment".to_string(),
4079                method_id: None,
4080                action_batch_id: None,
4081                operation: ManifestOperation::Increment,
4082                field: "count".to_string(),
4083                storage_id: None,
4084                operand: None,
4085            }]
4086        );
4087
4088        let manifest_json = template_manifest_json(&manifest);
4089        let manifest_value: serde_json::Value =
4090            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4091
4092        assert_eq!(manifest_value["schema_version"], serde_json::json!(1));
4093    }
4094
4095    #[test]
4096    fn builds_template_manifest_for_decrement_action() {
4097        let source =
4098            include_str!("../../../fixtures/0009-decrement-counter/input/DecrementCounter.tsx");
4099
4100        let parsed = presolve_parser::parse_file(
4101            "fixtures/0009-decrement-counter/input/DecrementCounter.tsx",
4102            source,
4103        );
4104
4105        let component_graph = build_component_graph(&parsed);
4106        let template_graph = build_template_graph(&component_graph);
4107        let manifest = build_template_manifest(&component_graph, &template_graph);
4108
4109        assert_eq!(
4110            manifest.components[0].actions,
4111            vec![ManifestAction {
4112                method: "decrement".to_string(),
4113                method_id: None,
4114                action_batch_id: None,
4115                operation: ManifestOperation::Decrement,
4116                field: "count".to_string(),
4117                storage_id: None,
4118                operand: None,
4119            }]
4120        );
4121
4122        let manifest_json = template_manifest_json(&manifest);
4123        let manifest_value: serde_json::Value =
4124            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4125
4126        assert_eq!(
4127            manifest_value["components"][0]["actions"][0]["operation"],
4128            serde_json::json!("decrement")
4129        );
4130    }
4131
4132    #[test]
4133    fn builds_template_manifest_for_add_and_subtract_assign_actions() {
4134        let source =
4135            include_str!("../../../fixtures/0010-add-subtract-assign/input/StepCounter.tsx");
4136
4137        let parsed = presolve_parser::parse_file(
4138            "fixtures/0010-add-subtract-assign/input/StepCounter.tsx",
4139            source,
4140        );
4141
4142        let component_graph = build_component_graph(&parsed);
4143        let template_graph = build_template_graph(&component_graph);
4144        let manifest = build_template_manifest(&component_graph, &template_graph);
4145
4146        assert_eq!(
4147            manifest.components[0].actions,
4148            vec![
4149                ManifestAction {
4150                    method: "addTwo".to_string(),
4151                    method_id: None,
4152                    action_batch_id: None,
4153                    operation: ManifestOperation::AddAssign,
4154                    field: "count".to_string(),
4155                    storage_id: None,
4156                    operand: Some(SerializableValue::Number("2".to_string())),
4157                },
4158                ManifestAction {
4159                    method: "subtractThree".to_string(),
4160                    method_id: None,
4161                    action_batch_id: None,
4162                    operation: ManifestOperation::SubtractAssign,
4163                    field: "count".to_string(),
4164                    storage_id: None,
4165                    operand: Some(SerializableValue::Number("3".to_string())),
4166                }
4167            ]
4168        );
4169
4170        let manifest_json = template_manifest_json(&manifest);
4171        let manifest_value: serde_json::Value =
4172            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4173
4174        assert_eq!(
4175            manifest_value["components"][0]["actions"][0]["operation"],
4176            serde_json::json!("add_assign")
4177        );
4178        assert_eq!(
4179            manifest_value["components"][0]["actions"][0]["operand"],
4180            serde_json::json!("2")
4181        );
4182        assert_eq!(
4183            manifest_value["components"][0]["actions"][1]["operation"],
4184            serde_json::json!("subtract_assign")
4185        );
4186        assert_eq!(
4187            manifest_value["components"][0]["actions"][1]["operand"],
4188            serde_json::json!("3")
4189        );
4190    }
4191
4192    #[test]
4193    fn builds_template_manifest_for_direct_assignment_action() {
4194        let source =
4195            include_str!("../../../fixtures/0011-direct-assignment/input/ResetCounter.tsx");
4196
4197        let parsed = presolve_parser::parse_file(
4198            "fixtures/0011-direct-assignment/input/ResetCounter.tsx",
4199            source,
4200        );
4201
4202        let component_graph = build_component_graph(&parsed);
4203        let template_graph = build_template_graph(&component_graph);
4204        let manifest = build_template_manifest(&component_graph, &template_graph);
4205
4206        assert_eq!(
4207            manifest.components[0].actions,
4208            vec![ManifestAction {
4209                method: "reset".to_string(),
4210                method_id: None,
4211                action_batch_id: None,
4212                operation: ManifestOperation::Assign,
4213                field: "count".to_string(),
4214                storage_id: None,
4215                operand: Some(SerializableValue::Number("0".to_string())),
4216            }]
4217        );
4218
4219        let manifest_json = template_manifest_json(&manifest);
4220        let manifest_value: serde_json::Value =
4221            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4222
4223        assert_eq!(
4224            manifest_value["components"][0]["actions"][0]["operation"],
4225            serde_json::json!("assign")
4226        );
4227        assert_eq!(
4228            manifest_value["components"][0]["actions"][0]["operand"],
4229            serde_json::json!("0")
4230        );
4231    }
4232
4233    #[test]
4234    fn builds_template_manifest_for_boolean_toggle_action() {
4235        let source = include_str!("../../../fixtures/0012-boolean-toggle/input/ToggleFlag.tsx");
4236
4237        let parsed = presolve_parser::parse_file(
4238            "fixtures/0012-boolean-toggle/input/ToggleFlag.tsx",
4239            source,
4240        );
4241
4242        let component_graph = build_component_graph(&parsed);
4243        let template_graph = build_template_graph(&component_graph);
4244        let manifest = build_template_manifest(&component_graph, &template_graph);
4245
4246        assert_eq!(
4247            manifest.components[0].actions,
4248            vec![ManifestAction {
4249                method: "toggle".to_string(),
4250                method_id: None,
4251                action_batch_id: None,
4252                operation: ManifestOperation::Toggle,
4253                field: "enabled".to_string(),
4254                storage_id: None,
4255                operand: None,
4256            }]
4257        );
4258
4259        let manifest_json = template_manifest_json(&manifest);
4260        let manifest_value: serde_json::Value =
4261            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4262
4263        assert_eq!(
4264            manifest_value["components"][0]["actions"][0]["operation"],
4265            serde_json::json!("toggle")
4266        );
4267        assert!(manifest_value["components"][0]["actions"][0]
4268            .get("operand")
4269            .is_none());
4270    }
4271
4272    #[test]
4273    fn builds_template_manifest_for_multi_step_action_in_source_order() {
4274        let source =
4275            include_str!("../../../fixtures/0013-multi-step-action/input/BatchActionCounter.tsx");
4276
4277        let parsed = presolve_parser::parse_file(
4278            "fixtures/0013-multi-step-action/input/BatchActionCounter.tsx",
4279            source,
4280        );
4281
4282        let component_graph = build_component_graph(&parsed);
4283        let template_graph = build_template_graph(&component_graph);
4284        let manifest = build_template_manifest(&component_graph, &template_graph);
4285
4286        assert_eq!(
4287            manifest.components[0].actions,
4288            vec![
4289                ManifestAction {
4290                    method: "apply".to_string(),
4291                    method_id: None,
4292                    action_batch_id: None,
4293                    operation: ManifestOperation::AddAssign,
4294                    field: "count".to_string(),
4295                    storage_id: None,
4296                    operand: Some(SerializableValue::Number("2".to_string())),
4297                },
4298                ManifestAction {
4299                    method: "apply".to_string(),
4300                    method_id: None,
4301                    action_batch_id: None,
4302                    operation: ManifestOperation::Decrement,
4303                    field: "count".to_string(),
4304                    storage_id: None,
4305                    operand: None,
4306                },
4307                ManifestAction {
4308                    method: "apply".to_string(),
4309                    method_id: None,
4310                    action_batch_id: None,
4311                    operation: ManifestOperation::Assign,
4312                    field: "count".to_string(),
4313                    storage_id: None,
4314                    operand: Some(SerializableValue::Number("8".to_string())),
4315                },
4316                ManifestAction {
4317                    method: "apply".to_string(),
4318                    method_id: None,
4319                    action_batch_id: None,
4320                    operation: ManifestOperation::Increment,
4321                    field: "count".to_string(),
4322                    storage_id: None,
4323                    operand: None,
4324                },
4325                ManifestAction {
4326                    method: "apply".to_string(),
4327                    method_id: None,
4328                    action_batch_id: None,
4329                    operation: ManifestOperation::Toggle,
4330                    field: "enabled".to_string(),
4331                    storage_id: None,
4332                    operand: None,
4333                }
4334            ]
4335        );
4336
4337        let manifest_json = template_manifest_json(&manifest);
4338        let manifest_value: serde_json::Value =
4339            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4340
4341        let actions = manifest_value["components"][0]["actions"]
4342            .as_array()
4343            .expect("manifest actions should be an array");
4344
4345        assert_eq!(
4346            actions
4347                .iter()
4348                .map(|action| action["operation"].as_str().expect("operation is a string"))
4349                .collect::<Vec<_>>(),
4350            vec!["add_assign", "decrement", "assign", "increment", "toggle"]
4351        );
4352        assert_eq!(actions[0]["method"], serde_json::json!("apply"));
4353        assert_eq!(actions[4]["method"], serde_json::json!("apply"));
4354        assert!(actions[1].get("operand").is_none());
4355        assert!(actions[3].get("operand").is_none());
4356        assert!(actions[4].get("operand").is_none());
4357    }
4358
4359    #[test]
4360    fn builds_template_manifest_for_dynamic_attribute_bindings() {
4361        let source = include_str!(
4362            "../../../fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx"
4363        );
4364
4365        let parsed = presolve_parser::parse_file(
4366            "fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx",
4367            source,
4368        );
4369
4370        let component_graph = build_component_graph(&parsed);
4371        let template_graph = build_template_graph(&component_graph);
4372        let manifest = build_template_manifest(&component_graph, &template_graph);
4373        let manifest_json = template_manifest_json(&manifest);
4374        let manifest_value: serde_json::Value =
4375            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4376
4377        assert_eq!(
4378            manifest.components[0].template.nodes[1],
4379            ManifestNode::Binding {
4380                id: "n1".to_string(),
4381                expression: "this.disabled".to_string(),
4382                initial_value: Some(SerializableValue::Boolean(false)),
4383                target: Some(ManifestBindingTarget::Attribute),
4384                element: Some("n0".to_string()),
4385                attribute: Some("disabled".to_string()),
4386            }
4387        );
4388        assert_eq!(
4389            manifest_value["components"][0]["template"]["nodes"][1]["target"],
4390            serde_json::json!("attribute")
4391        );
4392        assert_eq!(
4393            manifest_value["components"][0]["template"]["nodes"][1]["element"],
4394            serde_json::json!("n0")
4395        );
4396        assert_eq!(
4397            manifest_value["components"][0]["template"]["nodes"][1]["attribute"],
4398            serde_json::json!("disabled")
4399        );
4400        assert!(manifest_value["components"][0]["template"]["nodes"][3]
4401            .get("target")
4402            .is_none());
4403    }
4404
4405    #[test]
4406    fn generates_standalone_page_with_embedded_manifest() {
4407        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
4408
4409        let parsed =
4410            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
4411
4412        let component_graph = build_component_graph(&parsed);
4413        let template_graph = build_template_graph(&component_graph);
4414        let html = generate_static_html(&template_graph);
4415        let manifest = build_template_manifest(&component_graph, &template_graph);
4416        let page = generate_standalone_page("NestedCounter", &html, &manifest);
4417
4418        assert!(page.starts_with("<!doctype html>\n"));
4419        assert!(page.contains("<title>NestedCounter</title>"));
4420        assert!(page.contains("<section data-presolve-node=\"n0\">"));
4421        assert!(page.contains("id=\"presolve-template-manifest\""));
4422        assert!(page.contains("\"name\": \"NestedCounter\""));
4423        assert!(page.contains("<script src=\"./runtime.js\" defer></script>"));
4424    }
4425}