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