From ae31e4bf18c68b6e7153a4b675dc4a41d57ce898 Mon Sep 17 00:00:00 2001
From: Martijn Laarman <Mpdreamz@gmail.com>
Date: Mon, 18 May 2026 12:01:08 +0200
Subject: [PATCH 1/6] Merge Elastic.Documentation.Api.* into single project
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Three projects (Api.Core, Api.Infrastructure, Api.App) collapsed into
Elastic.Documentation.Api. Mcp.Remote no longer references any Api.*
project — its only deps are ServiceDefaults, services/Search, and
services/Assembler.

Key moves:
- EuidSpanProcessor + EuidLogProcessor → ServiceDefaults (shared)
- AddEuidEnrichment helper added to ServiceDefaults
- Search gateway interfaces (IFullSearchGateway, INavigationSearchGateway,
  IChangesGateway) → services/Elastic.Documentation.Search
- McpToolSourceName constant inlined into Mcp.Remote

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---
 aspire/AppHost.cs                             |  2 +-
 aspire/aspire.csproj                          |  2 +-
 docs-builder.slnx                             |  4 +-
 ...astic.Documentation.ServiceDefaults.csproj |  1 +
 .../Logging}/EuidLogProcessor.cs              |  4 +-
 .../Telemetry/EuidEnrichmentExtensions.cs}    | 93 ++++++------------
 .../Telemetry}/EuidSpanProcessor.cs           |  3 +-
 .../Telemetry/TelemetryConstants.cs           |  5 +
 .../Elastic.Documentation.Api.App.csproj      | 33 -------
 .../Elastic.Documentation.Api.Core.csproj     | 18 ----
 ...ic.Documentation.Api.Infrastructure.csproj | 30 ------
 .../AskAi/AgentBuilderAskAiGateway.cs         |  4 +-
 .../AskAi/AgentBuilderStreamTransformer.cs    |  4 +-
 .../Adapters/AskAi/AskAiGatewayFactory.cs     |  4 +-
 .../Adapters/AskAi/AskAiProviderResolver.cs   |  2 +-
 ...lasticsearchAskAiMessageFeedbackGateway.cs |  6 +-
 .../Adapters/AskAi/KibanaOptions.cs           |  2 +-
 .../Adapters/AskAi/LlmGatewayAskAiGateway.cs  |  6 +-
 .../Adapters/AskAi/LlmGatewayOptions.cs       |  2 +-
 .../AskAi/LlmGatewayStreamTransformer.cs      |  4 +-
 .../Adapters/AskAi/SseParser.cs               |  2 +-
 .../Adapters/AskAi/StreamTransformerBase.cs   |  6 +-
 .../AskAi/StreamTransformerFactory.cs         |  4 +-
 .../Adapters/Telemetry/AdotOtlpGateway.cs     |  4 +-
 .../AskAi/AskAiEvent.cs                       |  2 +-
 .../AskAi/AskAiMessageFeedbackRequest.cs      |  2 +-
 .../AskAi/AskAiMessageFeedbackUsecase.cs      |  2 +-
 .../AskAi/AskAiUsecase.cs                     |  2 +-
 .../AskAi/IAskAiGateway.cs                    |  2 +-
 .../AskAi/IAskAiMessageFeedbackGateway.cs     |  2 +-
 .../AskAi/IStreamTransformer.cs               |  2 +-
 .../Aws/IParameterProvider.cs                 |  2 +-
 .../Aws/LambdaExtensionParameterProvider.cs   |  2 +-
 .../Aws/LocalParameterProvider.cs             |  2 +-
 .../Caching/CacheKey.cs                       |  2 +-
 .../Caching/DynamoDbDistributedCache.cs       |  4 +-
 .../Caching/IDistributedCache.cs              |  2 +-
 .../Caching/InMemoryDistributedCache.cs       |  2 +-
 .../Caching/MultiLayerCache.cs                |  4 +-
 .../Changes/ChangesUsecase.cs                 | 11 +--
 .../Dockerfile                                |  4 +-
 .../Elastic.Documentation.Api.csproj          | 42 ++++++++
 .../Gcp/GcpIdTokenProvider.cs                 |  4 +-
 .../Gcp/IGcpIdTokenProvider.cs                |  2 +-
 .../MappingsExtensions.cs                     | 11 ++-
 .../OpenTelemetry/OpenTelemetryExtensions.cs  | 96 +++++++++++++++++++
 .../Program.cs                                |  4 +-
 .../Properties/launchSettings.json            |  0
 .../Search/FullSearchUsecase.cs               |  3 +-
 .../Search/NavigationSearchUsecase.cs         | 18 +---
 .../SerializationContext.cs                   |  9 +-
 .../ServicesExtension.cs                      | 26 +++--
 .../Telemetry/IOtlpGateway.cs                 |  2 +-
 .../Telemetry/OtlpForwardResult.cs            |  2 +-
 .../Telemetry/OtlpProxyOptions.cs             |  6 +-
 .../Telemetry/OtlpProxyRequest.cs             |  2 +-
 .../Telemetry/OtlpProxyUsecase.cs             |  2 +-
 .../TelemetryConstants.cs                     | 19 ++--
 .../Validation/IValidator.cs                  |  2 +-
 .../appsettings.Development.json}             |  0
 .../appsettings.edge.json                     |  0
 .../appsettings.json                          |  0
 .../Elastic.Documentation.Mcp.Remote.csproj   |  2 -
 .../Program.cs                                |  4 +-
 .../Telemetry/McpToolTelemetry.cs             |  4 +-
 .../Tools/CoherenceTools.cs                   |  2 +-
 .../Tools/SearchTools.cs                      |  2 +-
 .../ChangesGateway.cs                         |  1 -
 .../Elastic.Documentation.Search.csproj       |  1 -
 .../FullSearchGateway.cs                      |  1 -
 .../IChangesGateway.cs                        | 10 +-
 .../IFullSearchGateway.cs                     |  2 +-
 .../INavigationSearchGateway.cs               | 18 +++-
 .../MockSearchGateway.cs                      |  2 -
 .../NavigationSearchGateway.cs                |  1 -
 .../ServicesExtension.cs                      |  7 +-
 .../docs-builder/Http/DocumentationWebHost.cs |  2 +-
 .../docs-builder/Http/StaticWebHost.cs        |  2 +-
 src/tooling/docs-builder/docs-builder.csproj  |  3 +-
 .../Elastic.Assembler.IntegrationTests.csproj |  3 +-
 .../Search/SearchIntegrationTests.cs          |  2 +-
 .../AskAiGatewayStreamingTests.cs             |  6 +-
 ....Documentation.Api.IntegrationTests.csproj |  4 +-
 .../EuidEnrichmentIntegrationTests.cs         |  4 +-
 .../Fixtures/ApiWebApplicationFactory.cs      |  6 +-
 .../OtlpProxyIntegrationTests.cs              |  2 +-
 .../Mcp.Remote.IntegrationTests.csproj        |  1 -
 .../McpToolsIntegrationTestsBase.cs           |  3 +-
 .../Search.IntegrationTests.csproj            |  3 +-
 .../Adapters/AskAi/StreamTransformerTests.cs  |  4 +-
 .../Caching/DistributedCacheTests.cs          |  4 +-
 ...umentation.Api.Infrastructure.Tests.csproj |  3 +-
 92 files changed, 335 insertions(+), 318 deletions(-)
 rename src/{api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry => Elastic.Documentation.ServiceDefaults/Logging}/EuidLogProcessor.cs (92%)
 rename src/{api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs => Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs} (51%)
 rename src/{api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry => Elastic.Documentation.ServiceDefaults/Telemetry}/EuidSpanProcessor.cs (91%)
 delete mode 100644 src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj
 delete mode 100644 src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj
 delete mode 100644 src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/AgentBuilderAskAiGateway.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/AgentBuilderStreamTransformer.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/AskAiGatewayFactory.cs (91%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/AskAiProviderResolver.cs (95%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/KibanaOptions.cs (90%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/LlmGatewayAskAiGateway.cs (95%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/LlmGatewayOptions.cs (95%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/LlmGatewayStreamTransformer.cs (98%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/SseParser.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/StreamTransformerBase.cs (98%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/AskAi/StreamTransformerFactory.cs (94%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Adapters/Telemetry/AdotOtlpGateway.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/AskAiEvent.cs (98%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/AskAiMessageFeedbackRequest.cs (94%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/AskAiMessageFeedbackUsecase.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/AskAiUsecase.cs (98%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/IAskAiGateway.cs (95%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/IAskAiMessageFeedbackGateway.cs (95%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/AskAi/IStreamTransformer.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Aws/IParameterProvider.cs (90%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Aws/LambdaExtensionParameterProvider.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Aws/LocalParameterProvider.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Caching/CacheKey.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Caching/DynamoDbDistributedCache.cs (98%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Caching/IDistributedCache.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Caching/InMemoryDistributedCache.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Caching/MultiLayerCache.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Changes/ChangesUsecase.cs (91%)
 rename src/api/{Elastic.Documentation.Api.App => Elastic.Documentation.Api}/Dockerfile (90%)
 create mode 100644 src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Gcp/GcpIdTokenProvider.cs (98%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/Gcp/IGcpIdTokenProvider.cs (93%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/MappingsExtensions.cs (95%)
 create mode 100644 src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs
 rename src/api/{Elastic.Documentation.Api.App => Elastic.Documentation.Api}/Program.cs (97%)
 rename src/api/{Elastic.Documentation.Api.App => Elastic.Documentation.Api}/Properties/launchSettings.json (100%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Search/FullSearchUsecase.cs (97%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Search/NavigationSearchUsecase.cs (82%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/SerializationContext.cs (87%)
 rename src/api/{Elastic.Documentation.Api.Infrastructure => Elastic.Documentation.Api}/ServicesExtension.cs (91%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Telemetry/IOtlpGateway.cs (94%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Telemetry/OtlpForwardResult.cs (93%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Telemetry/OtlpProxyOptions.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Telemetry/OtlpProxyRequest.cs (95%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Telemetry/OtlpProxyUsecase.cs (96%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/TelemetryConstants.cs (82%)
 rename src/api/{Elastic.Documentation.Api.Core => Elastic.Documentation.Api}/Validation/IValidator.cs (86%)
 rename src/api/{Elastic.Documentation.Api.App/appsettings.development.json => Elastic.Documentation.Api/appsettings.Development.json} (100%)
 rename src/api/{Elastic.Documentation.Api.App => Elastic.Documentation.Api}/appsettings.edge.json (100%)
 rename src/api/{Elastic.Documentation.Api.App => Elastic.Documentation.Api}/appsettings.json (100%)
 rename src/{api/Elastic.Documentation.Api.Core/Changes => services/Elastic.Documentation.Search}/IChangesGateway.cs (81%)
 rename src/{api/Elastic.Documentation.Api.Core/Search => services/Elastic.Documentation.Search}/IFullSearchGateway.cs (98%)
 rename src/{api/Elastic.Documentation.Api.Core/Search => services/Elastic.Documentation.Search}/INavigationSearchGateway.cs (56%)

diff --git a/aspire/AppHost.cs b/aspire/AppHost.cs
index 6287569def..e89cdc29f1 100644
--- a/aspire/AppHost.cs
+++ b/aspire/AppHost.cs
@@ -61,7 +61,7 @@ internal static async Task Run(
 
 		var elasticsearchRemote = builder.AddExternalService(ElasticsearchRemote, elasticsearchUrl);
 
-		var api = builder.AddProject<Projects.Elastic_Documentation_Api_App>(Api)
+		var api = builder.AddProject<Projects.Elastic_Documentation_Api>(Api)
 			.WithArgs(GlobalArguments)
 			.WithEnvironment("ENVIRONMENT", "dev")
 			.WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl)
diff --git a/aspire/aspire.csproj b/aspire/aspire.csproj
index 5c1073d0e9..5bc3691a3f 100644
--- a/aspire/aspire.csproj
+++ b/aspire/aspire.csproj
@@ -20,7 +20,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\src\api\Elastic.Documentation.Api.App\Elastic.Documentation.Api.App.csproj" />
+    <ProjectReference Include="..\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
     <ProjectReference Include="..\src\api\Elastic.Documentation.Mcp.Remote\Elastic.Documentation.Mcp.Remote.csproj" />
     <ProjectReference Include="..\src\tooling\docs-builder\docs-builder.csproj"/>
     <ProjectReference Include="..\src\Elastic.Documentation\Elastic.Documentation.csproj" IsAspireProjectResource="false" />
diff --git a/docs-builder.slnx b/docs-builder.slnx
index acbab48b36..f5a36e725b 100644
--- a/docs-builder.slnx
+++ b/docs-builder.slnx
@@ -56,9 +56,7 @@
     <Project Path="src/Elastic.Markdown/Elastic.Markdown.csproj" />
   </Folder>
   <Folder Name="/src/api/">
-    <Project Path="src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj" />
-    <Project Path="src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj" />
-	<Project Path="src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj" />
+    <Project Path="src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj" />
 	<Project Path="src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj" />
   </Folder>
   <Folder Name="/src/authoring/">
diff --git a/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj b/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj
index 4bf0ef0294..76a05b7011 100644
--- a/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj
+++ b/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj
@@ -17,6 +17,7 @@
     <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore"/>
     <PackageReference Include="OpenTelemetry.Instrumentation.Http"/>
     <PackageReference Include="OpenTelemetry.Instrumentation.Runtime"/>
+    <PackageReference Include="Elastic.OpenTelemetry"/>
     <PackageReference Include="Crayon"/>
     <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets"/>
     <PackageReference Include="GitHub.Actions.Core" />
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidLogProcessor.cs b/src/Elastic.Documentation.ServiceDefaults/Logging/EuidLogProcessor.cs
similarity index 92%
rename from src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidLogProcessor.cs
rename to src/Elastic.Documentation.ServiceDefaults/Logging/EuidLogProcessor.cs
index 3d1e783184..511fe2677e 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidLogProcessor.cs
+++ b/src/Elastic.Documentation.ServiceDefaults/Logging/EuidLogProcessor.cs
@@ -3,11 +3,11 @@
 // See the LICENSE file in the project root for more information
 
 using System.Diagnostics;
-using Elastic.Documentation.Api.Core;
+using Elastic.Documentation.ServiceDefaults.Telemetry;
 using OpenTelemetry;
 using OpenTelemetry.Logs;
 
-namespace Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
+namespace Elastic.Documentation.ServiceDefaults.Logging;
 
 /// <summary>
 /// OpenTelemetry log processor that automatically adds user.euid attribute to log records
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs
similarity index 51%
rename from src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs
rename to src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs
index 41577a8e90..69cedcec96 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs
+++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs
@@ -3,8 +3,9 @@
 // See the LICENSE file in the project root for more information
 
 using System.Reflection;
-using Elastic.Documentation.Api.Core;
+using Elastic.Documentation.ServiceDefaults.Logging;
 using Elastic.OpenTelemetry;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using OpenTelemetry;
 using OpenTelemetry.Logs;
@@ -12,68 +13,18 @@
 using OpenTelemetry.Resources;
 using OpenTelemetry.Trace;
 
-namespace Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
+namespace Elastic.Documentation.ServiceDefaults.Telemetry;
 
-public static class OpenTelemetryExtensions
+public static class EuidEnrichmentExtensions
 {
 	/// <summary>
-	/// Configures logging for the Docs API with euid enrichment.
-	/// This is the shared configuration used in both production and tests.
+	/// Configures Elastic OpenTelemetry (EDOT) with euid enrichment for logging, tracing, and metrics.
+	/// Reads service.version from the calling assembly and configures the resource attribute.
+	/// No-ops if OTEL_EXPORTER_OTLP_ENDPOINT is not set.
 	/// </summary>
-	public static LoggerProviderBuilder AddDocsApiLogging(this LoggerProviderBuilder builder)
-	{
-		_ = builder.AddProcessor<EuidLogProcessor>();
-		return builder;
-	}
-
-	/// <summary>
-	/// Configures tracing for the Docs API with sources, instrumentation, and enrichment.
-	/// This is the shared configuration used in both production and tests.
-	/// </summary>
-	public static TracerProviderBuilder AddDocsApiTracing(this TracerProviderBuilder builder)
-	{
-		_ = builder
-			.AddSource(TelemetryConstants.AskAiSourceName)
-			.AddSource(TelemetryConstants.StreamTransformerSourceName)
-			.AddSource(TelemetryConstants.OtlpProxySourceName)
-			.AddSource(TelemetryConstants.CacheSourceName)
-			.AddSource(TelemetryConstants.AskAiFeedbackSourceName)
-			.AddSource(TelemetryConstants.McpToolSourceName)
-			.AddAspNetCoreInstrumentation(aspNetCoreOptions =>
-			{
-				// Don't trace root API endpoint (health check)
-				aspNetCoreOptions.Filter = (httpContext) =>
-				{
-					var path = httpContext.Request.Path.Value ?? string.Empty;
-					// Exclude root API path: /docs/_api/v1
-					return path != "/docs/_api/v1";
-				};
-
-				// Enrich spans with custom attributes from HTTP context
-				aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) =>
-				{
-					// Add euid cookie value to span attributes and baggage
-					if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid))
-					{
-						_ = activity.SetTag(TelemetryConstants.UserEuidAttributeName, euid);
-						// Add to baggage so it propagates to all child spans
-						_ = activity.AddBaggage(TelemetryConstants.UserEuidAttributeName, euid);
-					}
-				};
-			})
-			.AddProcessor<EuidSpanProcessor>() // Automatically add euid to all child spans
-			.AddHttpClientInstrumentation();
-
-		return builder;
-	}
-
-	/// <summary>
-	/// Configures Elastic OpenTelemetry (EDOT) for the Docs API.
-	/// </summary>
-	/// <param name="builder">The web application builder</param>
+	/// <param name="builder">The host application builder</param>
 	/// <returns>The builder for chaining</returns>
-	public static TBuilder AddDocsApiOpenTelemetry<TBuilder>(this TBuilder builder)
-		where TBuilder : IHostApplicationBuilder
+	public static TBuilder AddEuidEnrichment<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
 	{
 		var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
 		if (!useOtlpExporter)
@@ -89,8 +40,27 @@ public static TBuilder AddDocsApiOpenTelemetry<TBuilder>(this TBuilder builder)
 		_ = builder.AddElasticOpenTelemetry(options, edotBuilder =>
 		{
 			_ = edotBuilder
-				.WithLogging(logging => logging.AddDocsApiLogging())
-				.WithTracing(tracing => tracing.AddDocsApiTracing())
+				.WithLogging(logging => logging.AddProcessor<EuidLogProcessor>())
+				.WithTracing(tracing =>
+				{
+					_ = tracing
+						.AddAspNetCoreInstrumentation(aspNetCoreOptions =>
+						{
+							// Enrich spans with custom attributes from HTTP context
+							aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) =>
+							{
+								// Add euid cookie value to span attributes and baggage
+								if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid))
+								{
+									_ = activity.SetTag(TelemetryConstants.UserEuidAttributeName, euid);
+									// Add to baggage so it propagates to all child spans
+									_ = activity.AddBaggage(TelemetryConstants.UserEuidAttributeName, euid);
+								}
+							};
+						})
+						.AddProcessor<EuidSpanProcessor>() // Automatically add euid to all child spans
+						.AddHttpClientInstrumentation();
+				})
 				.WithMetrics(metrics =>
 				{
 					_ = metrics
@@ -110,8 +80,7 @@ public static TBuilder AddDocsApiOpenTelemetry<TBuilder>(this TBuilder builder)
 	private static void ConfigureServiceVersionAttributes<TBuilder>(TBuilder builder)
 		where TBuilder : IHostApplicationBuilder
 	{
-
-		var informationalVersion = Assembly.GetExecutingAssembly()
+		var informationalVersion = Assembly.GetCallingAssembly()
 			.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
 
 		if (informationalVersion is null)
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidSpanProcessor.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidSpanProcessor.cs
similarity index 91%
rename from src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidSpanProcessor.cs
rename to src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidSpanProcessor.cs
index 33de9ceeb2..f9b83e478e 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidSpanProcessor.cs
+++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidSpanProcessor.cs
@@ -3,10 +3,9 @@
 // See the LICENSE file in the project root for more information
 
 using System.Diagnostics;
-using Elastic.Documentation.Api.Core;
 using OpenTelemetry;
 
-namespace Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
+namespace Elastic.Documentation.ServiceDefaults.Telemetry;
 
 /// <summary>
 /// OpenTelemetry span processor that automatically adds user.euid tag to all spans
diff --git a/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs
index e4a90b864b..706cbb08d6 100644
--- a/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs
+++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs
@@ -11,4 +11,9 @@ namespace Elastic.Documentation.ServiceDefaults.Telemetry;
 public static class TelemetryConstants
 {
 	public const string AssemblerSyncInstrumentationName = "Elastic.Documentation.Assembler.Sync";
+
+	/// <summary>
+	/// Tag/baggage name used to annotate spans and log records with the user's EUID value.
+	/// </summary>
+	public const string UserEuidAttributeName = "user.euid";
 }
diff --git a/src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj b/src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj
deleted file mode 100644
index e9be25a83a..0000000000
--- a/src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj
+++ /dev/null
@@ -1,33 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
-
-  <PropertyGroup>
-    <OutputType>Exe</OutputType>
-    <TargetFramework>net10.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <Nullable>enable</Nullable>
-    <InvariantGlobalization>true</InvariantGlobalization>
-
-    <AssemblyName>docs-builder-api</AssemblyName>
-
-    <IsPublishable>true</IsPublishable>
-    <PublishAot>true</PublishAot>
-    <PublishTrimmed>true</PublishTrimmed>
-    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
-    <TrimmerSingleWarn>false</TrimmerSingleWarn>
-    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
-
-    <RootNamespace>Elastic.Documentation.Api.App</RootNamespace>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\..\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
-    <ProjectReference Include="..\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj"/>
-    <ProjectReference Include="..\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj"/>
-  </ItemGroup>
-
-  <ItemGroup>
-    <PackageReference Include="Elastic.OpenTelemetry" />
-    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
-  </ItemGroup>
-
-</Project>
diff --git a/src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj b/src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj
deleted file mode 100644
index 366861512a..0000000000
--- a/src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-﻿<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <TargetFramework>net10.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-        <AssemblyName>Elastic.Documentation.Api.Core</AssemblyName>
-        <RootNamespace>Elastic.Documentation.Api.Core</RootNamespace>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
-      <PackageReference Include="Microsoft.Extensions.Logging" />
-      <PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions" />
-      <PackageReference Include="NetEscapades.EnumGenerators" />
-    </ItemGroup>
-
-</Project>
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj b/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj
deleted file mode 100644
index 80382f47ee..0000000000
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj
+++ /dev/null
@@ -1,30 +0,0 @@
-﻿<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <TargetFramework>net10.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-        <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
-        <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsPreviewNamespaces>
-        <AssemblyName>Elastic.Documentation.Api.Infrastructure</AssemblyName>
-        <RootNamespace>Elastic.Documentation.Api.Infrastructure</RootNamespace>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <ProjectReference Include="..\..\Elastic.Documentation\Elastic.Documentation.csproj" />
-      <ProjectReference Include="..\..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
-      <ProjectReference Include="..\..\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
-      <ProjectReference Include="..\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <FrameworkReference Include="Microsoft.AspNetCore.App" />
-      <PackageReference Include="AWSSDK.DynamoDBv2" />
-      <PackageReference Include="Elastic.Clients.Elasticsearch" />
-      <PackageReference Include="NetEscapades.EnumGenerators" />
-      <PackageReference Include="Elastic.OpenTelemetry" />
-      <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
-      <PackageReference Include="OpenTelemetry.Instrumentation.Http" />
-    </ItemGroup>
-  
-</Project>
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs
index 6c182efde9..90b6e420c3 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs
@@ -7,10 +7,10 @@
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger<AgentBuilderAskAiGateway> logger) : IAskAiGateway
 {
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderStreamTransformer.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderStreamTransformer.cs
index 9eed7d6e0f..a7e0e49679 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderStreamTransformer.cs
@@ -3,10 +3,10 @@
 // See the LICENSE file in the project root for more information
 
 using System.Text.Json;
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Transforms Agent Builder SSE events to canonical AskAiEvent format
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiGatewayFactory.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs
similarity index 91%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiGatewayFactory.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs
index ec55afc03d..26d280eaa9 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiGatewayFactory.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs
@@ -2,11 +2,11 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Factory that creates the appropriate IAskAiGateway based on the resolved provider
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiProviderResolver.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiProviderResolver.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiProviderResolver.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiProviderResolver.cs
index 9c6791d24e..f92eadc930 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiProviderResolver.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiProviderResolver.cs
@@ -5,7 +5,7 @@
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Resolves which AI provider to use based on HTTP headers
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
index 99e20e5ec1..f7b898aa9d 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
@@ -5,13 +5,13 @@
 using System.Text.Json.Serialization;
 using Elastic.Clients.Elasticsearch;
 using Elastic.Clients.Elasticsearch.Serialization;
-using Elastic.Documentation.Api.Core;
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api;
+using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Configuration;
 using Elastic.Transport;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Records Ask AI message feedback to Elasticsearch.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/KibanaOptions.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/KibanaOptions.cs
similarity index 90%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/KibanaOptions.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/KibanaOptions.cs
index 69a043f0a8..df335c3cfc 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/KibanaOptions.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/KibanaOptions.cs
@@ -4,7 +4,7 @@
 
 using Microsoft.Extensions.Configuration;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 public class KibanaOptions(IConfiguration configuration)
 {
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs
index 58194dc80c..8807a79f24 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs
@@ -5,10 +5,10 @@
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using Elastic.Documentation.Api.Core.AskAi;
-using Elastic.Documentation.Api.Infrastructure.Gcp;
+using Elastic.Documentation.Api.AskAi;
+using Elastic.Documentation.Api.Gcp;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiGateway
 {
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayOptions.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayOptions.cs
index 665eb850d8..4102ffbc8d 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayOptions.cs
@@ -4,7 +4,7 @@
 
 using Microsoft.Extensions.Configuration;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 public class LlmGatewayOptions(IConfiguration configuration)
 {
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayStreamTransformer.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayStreamTransformer.cs
index 7808049560..57aec70ee7 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayStreamTransformer.cs
@@ -5,10 +5,10 @@
 using System.Diagnostics;
 using System.IO.Pipelines;
 using System.Text.Json;
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Transforms LLM Gateway SSE events to canonical AskAiEvent format
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/SseParser.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/SseParser.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/SseParser.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/SseParser.cs
index 20fab1570d..5cd2cb6bbc 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/SseParser.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/SseParser.cs
@@ -8,7 +8,7 @@
 using System.Text;
 using System.Text.Json.Serialization;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Represents a parsed Server-Sent Event (SSE)
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerBase.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerBase.cs
index d71d0f44f9..f3871a2da3 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerBase.cs
@@ -6,11 +6,11 @@
 using System.IO.Pipelines;
 using System.Text;
 using System.Text.Json;
-using Elastic.Documentation.Api.Core;
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Base class for stream transformers that handles common streaming logic
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerFactory.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerFactory.cs
similarity index 94%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerFactory.cs
rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerFactory.cs
index d59d9c6952..7cbb9ff3c6 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerFactory.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerFactory.cs
@@ -2,11 +2,11 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
 /// Factory that creates the appropriate IStreamTransformer based on the resolved provider
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs
rename to src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs
index d2a573238a..7f9414fc0a 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs
@@ -3,10 +3,10 @@
 // See the LICENSE file in the project root for more information
 
 using System.Net.Sockets;
-using Elastic.Documentation.Api.Core.Telemetry;
+using Elastic.Documentation.Api.Telemetry;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry;
+namespace Elastic.Documentation.Api.Adapters.Telemetry;
 
 /// <summary>
 /// Gateway that forwards OTLP telemetry to the ADOT Lambda Layer collector.
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiEvent.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs
rename to src/api/Elastic.Documentation.Api/AskAi/AskAiEvent.cs
index 2ad3e003c3..bb77ea4baa 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/AskAiEvent.cs
@@ -4,7 +4,7 @@
 
 using System.Text.Json.Serialization;
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
 /// Base class for all AskAI events streamed to the frontend
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackRequest.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackRequest.cs
similarity index 94%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackRequest.cs
rename to src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackRequest.cs
index 127159ca0a..1ab6752d41 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackRequest.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackRequest.cs
@@ -4,7 +4,7 @@
 
 using System.Text.Json.Serialization;
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
 /// Request model for submitting feedback on a specific Ask AI message.
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackUsecase.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackUsecase.cs
rename to src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs
index 82235e3924..7f08de6ab1 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackUsecase.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs
@@ -5,7 +5,7 @@
 using System.Diagnostics;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
 /// Use case for handling Ask AI message feedback submissions.
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs
rename to src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs
index 9c5c9f5b77..96efb4a9d1 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs
@@ -6,7 +6,7 @@
 using System.Text.Json;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 public class AskAiUsecase(
 	IAskAiGateway askAiGateway,
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiGateway.cs
rename to src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs
index 07e5161961..d1bb5c20da 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiGateway.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
 /// Response from an AI gateway containing the stream and conversation metadata
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiMessageFeedbackGateway.cs
rename to src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs
index e3c52da60d..ab815decd5 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiMessageFeedbackGateway.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
 /// Gateway interface for recording Ask AI message feedback.
diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/IStreamTransformer.cs b/src/api/Elastic.Documentation.Api/AskAi/IStreamTransformer.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Core/AskAi/IStreamTransformer.cs
rename to src/api/Elastic.Documentation.Api/AskAi/IStreamTransformer.cs
index 6bda052977..f1a20eb8bb 100644
--- a/src/api/Elastic.Documentation.Api.Core/AskAi/IStreamTransformer.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/IStreamTransformer.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.AskAi;
+namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
 /// Transforms raw SSE streams from various AI gateways into canonical AskAiEvent format
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/IParameterProvider.cs b/src/api/Elastic.Documentation.Api/Aws/IParameterProvider.cs
similarity index 90%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Aws/IParameterProvider.cs
rename to src/api/Elastic.Documentation.Api/Aws/IParameterProvider.cs
index b911b856dc..887dd12341 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/IParameterProvider.cs
+++ b/src/api/Elastic.Documentation.Api/Aws/IParameterProvider.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Infrastructure.Aws;
+namespace Elastic.Documentation.Api.Aws;
 
 /// <summary>
 /// Abstraction for retrieving configuration parameters.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LambdaExtensionParameterProvider.cs b/src/api/Elastic.Documentation.Api/Aws/LambdaExtensionParameterProvider.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Aws/LambdaExtensionParameterProvider.cs
rename to src/api/Elastic.Documentation.Api/Aws/LambdaExtensionParameterProvider.cs
index 37bb8519fb..80b600ac49 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LambdaExtensionParameterProvider.cs
+++ b/src/api/Elastic.Documentation.Api/Aws/LambdaExtensionParameterProvider.cs
@@ -7,7 +7,7 @@
 using System.Text.Json.Serialization;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Aws;
+namespace Elastic.Documentation.Api.Aws;
 
 public class LambdaExtensionParameterProvider(IHttpClientFactory httpClientFactory, AppEnvironment appEnvironment, ILogger<LambdaExtensionParameterProvider> logger) : IParameterProvider
 {
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs b/src/api/Elastic.Documentation.Api/Aws/LocalParameterProvider.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs
rename to src/api/Elastic.Documentation.Api/Aws/LocalParameterProvider.cs
index 8ec6d6d18e..e63b92a9c8 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs
+++ b/src/api/Elastic.Documentation.Api/Aws/LocalParameterProvider.cs
@@ -4,7 +4,7 @@
 
 using Microsoft.Extensions.Configuration;
 
-namespace Elastic.Documentation.Api.Infrastructure.Aws;
+namespace Elastic.Documentation.Api.Aws;
 
 public class LocalParameterProvider : IParameterProvider
 {
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/CacheKey.cs b/src/api/Elastic.Documentation.Api/Caching/CacheKey.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/CacheKey.cs
rename to src/api/Elastic.Documentation.Api/Caching/CacheKey.cs
index 67e565463a..d7f39b361a 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/CacheKey.cs
+++ b/src/api/Elastic.Documentation.Api/Caching/CacheKey.cs
@@ -5,7 +5,7 @@
 using System.Security.Cryptography;
 using System.Text;
 
-namespace Elastic.Documentation.Api.Infrastructure.Caching;
+namespace Elastic.Documentation.Api.Caching;
 
 /// <summary>
 /// Represents a cache key with automatic hashing of sensitive identifiers.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/DynamoDbDistributedCache.cs b/src/api/Elastic.Documentation.Api/Caching/DynamoDbDistributedCache.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/DynamoDbDistributedCache.cs
rename to src/api/Elastic.Documentation.Api/Caching/DynamoDbDistributedCache.cs
index e68cdf6791..f3f3ba940a 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/DynamoDbDistributedCache.cs
+++ b/src/api/Elastic.Documentation.Api/Caching/DynamoDbDistributedCache.cs
@@ -6,10 +6,10 @@
 using System.Globalization;
 using Amazon.DynamoDBv2;
 using Amazon.DynamoDBv2.Model;
-using Elastic.Documentation.Api.Core;
+using Elastic.Documentation.Api;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Caching;
+namespace Elastic.Documentation.Api.Caching;
 
 /// <summary>
 /// DynamoDB implementation of <see cref="IDistributedCache"/> for Lambda environments.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/IDistributedCache.cs b/src/api/Elastic.Documentation.Api/Caching/IDistributedCache.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/IDistributedCache.cs
rename to src/api/Elastic.Documentation.Api/Caching/IDistributedCache.cs
index de875cb36f..4920b4820b 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/IDistributedCache.cs
+++ b/src/api/Elastic.Documentation.Api/Caching/IDistributedCache.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Infrastructure.Caching;
+namespace Elastic.Documentation.Api.Caching;
 
 /// <summary>
 /// Abstraction for distributed caching across Lambda invocations.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/InMemoryDistributedCache.cs b/src/api/Elastic.Documentation.Api/Caching/InMemoryDistributedCache.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/InMemoryDistributedCache.cs
rename to src/api/Elastic.Documentation.Api/Caching/InMemoryDistributedCache.cs
index 9733ff574f..78e714d66d 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/InMemoryDistributedCache.cs
+++ b/src/api/Elastic.Documentation.Api/Caching/InMemoryDistributedCache.cs
@@ -4,7 +4,7 @@
 
 using System.Collections.Concurrent;
 
-namespace Elastic.Documentation.Api.Infrastructure.Caching;
+namespace Elastic.Documentation.Api.Caching;
 
 /// <summary>
 /// In-memory implementation of <see cref="IDistributedCache"/> for local development.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/MultiLayerCache.cs b/src/api/Elastic.Documentation.Api/Caching/MultiLayerCache.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/MultiLayerCache.cs
rename to src/api/Elastic.Documentation.Api/Caching/MultiLayerCache.cs
index 273dd1edb7..0f59aa42cd 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/MultiLayerCache.cs
+++ b/src/api/Elastic.Documentation.Api/Caching/MultiLayerCache.cs
@@ -4,10 +4,10 @@
 
 using System.Collections.Concurrent;
 using System.Diagnostics;
-using Elastic.Documentation.Api.Core;
+using Elastic.Documentation.Api;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Infrastructure.Caching;
+namespace Elastic.Documentation.Api.Caching;
 
 /// <summary>
 /// Multi-layer cache decorator implementing L1 (in-memory) + L2 (distributed) caching strategy.
diff --git a/src/api/Elastic.Documentation.Api.Core/Changes/ChangesUsecase.cs b/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs
similarity index 91%
rename from src/api/Elastic.Documentation.Api.Core/Changes/ChangesUsecase.cs
rename to src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs
index fac3706e64..797a7b66bb 100644
--- a/src/api/Elastic.Documentation.Api.Core/Changes/ChangesUsecase.cs
+++ b/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs
@@ -5,9 +5,10 @@
 using System.Buffers;
 using System.Text;
 using System.Text.Json;
+using Elastic.Documentation.Search;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Core.Changes;
+namespace Elastic.Documentation.Api.Changes;
 
 /// <summary>Use case for the documentation changes feed.</summary>
 public partial class ChangesUsecase(IChangesGateway changesGateway, ILogger<ChangesUsecase> logger)
@@ -113,11 +114,3 @@ public record ChangesApiResponse
 	public required bool HasMore { get; init; }
 	public string? NextCursor { get; init; }
 }
-
-/// <summary>A single changed page in the API response.</summary>
-public record ChangedPageDto
-{
-	public required string Url { get; init; }
-	public required string Title { get; init; }
-	public required DateTimeOffset LastUpdated { get; init; }
-}
diff --git a/src/api/Elastic.Documentation.Api.App/Dockerfile b/src/api/Elastic.Documentation.Api/Dockerfile
similarity index 90%
rename from src/api/Elastic.Documentation.Api.App/Dockerfile
rename to src/api/Elastic.Documentation.Api/Dockerfile
index 4912aaf0e9..feb94b95dd 100644
--- a/src/api/Elastic.Documentation.Api.App/Dockerfile
+++ b/src/api/Elastic.Documentation.Api/Dockerfile
@@ -31,12 +31,12 @@ RUN arch=$TARGETARCH \
     && if [ "$arch" = "amd64" ]; then arch="x64"; fi \
     && echo $TARGETOS-$arch > /tmp/rid
 
-RUN dotnet publish src/api/Elastic.Documentation.Api.App -r linux-x64 -c Release
+RUN dotnet publish src/api/Elastic.Documentation.Api -r linux-x64 -c Release
 
 # Runtime stage
 FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal AS runtime
 WORKDIR /app
-COPY --from=base /app/.artifacts/publish/Elastic.Documentation.Api.App/release_linux-x64/ /app/
+COPY --from=base /app/.artifacts/publish/Elastic.Documentation.Api/release_linux-x64/ /app/
 RUN chmod +x /app/docs-builder-api
 EXPOSE 8080
 ENV ASPNETCORE_URLS=http://+:8080
diff --git a/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj
new file mode 100644
index 0000000000..1c60dcab23
--- /dev/null
+++ b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj
@@ -0,0 +1,42 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net10.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <AssemblyName>docs-builder-api</AssemblyName>
+    <RootNamespace>Elastic.Documentation.Api</RootNamespace>
+    <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
+    <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsPreviewNamespaces>
+    <InvariantGlobalization>true</InvariantGlobalization>
+    <IsPublishable>true</IsPublishable>
+    <PublishAot>true</PublishAot>
+    <PublishTrimmed>true</PublishTrimmed>
+    <TrimmerSingleWarn>false</TrimmerSingleWarn>
+    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
+    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
+  </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
+    <ProjectReference Include="..\..\Elastic.Documentation\Elastic.Documentation.csproj" />
+    <ProjectReference Include="..\..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
+    <ProjectReference Include="..\..\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="AWSSDK.DynamoDBv2" />
+    <PackageReference Include="Elastic.Clients.Elasticsearch" />
+    <PackageReference Include="NetEscapades.EnumGenerators" />
+    <PackageReference Include="Elastic.OpenTelemetry" />
+    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
+    <PackageReference Include="OpenTelemetry.Instrumentation.Http" />
+    <PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions" />
+  </ItemGroup>
+  <ItemGroup>
+    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
+      <_Parameter1>Elastic.Documentation.Api.Tests</_Parameter1>
+    </AssemblyAttribute>
+    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
+      <_Parameter1>Elastic.Documentation.Api.IntegrationTests</_Parameter1>
+    </AssemblyAttribute>
+  </ItemGroup>
+</Project>
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/GcpIdTokenProvider.cs b/src/api/Elastic.Documentation.Api/Gcp/GcpIdTokenProvider.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Gcp/GcpIdTokenProvider.cs
rename to src/api/Elastic.Documentation.Api/Gcp/GcpIdTokenProvider.cs
index bb731d8f85..2a864af938 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/GcpIdTokenProvider.cs
+++ b/src/api/Elastic.Documentation.Api/Gcp/GcpIdTokenProvider.cs
@@ -6,9 +6,9 @@
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using Elastic.Documentation.Api.Infrastructure.Caching;
+using Elastic.Documentation.Api.Caching;
 
-namespace Elastic.Documentation.Api.Infrastructure.Gcp;
+namespace Elastic.Documentation.Api.Gcp;
 
 // This is a custom implementation to create an ID token for GCP.
 // Because Google.Api.Auth.OAuth2 is not compatible with AOT
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/IGcpIdTokenProvider.cs b/src/api/Elastic.Documentation.Api/Gcp/IGcpIdTokenProvider.cs
similarity index 93%
rename from src/api/Elastic.Documentation.Api.Infrastructure/Gcp/IGcpIdTokenProvider.cs
rename to src/api/Elastic.Documentation.Api/Gcp/IGcpIdTokenProvider.cs
index acfd64d3fd..48190d1e52 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/IGcpIdTokenProvider.cs
+++ b/src/api/Elastic.Documentation.Api/Gcp/IGcpIdTokenProvider.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Infrastructure.Gcp;
+namespace Elastic.Documentation.Api.Gcp;
 
 /// <summary>
 /// Interface for generating GCP ID tokens.
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs
rename to src/api/Elastic.Documentation.Api/MappingsExtensions.cs
index 36db7ca1b1..493b9a1d67 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs
+++ b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs
@@ -2,17 +2,18 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Core.AskAi;
-using Elastic.Documentation.Api.Core.Changes;
-using Elastic.Documentation.Api.Core.Search;
-using Elastic.Documentation.Api.Core.Telemetry;
+using Elastic.Documentation.Api.AskAi;
+using Elastic.Documentation.Api.Changes;
+using Elastic.Documentation.Api.Search;
+using Elastic.Documentation.Api.Telemetry;
 using Elastic.Documentation.Configuration.Products;
+using Elastic.Documentation.Search;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Routing;
 
-namespace Elastic.Documentation.Api.Infrastructure;
+namespace Elastic.Documentation.Api;
 
 public static class MappingsExtension
 {
diff --git a/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs b/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs
new file mode 100644
index 0000000000..12f9af265c
--- /dev/null
+++ b/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs
@@ -0,0 +1,96 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using Elastic.Documentation.ServiceDefaults.Telemetry;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OpenTelemetry;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Trace;
+
+namespace Elastic.Documentation.Api.OpenTelemetry;
+
+public static class OpenTelemetryExtensions
+{
+	/// <summary>
+	/// Configures Elastic OpenTelemetry (EDOT) for the Docs API.
+	/// Delegates euid enrichment and base configuration to AddEuidEnrichment,
+	/// then adds API-specific activity sources and path filters.
+	/// </summary>
+	/// <param name="builder">The web application builder</param>
+	/// <returns>The builder for chaining</returns>
+	public static TBuilder AddDocsApiOpenTelemetry<TBuilder>(this TBuilder builder)
+		where TBuilder : IHostApplicationBuilder
+	{
+		var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+		if (!useOtlpExporter)
+			return builder;
+
+		// Configure euid enrichment (euid processors, AspNetCore + HttpClient instrumentation, service.version)
+		_ = builder.AddEuidEnrichment();
+
+		// Additionally register the 5 API-specific activity sources
+		_ = builder.Services.ConfigureOpenTelemetryTracerProvider(tracing =>
+		{
+			_ = tracing
+				.AddSource(TelemetryConstants.AskAiSourceName)
+				.AddSource(TelemetryConstants.StreamTransformerSourceName)
+				.AddSource(TelemetryConstants.OtlpProxySourceName)
+				.AddSource(TelemetryConstants.CacheSourceName)
+				.AddSource(TelemetryConstants.AskAiFeedbackSourceName);
+		});
+
+		return builder;
+	}
+
+	/// <summary>
+	/// Configures logging for the Docs API with euid enrichment.
+	/// This is the shared configuration used in both production and tests.
+	/// </summary>
+	public static global::OpenTelemetry.Logs.LoggerProviderBuilder AddDocsApiLogging(this global::OpenTelemetry.Logs.LoggerProviderBuilder builder)
+	{
+		_ = builder.AddProcessor<Elastic.Documentation.ServiceDefaults.Logging.EuidLogProcessor>();
+		return builder;
+	}
+
+	/// <summary>
+	/// Configures tracing for the Docs API with sources, instrumentation, and enrichment.
+	/// This is the shared configuration used in both production and tests.
+	/// </summary>
+	public static global::OpenTelemetry.Trace.TracerProviderBuilder AddDocsApiTracing(this global::OpenTelemetry.Trace.TracerProviderBuilder builder)
+	{
+		_ = builder
+			.AddSource(TelemetryConstants.AskAiSourceName)
+			.AddSource(TelemetryConstants.StreamTransformerSourceName)
+			.AddSource(TelemetryConstants.OtlpProxySourceName)
+			.AddSource(TelemetryConstants.CacheSourceName)
+			.AddSource(TelemetryConstants.AskAiFeedbackSourceName)
+			.AddAspNetCoreInstrumentation(aspNetCoreOptions =>
+			{
+				// Don't trace root API endpoint (health check)
+				aspNetCoreOptions.Filter = (httpContext) =>
+				{
+					var path = httpContext.Request.Path.Value ?? string.Empty;
+					// Exclude root API path: /docs/_api/v1
+					return path != "/docs/_api/v1";
+				};
+
+				// Enrich spans with custom attributes from HTTP context
+				aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) =>
+				{
+					// Add euid cookie value to span attributes and baggage
+					if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid))
+					{
+						_ = activity.SetTag(Elastic.Documentation.ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid);
+						// Add to baggage so it propagates to all child spans
+						_ = activity.AddBaggage(Elastic.Documentation.ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid);
+					}
+				};
+			})
+			.AddProcessor<Elastic.Documentation.ServiceDefaults.Telemetry.EuidSpanProcessor>() // Automatically add euid to all child spans
+			.AddHttpClientInstrumentation();
+
+		return builder;
+	}
+}
diff --git a/src/api/Elastic.Documentation.Api.App/Program.cs b/src/api/Elastic.Documentation.Api/Program.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.App/Program.cs
rename to src/api/Elastic.Documentation.Api/Program.cs
index 980fe483e0..98dec28313 100644
--- a/src/api/Elastic.Documentation.Api.App/Program.cs
+++ b/src/api/Elastic.Documentation.Api/Program.cs
@@ -2,8 +2,8 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Infrastructure;
-using Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
+using Elastic.Documentation.Api;
+using Elastic.Documentation.Api.OpenTelemetry;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Assembler;
 using Elastic.Documentation.Search.Common;
diff --git a/src/api/Elastic.Documentation.Api.App/Properties/launchSettings.json b/src/api/Elastic.Documentation.Api/Properties/launchSettings.json
similarity index 100%
rename from src/api/Elastic.Documentation.Api.App/Properties/launchSettings.json
rename to src/api/Elastic.Documentation.Api/Properties/launchSettings.json
diff --git a/src/api/Elastic.Documentation.Api.Core/Search/FullSearchUsecase.cs b/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs
similarity index 97%
rename from src/api/Elastic.Documentation.Api.Core/Search/FullSearchUsecase.cs
rename to src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs
index 0f9d00c933..3eb6e84586 100644
--- a/src/api/Elastic.Documentation.Api.Core/Search/FullSearchUsecase.cs
+++ b/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs
@@ -2,9 +2,10 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation.Search;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Core.Search;
+namespace Elastic.Documentation.Api.Search;
 
 /// <summary>
 /// Use case for full-page search operations.
diff --git a/src/api/Elastic.Documentation.Api.Core/Search/NavigationSearchUsecase.cs b/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs
similarity index 82%
rename from src/api/Elastic.Documentation.Api.Core/Search/NavigationSearchUsecase.cs
rename to src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs
index 87d0ef695a..751a8182a3 100644
--- a/src/api/Elastic.Documentation.Api.Core/Search/NavigationSearchUsecase.cs
+++ b/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs
@@ -2,9 +2,10 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation.Search;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Core.Search;
+namespace Elastic.Documentation.Api.Search;
 
 // note still called SearchUseCase because we'll re-add Search() and ensure both share the same client.
 public partial class NavigationSearchUsecase(INavigationSearchGateway navigationSearchGateway, ILogger<NavigationSearchUsecase> logger)
@@ -70,18 +71,3 @@ public record NavigationSearchAggregations
 	public IReadOnlyDictionary<string, long> Type { get; init; } = new Dictionary<string, long>();
 }
 
-public record NavigationSearchResultItemParent
-{
-	public required string Title { get; init; }
-	public required string Url { get; init; }
-}
-
-public record NavigationSearchResultItem
-{
-	public required string Type { get; init; }
-	public required string Url { get; init; }
-	public required string Title { get; init; }
-	public required string Description { get; init; }
-	public required NavigationSearchResultItemParent[] Parents { get; init; }
-	public float Score { get; init; }
-}
diff --git a/src/api/Elastic.Documentation.Api.Core/SerializationContext.cs b/src/api/Elastic.Documentation.Api/SerializationContext.cs
similarity index 87%
rename from src/api/Elastic.Documentation.Api.Core/SerializationContext.cs
rename to src/api/Elastic.Documentation.Api/SerializationContext.cs
index 5c28076f00..b293ee9975 100644
--- a/src/api/Elastic.Documentation.Api.Core/SerializationContext.cs
+++ b/src/api/Elastic.Documentation.Api/SerializationContext.cs
@@ -3,11 +3,12 @@
 // See the LICENSE file in the project root for more information
 
 using System.Text.Json.Serialization;
-using Elastic.Documentation.Api.Core.AskAi;
-using Elastic.Documentation.Api.Core.Changes;
-using Elastic.Documentation.Api.Core.Search;
+using Elastic.Documentation.Api.AskAi;
+using Elastic.Documentation.Api.Changes;
+using Elastic.Documentation.Api.Search;
+using Elastic.Documentation.Search;
 
-namespace Elastic.Documentation.Api.Core;
+namespace Elastic.Documentation.Api;
 
 /// <summary>
 /// Types for OpenTelemetry telemetry serialization (AOT-compatible)
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs b/src/api/Elastic.Documentation.Api/ServicesExtension.cs
similarity index 91%
rename from src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs
rename to src/api/Elastic.Documentation.Api/ServicesExtension.cs
index e597e9e45f..436b457b20 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs
+++ b/src/api/Elastic.Documentation.Api/ServicesExtension.cs
@@ -4,20 +4,22 @@
 
 using System.ComponentModel.DataAnnotations;
 using Amazon.DynamoDBv2;
-using Elastic.Documentation.Api.Core;
-using Elastic.Documentation.Api.Core.AskAi;
-using Elastic.Documentation.Api.Core.Telemetry;
-using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
-using Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry;
-using Elastic.Documentation.Api.Infrastructure.Caching;
-using Elastic.Documentation.Api.Infrastructure.Gcp;
+using Elastic.Documentation.Api;
+using Elastic.Documentation.Api.AskAi;
+using Elastic.Documentation.Api.Changes;
+using Elastic.Documentation.Api.Telemetry;
+using Elastic.Documentation.Api.Adapters.AskAi;
+using Elastic.Documentation.Api.Adapters.Telemetry;
+using Elastic.Documentation.Api.Caching;
+using Elastic.Documentation.Api.Gcp;
+using Elastic.Documentation.Api.Search;
 using Elastic.Documentation.Search;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using NetEscapades.EnumGenerators;
 
-namespace Elastic.Documentation.Api.Infrastructure;
+namespace Elastic.Documentation.Api;
 
 [EnumExtensions]
 public enum AppEnv
@@ -193,8 +195,14 @@ private static void AddSearchUsecase(IServiceCollection services, AppEnv appEnv)
 		var logger = GetLogger(services);
 		logger?.LogInformation("Configuring Search use case for environment {AppEnvironment}", appEnv);
 
-		// Use the shared search service for DI registration
+		// Use the shared search service for DI registration (registers gateways)
 		_ = services.AddSearchServices();
+
+		// Register usecases (depend on the gateways from search service)
+		_ = services.AddScoped<NavigationSearchUsecase>();
+		_ = services.AddScoped<FullSearchUsecase>();
+		logger?.LogInformation("Full search use case registered with hybrid RRF support");
+		_ = services.AddScoped<ChangesUsecase>();
 	}
 
 	private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appEnv)
diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs
similarity index 94%
rename from src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs
index 7cb0a67d85..5c00f37ca2 100644
--- a/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.Telemetry;
+namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
 /// Gateway for forwarding OTLP telemetry to a collector.
diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpForwardResult.cs
similarity index 93%
rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpForwardResult.cs
index 7a9b4a1d0d..9acddc8fb7 100644
--- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpForwardResult.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.Telemetry;
+namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
 /// Result of forwarding OTLP telemetry to a collector.
diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs
index a684e60f8b..c42bca4be0 100644
--- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs
@@ -4,7 +4,7 @@
 
 using Microsoft.Extensions.Configuration;
 
-namespace Elastic.Documentation.Api.Core.Telemetry;
+namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
 /// Configuration options for the OTLP proxy.
@@ -14,12 +14,12 @@ namespace Elastic.Documentation.Api.Core.Telemetry;
 /// ADOT Lambda Layer runs a local OpenTelemetry Collector that accepts OTLP/HTTP on:
 /// - localhost:4318 (HTTP/JSON and HTTP/protobuf)
 /// - localhost:4317 (gRPC)
-/// 
+///
 /// Configuration priority:
 /// 1. OtlpProxy:Endpoint in IConfiguration (for tests/overrides)
 /// 2. OTEL_EXPORTER_OTLP_ENDPOINT environment variable
 /// 3. Default: http://localhost:4318
-/// 
+///
 /// The proxy will return 503 if the collector is not available.
 /// </remarks>
 public class OtlpProxyOptions(IConfiguration configuration)
diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyRequest.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyRequest.cs
similarity index 95%
rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyRequest.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyRequest.cs
index 6b79762ccd..3a5b48d5e0 100644
--- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyRequest.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyRequest.cs
@@ -5,7 +5,7 @@
 using System.ComponentModel.DataAnnotations;
 using NetEscapades.EnumGenerators;
 
-namespace Elastic.Documentation.Api.Core.Telemetry;
+namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
 /// OTLP signal types supported by the proxy.
diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs
similarity index 96%
rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs
index 9c051a5794..5ac9963547 100644
--- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs
@@ -4,7 +4,7 @@
 
 using System.Diagnostics;
 
-namespace Elastic.Documentation.Api.Core.Telemetry;
+namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
 /// Proxies OTLP telemetry from the frontend to the local ADOT Lambda Layer collector.
diff --git a/src/api/Elastic.Documentation.Api.Core/TelemetryConstants.cs b/src/api/Elastic.Documentation.Api/TelemetryConstants.cs
similarity index 82%
rename from src/api/Elastic.Documentation.Api.Core/TelemetryConstants.cs
rename to src/api/Elastic.Documentation.Api/TelemetryConstants.cs
index 8619d885c0..cabaf135b8 100644
--- a/src/api/Elastic.Documentation.Api.Core/TelemetryConstants.cs
+++ b/src/api/Elastic.Documentation.Api/TelemetryConstants.cs
@@ -2,13 +2,19 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core;
+namespace Elastic.Documentation.Api;
 
 /// <summary>
 /// Constants for OpenTelemetry instrumentation in the Docs API.
 /// </summary>
 public static class TelemetryConstants
 {
+	/// <summary>
+	/// Tag/baggage name used to annotate spans with the user's EUID value.
+	/// Forwarded from ServiceDefaults.Telemetry.TelemetryConstants for convenience.
+	/// </summary>
+	public const string UserEuidAttributeName = Elastic.Documentation.ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName;
+
 	/// <summary>
 	/// ActivitySource name for AskAi operations.
 	/// Used in AskAiUsecase to create spans.
@@ -21,11 +27,6 @@ public static class TelemetryConstants
 	/// </summary>
 	public const string StreamTransformerSourceName = "Elastic.Documentation.Api.StreamTransformer";
 
-	/// <summary>
-	/// Tag/baggage name used to annotate spans with the user's EUID value.
-	/// </summary>
-	public const string UserEuidAttributeName = "user.euid";
-
 	/// <summary>
 	/// ActivitySource name for OTLP proxy operations.
 	/// Used to trace frontend telemetry proxying.
@@ -43,10 +44,4 @@ public static class TelemetryConstants
 	/// Used to trace feedback submissions.
 	/// </summary>
 	public const string AskAiFeedbackSourceName = "Elastic.Documentation.Api.AskAiFeedback";
-
-	/// <summary>
-	/// ActivitySource name for MCP remote tool call operations.
-	/// Used to trace MCP tool execution and outcomes.
-	/// </summary>
-	public const string McpToolSourceName = "Elastic.Documentation.Api.McpTools";
 }
diff --git a/src/api/Elastic.Documentation.Api.Core/Validation/IValidator.cs b/src/api/Elastic.Documentation.Api/Validation/IValidator.cs
similarity index 86%
rename from src/api/Elastic.Documentation.Api.Core/Validation/IValidator.cs
rename to src/api/Elastic.Documentation.Api/Validation/IValidator.cs
index 1aa281764d..3948cd2783 100644
--- a/src/api/Elastic.Documentation.Api.Core/Validation/IValidator.cs
+++ b/src/api/Elastic.Documentation.Api/Validation/IValidator.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.Validation;
+namespace Elastic.Documentation.Api.Validation;
 
 public interface IValidator
 {
diff --git a/src/api/Elastic.Documentation.Api.App/appsettings.development.json b/src/api/Elastic.Documentation.Api/appsettings.Development.json
similarity index 100%
rename from src/api/Elastic.Documentation.Api.App/appsettings.development.json
rename to src/api/Elastic.Documentation.Api/appsettings.Development.json
diff --git a/src/api/Elastic.Documentation.Api.App/appsettings.edge.json b/src/api/Elastic.Documentation.Api/appsettings.edge.json
similarity index 100%
rename from src/api/Elastic.Documentation.Api.App/appsettings.edge.json
rename to src/api/Elastic.Documentation.Api/appsettings.edge.json
diff --git a/src/api/Elastic.Documentation.Api.App/appsettings.json b/src/api/Elastic.Documentation.Api/appsettings.json
similarity index 100%
rename from src/api/Elastic.Documentation.Api.App/appsettings.json
rename to src/api/Elastic.Documentation.Api/appsettings.json
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj
index ba0f765322..9fc53017f1 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj
@@ -23,8 +23,6 @@
     <ProjectReference Include="..\..\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
     <ProjectReference Include="..\..\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
     <ProjectReference Include="..\..\services\Elastic.Documentation.Assembler\Elastic.Documentation.Assembler.csproj" />
-    <ProjectReference Include="..\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj"/>
-    <ProjectReference Include="..\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj"/>
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
index 6e8100c5cb..344c6e515c 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
@@ -2,7 +2,6 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
 using Elastic.Documentation.Assembler.Links;
 using Elastic.Documentation.Assembler.Mcp;
 using Elastic.Documentation.Configuration;
@@ -11,6 +10,7 @@
 using Elastic.Documentation.Mcp.Remote;
 using Elastic.Documentation.Search.Common;
 using Elastic.Documentation.ServiceDefaults;
+using Elastic.Documentation.ServiceDefaults.Telemetry;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Diagnostics;
 using Microsoft.AspNetCore.Diagnostics.HealthChecks;
@@ -24,7 +24,7 @@
 	var builder = WebApplication.CreateSlimBuilder(args);
 	_ = builder.AddDocumentationServiceDefaults();
 	_ = builder.AddDefaultHealthChecks();
-	_ = builder.AddDocsApiOpenTelemetry();
+	_ = builder.AddEuidEnrichment();
 
 	// Configure Kestrel to listen on port 8080 (standard container port)
 	_ = builder.WebHost.ConfigureKestrel(serverOptions =>
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs b/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs
index 5f1f37243c..0513da7239 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs
@@ -4,7 +4,6 @@
 
 using System.Diagnostics;
 using System.Reflection;
-using Elastic.Documentation.Api.Core;
 using Elastic.Documentation.Configuration;
 using Microsoft.Extensions.Logging;
 
@@ -12,7 +11,8 @@ namespace Elastic.Documentation.Mcp.Remote.Telemetry;
 
 public static class McpToolTelemetry
 {
-	private static readonly ActivitySource McpActivitySource = new(TelemetryConstants.McpToolSourceName);
+	private const string McpToolSourceName = "Elastic.Documentation.Api.McpTools";
+	private static readonly ActivitySource McpActivitySource = new(McpToolSourceName);
 	private static readonly McpServerProfile ServerProfile = ResolveServerProfile();
 	private static readonly string? ServerVersion = ResolveServerVersion();
 
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
index 3c13089eb1..fa64d2215d 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
@@ -5,8 +5,8 @@
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Text.Json;
-using Elastic.Documentation.Api.Core.Search;
 using Elastic.Documentation.Assembler.Mcp;
+using Elastic.Documentation.Search;
 using Elastic.Documentation.Mcp.Remote.Responses;
 using Elastic.Documentation.Mcp.Remote.Telemetry;
 using Microsoft.Extensions.Logging;
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
index b85a12a4f6..274752873c 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
@@ -5,8 +5,8 @@
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Text.Json;
-using Elastic.Documentation.Api.Core.Search;
 using Elastic.Documentation.Assembler.Mcp;
+using Elastic.Documentation.Search;
 using Elastic.Documentation.Mcp.Remote.Responses;
 using Elastic.Documentation.Mcp.Remote.Telemetry;
 using Microsoft.Extensions.Logging;
diff --git a/src/services/Elastic.Documentation.Search/ChangesGateway.cs b/src/services/Elastic.Documentation.Search/ChangesGateway.cs
index d1715ba421..1e065a64dd 100644
--- a/src/services/Elastic.Documentation.Search/ChangesGateway.cs
+++ b/src/services/Elastic.Documentation.Search/ChangesGateway.cs
@@ -3,7 +3,6 @@
 // See the LICENSE file in the project root for more information
 
 using Elastic.Clients.Elasticsearch;
-using Elastic.Documentation.Api.Core.Changes;
 using Elastic.Documentation.Search.Common;
 using Microsoft.Extensions.Logging;
 
diff --git a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj b/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
index 8a350648d2..5b7ce28433 100644
--- a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
+++ b/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
@@ -11,7 +11,6 @@
     <ItemGroup>
       <ProjectReference Include="..\..\Elastic.Documentation\Elastic.Documentation.csproj" />
       <ProjectReference Include="..\..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
-      <ProjectReference Include="..\..\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
     </ItemGroup>
 
     <ItemGroup>
diff --git a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs b/src/services/Elastic.Documentation.Search/FullSearchGateway.cs
index 54838c253f..5eb7bd8931 100644
--- a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs
+++ b/src/services/Elastic.Documentation.Search/FullSearchGateway.cs
@@ -5,7 +5,6 @@
 using System.Text.RegularExpressions;
 using Elastic.Clients.Elasticsearch;
 using Elastic.Clients.Elasticsearch.QueryDsl;
-using Elastic.Documentation.Api.Core.Search;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Search.Common;
 using Microsoft.Extensions.Logging;
diff --git a/src/api/Elastic.Documentation.Api.Core/Changes/IChangesGateway.cs b/src/services/Elastic.Documentation.Search/IChangesGateway.cs
similarity index 81%
rename from src/api/Elastic.Documentation.Api.Core/Changes/IChangesGateway.cs
rename to src/services/Elastic.Documentation.Search/IChangesGateway.cs
index 27a02fe317..63127c7b2d 100644
--- a/src/api/Elastic.Documentation.Api.Core/Changes/IChangesGateway.cs
+++ b/src/services/Elastic.Documentation.Search/IChangesGateway.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.Changes;
+namespace Elastic.Documentation.Search;
 
 /// <summary>Gateway interface for querying documentation page changes.</summary>
 public interface IChangesGateway
@@ -34,3 +34,11 @@ public static class ChangesDefaults
 	public const int PageSize = 100;
 	public const int MaxPageSize = 1000;
 }
+
+/// <summary>A single changed page in the API response.</summary>
+public record ChangedPageDto
+{
+	public required string Url { get; init; }
+	public required string Title { get; init; }
+	public required DateTimeOffset LastUpdated { get; init; }
+}
diff --git a/src/api/Elastic.Documentation.Api.Core/Search/IFullSearchGateway.cs b/src/services/Elastic.Documentation.Search/IFullSearchGateway.cs
similarity index 98%
rename from src/api/Elastic.Documentation.Api.Core/Search/IFullSearchGateway.cs
rename to src/services/Elastic.Documentation.Search/IFullSearchGateway.cs
index 21ed3d6ce4..876ca4aee7 100644
--- a/src/api/Elastic.Documentation.Api.Core/Search/IFullSearchGateway.cs
+++ b/src/services/Elastic.Documentation.Search/IFullSearchGateway.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.Search;
+namespace Elastic.Documentation.Search;
 
 /// <summary>
 /// Gateway interface for full-page search operations.
diff --git a/src/api/Elastic.Documentation.Api.Core/Search/INavigationSearchGateway.cs b/src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs
similarity index 56%
rename from src/api/Elastic.Documentation.Api.Core/Search/INavigationSearchGateway.cs
rename to src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs
index 02eb092778..8e172d1834 100644
--- a/src/api/Elastic.Documentation.Api.Core/Search/INavigationSearchGateway.cs
+++ b/src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Api.Core.Search;
+namespace Elastic.Documentation.Search;
 
 public interface INavigationSearchGateway
 {
@@ -21,3 +21,19 @@ public record NavigationSearchResult
 	public required List<NavigationSearchResultItem> Results { get; init; }
 	public IReadOnlyDictionary<string, long> Aggregations { get; init; } = new Dictionary<string, long>();
 }
+
+public record NavigationSearchResultItemParent
+{
+	public required string Title { get; init; }
+	public required string Url { get; init; }
+}
+
+public record NavigationSearchResultItem
+{
+	public required string Type { get; init; }
+	public required string Url { get; init; }
+	public required string Title { get; init; }
+	public required string Description { get; init; }
+	public required NavigationSearchResultItemParent[] Parents { get; init; }
+	public float Score { get; init; }
+}
diff --git a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs b/src/services/Elastic.Documentation.Search/MockSearchGateway.cs
index 7cb8de28c3..74ced72ee9 100644
--- a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs
+++ b/src/services/Elastic.Documentation.Search/MockSearchGateway.cs
@@ -2,8 +2,6 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Core.Search;
-
 namespace Elastic.Documentation.Search;
 
 public class MockNavigationSearchGateway : INavigationSearchGateway
diff --git a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs b/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs
index ec01a14947..67f3814c0c 100644
--- a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs
+++ b/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs
@@ -6,7 +6,6 @@
 using Elastic.Clients.Elasticsearch;
 using Elastic.Clients.Elasticsearch.Core.Explain;
 using Elastic.Clients.Elasticsearch.QueryDsl;
-using Elastic.Documentation.Api.Core.Search;
 using Elastic.Documentation.Search.Common;
 using Microsoft.Extensions.Logging;
 
diff --git a/src/services/Elastic.Documentation.Search/ServicesExtension.cs b/src/services/Elastic.Documentation.Search/ServicesExtension.cs
index 0571b470df..373b5a1c3a 100644
--- a/src/services/Elastic.Documentation.Search/ServicesExtension.cs
+++ b/src/services/Elastic.Documentation.Search/ServicesExtension.cs
@@ -2,8 +2,6 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using Elastic.Documentation.Api.Core.Changes;
-using Elastic.Documentation.Api.Core.Search;
 using Elastic.Documentation.Search.Common;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -30,17 +28,14 @@ public static IServiceCollection AddSearchServices(this IServiceCollection servi
 
 		// Navigation Search (autocomplete/navigation search)
 		_ = services.AddScoped<INavigationSearchGateway, NavigationSearchGateway>();
-		_ = services.AddScoped<NavigationSearchUsecase>();
 
 		// FullSearch (full-page search with hybrid RRF)
 		_ = services.AddScoped<IFullSearchGateway, FullSearchGateway>();
-		_ = services.AddScoped<FullSearchUsecase>();
-		logger?.LogInformation("Full search use case registered with hybrid RRF support");
+		logger?.LogInformation("Full search gateways registered with hybrid RRF support");
 
 		// Changes feed (cursor-paginated changes since a given date)
 		_ = services.AddSingleton<SharedPointInTimeManager>();
 		_ = services.AddScoped<IChangesGateway, ChangesGateway>();
-		_ = services.AddScoped<ChangesUsecase>();
 
 		return services;
 	}
diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
index 42c1c52499..cf89234236 100644
--- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs
+++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
@@ -11,7 +11,7 @@
 using Documentation.Builder.Diagnostics.LiveMode;
 using Elastic.Documentation;
 #if DEBUG
-using Elastic.Documentation.Api.Infrastructure;
+using Elastic.Documentation.Api;
 #endif
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.ServiceDefaults;
diff --git a/src/tooling/docs-builder/Http/StaticWebHost.cs b/src/tooling/docs-builder/Http/StaticWebHost.cs
index 788509e6bc..539ed4e8ac 100644
--- a/src/tooling/docs-builder/Http/StaticWebHost.cs
+++ b/src/tooling/docs-builder/Http/StaticWebHost.cs
@@ -4,7 +4,7 @@
 
 using System.IO.Abstractions;
 #if DEBUG
-using Elastic.Documentation.Api.Infrastructure;
+using Elastic.Documentation.Api;
 #endif
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Extensions;
diff --git a/src/tooling/docs-builder/docs-builder.csproj b/src/tooling/docs-builder/docs-builder.csproj
index 40d37cb902..63cbb5997f 100644
--- a/src/tooling/docs-builder/docs-builder.csproj
+++ b/src/tooling/docs-builder/docs-builder.csproj
@@ -32,8 +32,7 @@
   </ItemGroup>
 
   <ItemGroup Condition="'$(Configuration)' == 'Debug'">
-    <ProjectReference Include="..\..\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
-    <ProjectReference Include="..\..\api\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj" />
+    <ProjectReference Include="..\..\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj b/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj
index a6be439513..e5b507a5fd 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj
@@ -11,8 +11,7 @@
     <ProjectReference Include="$(SolutionRoot)\src\tooling\docs-builder\docs-builder.csproj"/>
     <ProjectReference Include="$(SolutionRoot)\aspire\aspire.csproj" />
     <ProjectReference Include="$(SolutionRoot)\src\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
-    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
-    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj" />
+    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
     <ProjectReference Include="$(SolutionRoot)\src\Elastic.Markdown\Elastic.Markdown.csproj" />
   </ItemGroup>
   <ItemGroup>
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs
index 8bf29348cf..4e3e212695 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs
@@ -4,7 +4,7 @@
 
 using System.Net.Http.Json;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Core.Search;
+using Elastic.Documentation.Api.Search;
 
 namespace Elastic.Assembler.IntegrationTests.Search;
 
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs
index aa8e7c9887..b99d801702 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs
@@ -5,9 +5,9 @@
 using System.Net;
 using System.Text;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Core.AskAi;
-using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
-using Elastic.Documentation.Api.Infrastructure.Gcp;
+using Elastic.Documentation.Api.AskAi;
+using Elastic.Documentation.Api.Adapters.AskAi;
+using Elastic.Documentation.Api.Gcp;
 using FakeItEasy;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj
index 569011a7f2..3e30d7dc32 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj
@@ -5,9 +5,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api.App\Elastic.Documentation.Api.App.csproj"/>
-    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj"/>
-    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj"/>
+    <ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj"/>
   </ItemGroup>
 
   <ItemGroup>
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs
index e612802042..c2a28743bc 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs
@@ -5,8 +5,8 @@
 using System.Diagnostics;
 using System.Text;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Core;
-using Elastic.Documentation.Api.Core.AskAi;
+using Elastic.Documentation.Api;
+using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Api.IntegrationTests.Fixtures;
 using FakeItEasy;
 using Microsoft.Extensions.DependencyInjection;
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs
index 47794d8a77..d7d5bf88ed 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs
@@ -3,9 +3,9 @@
 // See the LICENSE file in the project root for more information
 
 using System.Diagnostics;
-using Elastic.Documentation.Api.Infrastructure;
-using Elastic.Documentation.Api.Infrastructure.Aws;
-using Elastic.Documentation.Api.Infrastructure.OpenTelemetry;
+using Elastic.Documentation.Api;
+using Elastic.Documentation.Api.Aws;
+using Elastic.Documentation.Api.OpenTelemetry;
 using FakeItEasy;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Mvc.Testing;
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs
index 8f5c48dff9..714ba2b7ac 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs
@@ -5,7 +5,7 @@
 using System.Net;
 using System.Text;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry;
+using Elastic.Documentation.Api.Adapters.Telemetry;
 using Elastic.Documentation.Api.IntegrationTests.Fixtures;
 using FakeItEasy;
 using Microsoft.Extensions.DependencyInjection;
diff --git a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj
index 8889bbcbaa..e18027f7c3 100644
--- a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj
+++ b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj
@@ -7,7 +7,6 @@
     </PropertyGroup>
 
     <ItemGroup>
-      <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
       <ProjectReference Include="..\..\src\api\Elastic.Documentation.Mcp.Remote\Elastic.Documentation.Mcp.Remote.csproj" />
       <ProjectReference Include="..\..\src\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
       <ProjectReference Include="..\..\src\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
index 0d231b762a..9fa2064b16 100644
--- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
+++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
@@ -3,12 +3,11 @@
 // See the LICENSE file in the project root for more information
 
 using System.Collections.Frozen;
-using Elastic.Documentation.Api.Core.Search;
+using Elastic.Documentation.Search;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Mcp.Remote.Gateways;
 using Elastic.Documentation.Mcp.Remote.Tools;
-using Elastic.Documentation.Search;
 using Elastic.Documentation.Search.Common;
 using Elastic.Documentation.ServiceDefaults;
 using Microsoft.Extensions.Logging.Abstractions;
diff --git a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj
index 4763c5eb0d..c7dcf104d1 100644
--- a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj
+++ b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj
@@ -7,8 +7,7 @@
     </PropertyGroup>
 
     <ItemGroup>
-      <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
-      <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj" />
+      <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
       <ProjectReference Include="..\..\src\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
       <ProjectReference Include="..\..\src\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
     </ItemGroup>
diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs
index 8cbbefa7d7..f36e62349e 100644
--- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs
+++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs
@@ -6,8 +6,8 @@
 using System.Text;
 using System.Text.Json;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Core.AskAi;
-using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
+using Elastic.Documentation.Api.AskAi;
+using Elastic.Documentation.Api.Adapters.AskAi;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Elastic.Documentation.Api.Infrastructure.Tests.Adapters.AskAi;
diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs
index a651cd619a..b487c60d36 100644
--- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs
+++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs
@@ -7,8 +7,8 @@
 using Amazon.DynamoDBv2;
 using Amazon.DynamoDBv2.Model;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Infrastructure.Caching;
-using Elastic.Documentation.Api.Infrastructure.Gcp;
+using Elastic.Documentation.Api.Caching;
+using Elastic.Documentation.Api.Gcp;
 using FakeItEasy;
 using Microsoft.Extensions.Logging.Abstractions;
 
diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj
index 2d317e4f79..150e8f13af 100644
--- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj
+++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj
@@ -5,8 +5,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api.Infrastructure\Elastic.Documentation.Api.Infrastructure.csproj" />
-    <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api.Core\Elastic.Documentation.Api.Core.csproj" />
+    <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
     <ProjectReference Include="..\..\src\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
   </ItemGroup>
 

From 065710c57fee38799907ce4f2298984374abf543 Mon Sep 17 00:00:00 2001
From: Martijn Laarman <Mpdreamz@gmail.com>
Date: Mon, 18 May 2026 12:15:58 +0200
Subject: [PATCH 2/6] Fix C# import ordering and name simplification after Api
 merge

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---
 .../OpenTelemetry/OpenTelemetryExtensions.cs                | 4 ++--
 src/api/Elastic.Documentation.Api/ServicesExtension.cs      | 6 +++---
 src/api/Elastic.Documentation.Api/TelemetryConstants.cs     | 2 +-
 .../Tools/CoherenceTools.cs                                 | 2 +-
 .../Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs   | 2 +-
 .../AskAiGatewayStreamingTests.cs                           | 2 +-
 .../McpToolsIntegrationTestsBase.cs                         | 2 +-
 .../Adapters/AskAi/StreamTransformerTests.cs                | 2 +-
 8 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs b/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs
index 12f9af265c..a0511fba66 100644
--- a/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs
+++ b/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs
@@ -82,9 +82,9 @@ public static TBuilder AddDocsApiOpenTelemetry<TBuilder>(this TBuilder builder)
 					// Add euid cookie value to span attributes and baggage
 					if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid))
 					{
-						_ = activity.SetTag(Elastic.Documentation.ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid);
+						_ = activity.SetTag(ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid);
 						// Add to baggage so it propagates to all child spans
-						_ = activity.AddBaggage(Elastic.Documentation.ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid);
+						_ = activity.AddBaggage(ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid);
 					}
 				};
 			})
diff --git a/src/api/Elastic.Documentation.Api/ServicesExtension.cs b/src/api/Elastic.Documentation.Api/ServicesExtension.cs
index 436b457b20..520df0e6b9 100644
--- a/src/api/Elastic.Documentation.Api/ServicesExtension.cs
+++ b/src/api/Elastic.Documentation.Api/ServicesExtension.cs
@@ -5,14 +5,14 @@
 using System.ComponentModel.DataAnnotations;
 using Amazon.DynamoDBv2;
 using Elastic.Documentation.Api;
-using Elastic.Documentation.Api.AskAi;
-using Elastic.Documentation.Api.Changes;
-using Elastic.Documentation.Api.Telemetry;
 using Elastic.Documentation.Api.Adapters.AskAi;
 using Elastic.Documentation.Api.Adapters.Telemetry;
+using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Api.Caching;
+using Elastic.Documentation.Api.Changes;
 using Elastic.Documentation.Api.Gcp;
 using Elastic.Documentation.Api.Search;
+using Elastic.Documentation.Api.Telemetry;
 using Elastic.Documentation.Search;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
diff --git a/src/api/Elastic.Documentation.Api/TelemetryConstants.cs b/src/api/Elastic.Documentation.Api/TelemetryConstants.cs
index cabaf135b8..5e33d70fc2 100644
--- a/src/api/Elastic.Documentation.Api/TelemetryConstants.cs
+++ b/src/api/Elastic.Documentation.Api/TelemetryConstants.cs
@@ -13,7 +13,7 @@ public static class TelemetryConstants
 	/// Tag/baggage name used to annotate spans with the user's EUID value.
 	/// Forwarded from ServiceDefaults.Telemetry.TelemetryConstants for convenience.
 	/// </summary>
-	public const string UserEuidAttributeName = Elastic.Documentation.ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName;
+	public const string UserEuidAttributeName = ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName;
 
 	/// <summary>
 	/// ActivitySource name for AskAi operations.
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
index fa64d2215d..fd61e92110 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
@@ -6,9 +6,9 @@
 using System.Diagnostics;
 using System.Text.Json;
 using Elastic.Documentation.Assembler.Mcp;
-using Elastic.Documentation.Search;
 using Elastic.Documentation.Mcp.Remote.Responses;
 using Elastic.Documentation.Mcp.Remote.Telemetry;
+using Elastic.Documentation.Search;
 using Microsoft.Extensions.Logging;
 using ModelContextProtocol.Server;
 
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
index 274752873c..94778a98a2 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
@@ -6,9 +6,9 @@
 using System.Diagnostics;
 using System.Text.Json;
 using Elastic.Documentation.Assembler.Mcp;
-using Elastic.Documentation.Search;
 using Elastic.Documentation.Mcp.Remote.Responses;
 using Elastic.Documentation.Mcp.Remote.Telemetry;
+using Elastic.Documentation.Search;
 using Microsoft.Extensions.Logging;
 using ModelContextProtocol.Server;
 
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs
index b99d801702..2b2fc35712 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs
@@ -5,8 +5,8 @@
 using System.Net;
 using System.Text;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Api.Adapters.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Api.Gcp;
 using FakeItEasy;
 using Microsoft.Extensions.Configuration;
diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
index 9fa2064b16..7dc58a73b8 100644
--- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
+++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
@@ -3,11 +3,11 @@
 // See the LICENSE file in the project root for more information
 
 using System.Collections.Frozen;
-using Elastic.Documentation.Search;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Mcp.Remote.Gateways;
 using Elastic.Documentation.Mcp.Remote.Tools;
+using Elastic.Documentation.Search;
 using Elastic.Documentation.Search.Common;
 using Elastic.Documentation.ServiceDefaults;
 using Microsoft.Extensions.Logging.Abstractions;
diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs
index f36e62349e..111d5bc9a9 100644
--- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs
+++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs
@@ -6,8 +6,8 @@
 using System.Text;
 using System.Text.Json;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Api.Adapters.AskAi;
+using Elastic.Documentation.Api.AskAi;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Elastic.Documentation.Api.Infrastructure.Tests.Adapters.AskAi;

From e09ade66fabea8d9e456fed5d9a763209dee6f4d Mon Sep 17 00:00:00 2001
From: Martijn Laarman <Mpdreamz@gmail.com>
Date: Mon, 18 May 2026 12:26:12 +0200
Subject: [PATCH 3/6] Fix Dockerfile publish RID to use TARGETARCH/TARGETOS
 instead of linux-x64

Enables arm64 image builds to publish and copy the correct architecture
artifact rather than always using the amd64 binary.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---
 src/api/Elastic.Documentation.Api/Dockerfile | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/api/Elastic.Documentation.Api/Dockerfile b/src/api/Elastic.Documentation.Api/Dockerfile
index feb94b95dd..52b9699870 100644
--- a/src/api/Elastic.Documentation.Api/Dockerfile
+++ b/src/api/Elastic.Documentation.Api/Dockerfile
@@ -29,14 +29,14 @@ ENV DOTNET_NOLOGO=true \
 
 RUN arch=$TARGETARCH \
     && if [ "$arch" = "amd64" ]; then arch="x64"; fi \
-    && echo $TARGETOS-$arch > /tmp/rid
-
-RUN dotnet publish src/api/Elastic.Documentation.Api -r linux-x64 -c Release
+    && RID="${TARGETOS}-${arch}" \
+    && dotnet publish src/api/Elastic.Documentation.Api -r "${RID}" -c Release \
+    && cp -r ".artifacts/publish/Elastic.Documentation.Api/release_${RID}/." /app/publish/
 
 # Runtime stage
 FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal AS runtime
 WORKDIR /app
-COPY --from=base /app/.artifacts/publish/Elastic.Documentation.Api/release_linux-x64/ /app/
+COPY --from=base /app/publish/ /app/
 RUN chmod +x /app/docs-builder-api
 EXPOSE 8080
 ENV ASPNETCORE_URLS=http://+:8080

From c4cef7b4694705ad45eb78233103030f29fe8c8c Mon Sep 17 00:00:00 2001
From: Martijn Laarman <Mpdreamz@gmail.com>
Date: Mon, 18 May 2026 12:30:57 +0200
Subject: [PATCH 4/6] Address PR review: OTel source registration, assembly
 version, env var key

- Register McpTools ActivitySource in Mcp.Remote TracerProvider so MCP
  spans are captured
- Use GetEntryAssembly() over GetCallingAssembly() for service.version so
  the host service's version is reported rather than ServiceDefaults
- OtlpProxyOptions now checks OTEL_EXPORTER_OTLP_ENDPOINT first then
  OTLP_PROXY_ENDPOINT as backward-compatible fallback, matching the
  documented priority order

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---
 .../Telemetry/EuidEnrichmentExtensions.cs                | 2 +-
 .../Telemetry/OtlpProxyOptions.cs                        | 9 +++++----
 src/api/Elastic.Documentation.Mcp.Remote/Program.cs      | 4 ++++
 .../Telemetry/McpToolTelemetry.cs                        | 2 +-
 4 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs
index 69cedcec96..903ac7a416 100644
--- a/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs
+++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs
@@ -80,7 +80,7 @@ public static TBuilder AddEuidEnrichment<TBuilder>(this TBuilder builder) where
 	private static void ConfigureServiceVersionAttributes<TBuilder>(TBuilder builder)
 		where TBuilder : IHostApplicationBuilder
 	{
-		var informationalVersion = Assembly.GetCallingAssembly()
+		var informationalVersion = (Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly())
 			.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
 
 		if (informationalVersion is null)
diff --git a/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs
index c42bca4be0..6e355de68f 100644
--- a/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs
@@ -33,16 +33,17 @@ public class OtlpProxyOptions(IConfiguration configuration)
 	private static string ResolveEndpoint(IConfiguration configuration)
 	{
 		const string configKey = "OtlpProxy:Endpoint";
-		const string envVarKey = "OTLP_PROXY_ENDPOINT";
 		const string defaultEndpoint = "http://localhost:4318";
 
 		// Priority 1: Explicit configuration (for tests or custom deployments)
 		if (!string.IsNullOrEmpty(configuration[configKey]))
 			return configuration[configKey]!;
 
-		// Priority 2: Environment variable (ADOT Lambda Layer standard)
-		if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVarKey)))
-			return Environment.GetEnvironmentVariable(envVarKey)!;
+		// Priority 2: Standard OTEL env var, then legacy fallback
+		var endpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")
+			?? Environment.GetEnvironmentVariable("OTLP_PROXY_ENDPOINT");
+		if (!string.IsNullOrEmpty(endpoint))
+			return endpoint;
 
 		// Priority 3: Default (ADOT Lambda Layer collector)
 		return defaultEndpoint;
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
index 344c6e515c..d4e49ef05a 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
@@ -8,9 +8,11 @@
 using Elastic.Documentation.LinkIndex;
 using Elastic.Documentation.Links.InboundLinks;
 using Elastic.Documentation.Mcp.Remote;
+using Elastic.Documentation.Mcp.Remote.Telemetry;
 using Elastic.Documentation.Search.Common;
 using Elastic.Documentation.ServiceDefaults;
 using Elastic.Documentation.ServiceDefaults.Telemetry;
+using OpenTelemetry.Trace;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Diagnostics;
 using Microsoft.AspNetCore.Diagnostics.HealthChecks;
@@ -25,6 +27,8 @@
 	_ = builder.AddDocumentationServiceDefaults();
 	_ = builder.AddDefaultHealthChecks();
 	_ = builder.AddEuidEnrichment();
+	_ = builder.Services.ConfigureOpenTelemetryTracerProvider(t =>
+		t.AddSource(McpToolTelemetry.McpToolSourceName));
 
 	// Configure Kestrel to listen on port 8080 (standard container port)
 	_ = builder.WebHost.ConfigureKestrel(serverOptions =>
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs b/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs
index 0513da7239..3aa2424060 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs
@@ -11,7 +11,7 @@ namespace Elastic.Documentation.Mcp.Remote.Telemetry;
 
 public static class McpToolTelemetry
 {
-	private const string McpToolSourceName = "Elastic.Documentation.Api.McpTools";
+	internal const string McpToolSourceName = "Elastic.Documentation.Api.McpTools";
 	private static readonly ActivitySource McpActivitySource = new(McpToolSourceName);
 	private static readonly McpServerProfile ServerProfile = ResolveServerProfile();
 	private static readonly string? ServerVersion = ResolveServerVersion();

From 1622d3da3c8d751f50c52d59d1dd2543cf4a71a7 Mon Sep 17 00:00:00 2001
From: Martijn Laarman <Mpdreamz@gmail.com>
Date: Mon, 18 May 2026 12:32:25 +0200
Subject: [PATCH 5/6] Fix import ordering in Mcp.Remote Program.cs

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---
 src/api/Elastic.Documentation.Mcp.Remote/Program.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
index d4e49ef05a..291c61f9fc 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs
@@ -12,7 +12,6 @@
 using Elastic.Documentation.Search.Common;
 using Elastic.Documentation.ServiceDefaults;
 using Elastic.Documentation.ServiceDefaults.Telemetry;
-using OpenTelemetry.Trace;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Diagnostics;
 using Microsoft.AspNetCore.Diagnostics.HealthChecks;
@@ -20,6 +19,7 @@
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using ModelContextProtocol;
+using OpenTelemetry.Trace;
 
 try
 {

From 90965e50e2b3f2693320be9b1bc4fc20dfad308a Mon Sep 17 00:00:00 2001
From: Martijn Laarman <Mpdreamz@gmail.com>
Date: Tue, 19 May 2026 11:22:16 +0200
Subject: [PATCH 6/6] [2/5] VSA: Collapse Gateway+Usecase into *Service per
 feature (#3335)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---
 docs-builder.slnx                             |   6 +-
 .../Elasticsearch/OpenApiDocumentExporter.cs  |   3 +-
 .../Operations/AvailabilityBadgeHelper.cs     |   1 +
 .../Building/CodexBuildService.cs             |   1 +
 .../Assembler/AssemblyConfiguration.cs        |   1 +
 .../BuildContext.cs                           |   2 +-
 .../Builder/ConfigurationFile.cs              |   2 +-
 .../{ => Builder}/CrossLinkEntry.cs           |   2 +-
 .../{ => Builder}/DocSetRegistry.cs           |   2 +-
 .../Builder/RedirectFile.cs                   |   2 +-
 .../Changelog/ChangelogConfiguration.cs       |   1 +
 .../ChangelogConfigurationLoader.cs           |   7 +-
 .../Changelog}/ChangelogConfigurationYaml.cs  |  26 +--
 .../Changelog}/TypeEntryYamlConverter.cs      |   2 +-
 .../Changelog}/YamlLenientList.cs             |   4 +-
 .../Changelog}/YamlLenientListConverter.cs    |   2 +-
 ...Elastic.Documentation.Configuration.csproj |   2 +-
 .../Inference/VersionInference.cs             |   1 +
 .../ReleaseNotes/BundleLoader.cs              |   1 +
 .../LenientStringListConverter.cs             |  66 --------
 .../Serialization/YamlStaticContext.cs        |  15 ++
 .../SystemEnvironmentVariables.cs             |   2 +
 .../Toc/DocumentationSetFile.cs               |   2 +-
 .../Versions/VersionConfiguration.cs          |   1 +
 .../VersionsConfigurationExtensions.cs        |   1 +
 .../CrossLinks/CrossLinkFetcher.cs            |   1 +
 .../CrossLinks/CrossLinkResolver.cs           |   1 +
 .../InboundLinks/LinkIndexLinkChecker.cs      |   4 +-
 .../Elastic.Documentation.Tooling.csproj      |  27 ++++
 .../Exporter.cs                               |   5 +-
 .../ExternalCommandExecutor.cs                |   0
 .../ExternalCommands/NoopConsoleWriter.cs     |   0
 .../FileSystemFactory.cs                      |   2 +
 .../GitCheckoutInformationFactory.cs          | 149 +++++++++++++++++
 .../GlobalCliOptions.cs                       |   2 +
 .../Paths.cs                                  |   2 +
 .../AppliesTo/Applicability.cs                |   4 +-
 .../AppliesTo/ApplicabilitySelector.cs        |   2 +
 .../AppliesTo/ApplicableTo.cs                 |  96 +++++++++--
 .../AppliesTo/ApplicableToJsonConverter.cs    |  11 +-
 .../AppliesTo/ProductApplicability.cs         |  51 +++---
 .../AppliesTo/ProductLifecycle.cs             |  21 ++-
 .../Diagnostics/DiagnosticsCollector.cs       |   7 +-
 .../Diagnostics/IDiagnosticsCollector.cs      |   6 +-
 .../Elastic.Documentation.csproj              |  15 +-
 .../GitCheckoutInformation.cs                 | 151 +-----------------
 .../IEnvironmentVariables.cs                  |   2 +-
 .../ChangelogEntrySubtype.cs                  |   2 +-
 .../{ => ReleaseNotes}/ChangelogEntryType.cs  |   2 +-
 .../Serialization/SourceGenerationContext.cs  |   2 +
 .../SymlinkValidator.cs                       |   2 +-
 .../{ => Versions}/SemVersion.cs              |   3 +-
 .../{ => Versions}/VersionOrDate.cs           |   2 +-
 .../{ => Versions}/VersionSpec.cs             |   2 +-
 src/Elastic.Markdown/Elastic.Markdown.csproj  |   1 +
 .../ElasticsearchMarkdownExporter.Export.cs   |   2 +-
 .../AppliesTo/ApplicableToYamlConverter.cs    |   3 +
 .../Myst/AppliesTo/SemVersionConverter.cs}    |   3 +
 .../Myst/Components/ApplicabilityRenderer.cs  |   1 +
 .../Myst/Components/ApplicableToViewModel.cs  |   1 +
 .../Directives/Changelog/ChangelogBlock.cs    |   1 +
 .../Changelog/ChangelogInlineRenderer.cs      |   1 +
 .../Directives/CsvInclude/CsvIncludeBlock.cs  |   1 +
 .../Myst/Directives/Include/IncludeBlock.cs   |   1 +
 .../Myst/Directives/Settings/SettingsBlock.cs |   1 +
 .../Myst/Directives/Version/VersionBlock.cs   |   1 +
 .../SubstitutionMutationHelper.cs             |   1 +
 .../Myst/Roles/AppliesTo/AppliesToRole.cs     |   1 +
 .../Myst/YamlSerialization.cs                 |   1 -
 .../AskAi/AgentBuilderAskAiGateway.cs         |   2 +-
 .../Adapters/AskAi/AskAiGatewayFactory.cs     |   6 +-
 ...lasticsearchAskAiMessageFeedbackGateway.cs |   2 +-
 .../Adapters/AskAi/LlmGatewayAskAiGateway.cs  |   2 +-
 .../AskAi/AskAiMessageFeedbackUsecase.cs      |  43 -----
 .../AskAi/AskAiUsecase.cs                     |  57 -------
 .../AskAi/IAskAiGateway.cs                    |   4 +-
 .../AskAi/IAskAiMessageFeedbackGateway.cs     |   4 +-
 .../Changes/ChangesUsecase.cs                 | 116 --------------
 .../Elastic.Documentation.Api.csproj          |   2 +-
 .../MappingsExtensions.cs                     |  86 +++++++---
 src/api/Elastic.Documentation.Api/Program.cs  |   2 +-
 .../Search/FullSearchUsecase.cs               |  97 -----------
 .../Search/NavigationSearchUsecase.cs         |  73 ---------
 .../SerializationContext.cs                   |  12 +-
 .../ServicesExtension.cs                      |  66 ++++----
 .../AdotOtlpService.cs}                       |  13 +-
 .../{IOtlpGateway.cs => IOtlpService.cs}      |   4 +-
 .../Telemetry/OtlpProxyUsecase.cs             |  36 -----
 .../Elastic.Documentation.Mcp.Remote.csproj   |   2 +-
 .../McpBearerAuthMiddleware.cs                |   1 +
 .../McpOAuthMetadata.cs                       |   1 +
 .../Tools/CoherenceTools.cs                   |   8 +-
 .../Tools/SearchTools.cs                      |   4 +-
 .../Bundling/ChangelogBundleAmendService.cs   |   1 -
 .../Bundling/ChangelogBundlingService.cs      |   1 -
 .../Bundling/ChangelogRemoveService.cs        |   1 -
 .../Creation/ChangelogCreationService.cs      |   2 +-
 .../ChangelogPrEvaluationService.cs           |   1 -
 .../ChangelogPrepareArtifactService.cs        |   2 +-
 .../GitHubReleaseChangelogService.cs          |   1 -
 .../Asciidoc/ChangelogAsciidocRenderer.cs     |   2 +-
 .../Rendering/ChangelogRenderingService.cs    |   2 +-
 .../BreakingChangesMarkdownRenderer.cs        |   2 +-
 .../Markdown/ChangelogGfmRenderer.cs          |   2 +-
 .../Markdown/DeprecationsMarkdownRenderer.cs  |   2 +-
 .../Markdown/IndexMarkdownRenderer.cs         |   2 +-
 .../Markdown/KnownIssuesMarkdownRenderer.cs   |   2 +-
 .../ChangelogYamlStaticContext.cs             |  24 ---
 .../Uploading/ChangelogUploadService.cs       |   2 +-
 .../Building/AssemblerBuildService.cs         |   1 +
 .../Navigation/GlobalNavigationService.cs     |   2 +-
 .../IsolatedBuildService.cs                   |   1 +
 .../AppliesToEntry.cs                         |  28 ++++
 .../DocumentationDocument.cs                  |   3 +-
 .../DocumentationMappingConfig.cs             |   0
 ...astic.Documentation.Search.Contract.csproj |  17 ++
 .../IndexedProduct.cs                         |   0
 .../ChangesService.cs}                        |  99 +++++++++++-
 .../Common/ElasticsearchClientAccessor.cs     |   0
 .../Common/SearchQueryBuilder.cs              |   0
 .../Common/SearchResultProcessor.cs           |   0
 .../Elastic.Documentation.Search.csproj       |   4 +-
 .../EsJsonContext.cs                          |   0
 .../FullSearchService.cs}                     |  42 ++++-
 .../IChangesService.cs}                       |  22 ++-
 .../IFullSearchService.cs}                    |  24 ++-
 .../INavigationSearchService.cs}              |  35 +++-
 .../MockSearchService.cs}                     |  42 +++--
 .../NavigationSearchService.cs}               |  34 +++-
 .../ServicesExtension.cs                      |   8 +-
 .../SharedPointInTimeManager.cs               |   0
 .../StringHighlightExtensions.cs              |   0
 .../docs-builder/Commands/ChangelogCommand.cs |   3 +-
 .../docs-builder/Http/DocumentationWebHost.cs |   5 +-
 .../docs-builder/Http/StaticWebHost.cs        |   2 +-
 .../Middleware/CheckForUpdatesMiddleware.cs   |   1 +
 .../Search/SearchIntegrationTests.cs          |  14 +-
 .../TestHelpers.cs                            |   1 +
 .../EuidEnrichmentIntegrationTests.cs         |   9 +-
 .../OtlpProxyIntegrationTests.cs              |  10 +-
 .../Mcp.Remote.IntegrationTests.csproj        |   2 +-
 .../McpToolsIntegrationTestsBase.cs           |   6 +-
 .../Search.IntegrationTests.csproj            |   2 +-
 .../SearchRelevanceTests.cs                   |   4 +-
 .../OpenApiDocumentExporterTests.cs           |   1 +
 .../Elastic.ApiExplorer.Tests/TestHelpers.cs  |   1 +
 .../Changelogs/ChangelogConfigurationTests.cs |   4 +-
 .../Changelogs/ChangelogTestBase.cs           |   1 +
 .../Create/CreateChangelogTestBase.cs         |   1 +
 .../Create/ReleaseNoteExtractionTests.cs      |   1 +
 .../Changelogs/Render/ErrorHandlingTests.cs   |   2 +-
 .../Creation/CIEnrichmentTests.cs             |   1 +
 .../Creation/ChangelogCreationServiceTests.cs |   1 +
 ...umentation.Api.Infrastructure.Tests.csproj |   2 +-
 .../MockEnvironmentVariables.cs               |   1 +
 .../SymlinkValidationTests.cs                 |   1 +
 .../TestHelpers.cs                            |   1 +
 .../ApiConfigurationTests.cs                  |   2 +-
 .../AssemblyConfigurationMatchTests.cs        |   1 +
 .../ConfigurationFileExcludeTests.cs          |   2 +-
 .../CrossLinkRegistryTests.cs                 |   2 +-
 .../DocumentInferrerServiceTests.cs           |   1 +
 ...c.Documentation.Configuration.Tests.csproj |   1 +
 .../ReleaseNotes/VersionOrDateTests.cs        |   1 +
 .../VersionInferenceTests.cs                  |   1 +
 ...ApplicableToJsonConverterRoundTripTests.cs |   1 +
 ...icableToJsonConverterSerializationTests.cs |   1 +
 .../ProductApplicabilityToStringTests.cs      |  15 +-
 .../CrossLinks/UriEnvironmentResolverTests.cs |   1 +
 .../Directives/VersionTests.cs                |   1 +
 .../DocSet/RepositoryLinksTests.cs            |   2 +-
 .../Elastic.Markdown.Tests.csproj             |   1 +
 ...DocumentationDocumentSerializationTests.cs |  41 ++---
 tests/Elastic.Markdown.Tests/TestHelpers.cs   |   1 +
 tests/authoring/Framework/Setup.fs            |   1 +
 175 files changed, 950 insertions(+), 1006 deletions(-)
 rename src/Elastic.Documentation.Configuration/{ => Builder}/CrossLinkEntry.cs (91%)
 rename src/Elastic.Documentation.Configuration/{ => Builder}/DocSetRegistry.cs (93%)
 rename src/{services/Elastic.Changelog/Configuration => Elastic.Documentation.Configuration/Changelog}/ChangelogConfigurationLoader.cs (99%)
 rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/ChangelogConfigurationYaml.cs (95%)
 rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/TypeEntryYamlConverter.cs (98%)
 rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/YamlLenientList.cs (86%)
 rename src/{services/Elastic.Changelog/Serialization => Elastic.Documentation.Configuration/Changelog}/YamlLenientListConverter.cs (98%)
 delete mode 100644 src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs
 create mode 100644 src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj
 rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/Exporter.cs (56%)
 rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/ExternalCommands/ExternalCommandExecutor.cs (100%)
 rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/ExternalCommands/NoopConsoleWriter.cs (100%)
 rename src/{Elastic.Documentation.Configuration => Elastic.Documentation.Tooling}/FileSystemFactory.cs (98%)
 create mode 100644 src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs
 rename src/{Elastic.Documentation => Elastic.Documentation.Tooling}/GlobalCliOptions.cs (84%)
 rename src/{Elastic.Documentation.Configuration => Elastic.Documentation.Tooling}/Paths.cs (98%)
 rename src/{Elastic.Documentation.Configuration => Elastic.Documentation}/IEnvironmentVariables.cs (98%)
 rename src/Elastic.Documentation/{ => ReleaseNotes}/ChangelogEntrySubtype.cs (96%)
 rename src/Elastic.Documentation/{ => ReleaseNotes}/ChangelogEntryType.cs (97%)
 rename src/{Elastic.Documentation.Configuration => Elastic.Documentation}/SymlinkValidator.cs (98%)
 rename src/Elastic.Documentation/{ => Versions}/SemVersion.cs (99%)
 rename src/Elastic.Documentation/{ => Versions}/VersionOrDate.cs (99%)
 rename src/Elastic.Documentation/{ => Versions}/VersionSpec.cs (99%)
 rename src/{Elastic.Documentation => Elastic.Markdown/Myst}/AppliesTo/ApplicableToYamlConverter.cs (98%)
 rename src/{Elastic.Documentation/AppliesTo/AllVersions.cs => Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs} (87%)
 delete mode 100644 src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs
 delete mode 100644 src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs
 delete mode 100644 src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs
 delete mode 100644 src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs
 delete mode 100644 src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs
 rename src/api/Elastic.Documentation.Api/{Adapters/Telemetry/AdotOtlpGateway.cs => Telemetry/AdotOtlpService.cs} (87%)
 rename src/api/Elastic.Documentation.Api/Telemetry/{IOtlpGateway.cs => IOtlpService.cs} (90%)
 delete mode 100644 src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs
 delete mode 100644 src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs
 create mode 100644 src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs
 rename src/{Elastic.Documentation/Search => services/search/Elastic.Documentation.Search.Contract}/DocumentationDocument.cs (98%)
 rename src/{Elastic.Documentation/Search => services/search/Elastic.Documentation.Search.Contract}/DocumentationMappingConfig.cs (100%)
 create mode 100644 src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj
 rename src/{Elastic.Documentation/Search => services/search/Elastic.Documentation.Search.Contract}/IndexedProduct.cs (100%)
 rename src/services/{Elastic.Documentation.Search/ChangesGateway.cs => search/Elastic.Documentation.Search/ChangesService.cs} (60%)
 rename src/services/{ => search}/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs (100%)
 rename src/services/{ => search}/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs (100%)
 rename src/services/{ => search}/Elastic.Documentation.Search/Common/SearchResultProcessor.cs (100%)
 rename src/services/{ => search}/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj (74%)
 rename src/services/{ => search}/Elastic.Documentation.Search/EsJsonContext.cs (100%)
 rename src/services/{Elastic.Documentation.Search/FullSearchGateway.cs => search/Elastic.Documentation.Search/FullSearchService.cs} (91%)
 rename src/services/{Elastic.Documentation.Search/IChangesGateway.cs => search/Elastic.Documentation.Search/IChangesService.cs} (68%)
 rename src/services/{Elastic.Documentation.Search/IFullSearchGateway.cs => search/Elastic.Documentation.Search/IFullSearchService.cs} (79%)
 rename src/services/{Elastic.Documentation.Search/INavigationSearchGateway.cs => search/Elastic.Documentation.Search/INavigationSearchService.cs} (50%)
 rename src/services/{Elastic.Documentation.Search/MockSearchGateway.cs => search/Elastic.Documentation.Search/MockSearchService.cs} (73%)
 rename src/services/{Elastic.Documentation.Search/NavigationSearchGateway.cs => search/Elastic.Documentation.Search/NavigationSearchService.cs} (87%)
 rename src/services/{ => search}/Elastic.Documentation.Search/ServicesExtension.cs (85%)
 rename src/services/{ => search}/Elastic.Documentation.Search/SharedPointInTimeManager.cs (100%)
 rename src/services/{ => search}/Elastic.Documentation.Search/StringHighlightExtensions.cs (100%)

diff --git a/docs-builder.slnx b/docs-builder.slnx
index f5a36e725b..12215d0721 100644
--- a/docs-builder.slnx
+++ b/docs-builder.slnx
@@ -53,6 +53,7 @@
     <Project Path="src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj" />
     <Project Path="src/Elastic.Documentation.Svg/Elastic.Documentation.Svg.csproj" />
     <Project Path="src/Elastic.Documentation/Elastic.Documentation.csproj" />
+    <Project Path="src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj" />
     <Project Path="src/Elastic.Markdown/Elastic.Markdown.csproj" />
   </Folder>
   <Folder Name="/src/api/">
@@ -71,7 +72,10 @@
     <Project Path="src/services/Elastic.Documentation.Isolated/Elastic.Documentation.Isolated.csproj" />
     <Project Path="src/services/Elastic.Documentation.Integrations/Elastic.Documentation.Integrations.csproj" />
     <Project Path="src/services/Elastic.Documentation.Services/Elastic.Documentation.Services.csproj" />
-	<Project Path="src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj" />
+  </Folder>
+  <Folder Name="/src/services/search/">
+    <Project Path="src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj" />
+    <Project Path="src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj" />
   </Folder>
   <Folder Name="/src/tooling/">
     <File Path="src/tooling/Directory.Build.props" />
diff --git a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
index 8c17cc0223..38a90d901e 100644
--- a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
+++ b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
@@ -11,6 +11,7 @@
 using Elastic.Documentation.Configuration.Inference;
 using Elastic.Documentation.Configuration.Versions;
 using Elastic.Documentation.Search;
+using Elastic.Documentation.Versions;
 using Microsoft.OpenApi;
 using Microsoft.OpenApi.Reader;
 
@@ -177,7 +178,7 @@ private IEnumerable<DocumentationDocument> ConvertToDocuments(OpenApiDocument op
 					StrippedBody = body,
 					Headings = headings,
 					Links = [],
-					Applies = applies,
+					Applies = applies?.ToAppliesTo(),
 					Parents =
 					[
 						new ParentDocument { Title = "API Reference", Url = "/docs/api" },
diff --git a/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs b/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs
index bd1b0cb869..ba8cbf3cdd 100644
--- a/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs
+++ b/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs
@@ -8,6 +8,7 @@
 using Elastic.Documentation.AppliesTo;
 using Elastic.Documentation.Configuration.Versions;
 using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.Versions;
 using Microsoft.OpenApi;
 
 namespace Elastic.ApiExplorer.Operations;
diff --git a/src/Elastic.Codex/Building/CodexBuildService.cs b/src/Elastic.Codex/Building/CodexBuildService.cs
index 5d2efb8222..8a5ba281d0 100644
--- a/src/Elastic.Codex/Building/CodexBuildService.cs
+++ b/src/Elastic.Codex/Building/CodexBuildService.cs
@@ -10,6 +10,7 @@
 using Elastic.Codex.Sourcing;
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Builder;
 using Elastic.Documentation.Configuration.Codex;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.Isolated;
diff --git a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs
index 133ab98db6..f9833a0772 100644
--- a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs
+++ b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs
@@ -5,6 +5,7 @@
 using System.Text.RegularExpressions;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Extensions;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging;
 using YamlDotNet.Serialization;
 
diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs
index 28cf823d1e..5ea7d914a9 100644
--- a/src/Elastic.Documentation.Configuration/BuildContext.cs
+++ b/src/Elastic.Documentation.Configuration/BuildContext.cs
@@ -117,7 +117,7 @@ public BuildContext(
 		if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName)
 			DocumentationSourceDirectory = ConfigurationPath.Directory!;
 
-		Git = gitCheckoutInformation ?? GitCheckoutInformation.Create(DocumentationCheckoutDirectory, ReadFileSystem);
+		Git = gitCheckoutInformation ?? GitCheckoutInformationFactory.Create(DocumentationCheckoutDirectory, ReadFileSystem);
 
 		// Load and resolve the docset file, or create an empty one if it doesn't exist
 		ConfigurationYaml = ConfigurationPath.Exists
diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs
index d3a0b8eec8..f20055f83b 100644
--- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs
+++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs
@@ -11,7 +11,7 @@
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.Extensions;
 using Elastic.Documentation.Links;
-using static Elastic.Documentation.Configuration.SymlinkValidator;
+using static Elastic.Documentation.SymlinkValidator;
 
 namespace Elastic.Documentation.Configuration.Builder;
 
diff --git a/src/Elastic.Documentation.Configuration/CrossLinkEntry.cs b/src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs
similarity index 91%
rename from src/Elastic.Documentation.Configuration/CrossLinkEntry.cs
rename to src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs
index 9dc66af13d..88e2e591d4 100644
--- a/src/Elastic.Documentation.Configuration/CrossLinkEntry.cs
+++ b/src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Configuration;
+namespace Elastic.Documentation.Configuration.Builder;
 
 /// <summary>
 /// A parsed cross-link entry from docset.yml, with the target registry for lookup.
diff --git a/src/Elastic.Documentation.Configuration/DocSetRegistry.cs b/src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs
similarity index 93%
rename from src/Elastic.Documentation.Configuration/DocSetRegistry.cs
rename to src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs
index cfcf9bbf23..ae06ae372d 100644
--- a/src/Elastic.Documentation.Configuration/DocSetRegistry.cs
+++ b/src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs
@@ -5,7 +5,7 @@
 using System.ComponentModel.DataAnnotations;
 using NetEscapades.EnumGenerators;
 
-namespace Elastic.Documentation.Configuration;
+namespace Elastic.Documentation.Configuration.Builder;
 
 /// <summary>
 /// Registry type for cross-link resolution. Maps to the link index source:
diff --git a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs
index ca43dadb84..ebb6a12597 100644
--- a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs
+++ b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs
@@ -5,7 +5,7 @@
 using System.IO.Abstractions;
 using Elastic.Documentation.Links;
 using YamlDotNet.RepresentationModel;
-using static Elastic.Documentation.Configuration.SymlinkValidator;
+using static Elastic.Documentation.SymlinkValidator;
 
 namespace Elastic.Documentation.Configuration.Builder;
 
diff --git a/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs
index 6b52229a0a..f24c55019c 100644
--- a/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs
+++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using Elastic.Documentation.Configuration.Products;
+using Elastic.Documentation.ReleaseNotes;
 
 namespace Elastic.Documentation.Configuration.Changelog;
 
diff --git a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs
similarity index 99%
rename from src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs
rename to src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs
index 4c866be716..38a73ee728 100644
--- a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs
+++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs
@@ -3,11 +3,10 @@
 // See the LICENSE file in the project root for more information
 
 using System.IO.Abstractions;
-using Elastic.Changelog.Serialization;
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
-using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Configuration.Products;
+using Elastic.Documentation.Configuration.Serialization;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.ReleaseNotes;
 using Microsoft.Extensions.Logging;
@@ -15,7 +14,7 @@
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
 
-namespace Elastic.Changelog.Configuration;
+namespace Elastic.Documentation.Configuration.Changelog;
 
 /// <summary>
 /// Service for loading and validating changelog configuration
@@ -25,7 +24,7 @@ public class ChangelogConfigurationLoader(ILoggerFactory logFactory, IConfigurat
 	private readonly ILogger _logger = logFactory.CreateLogger<ChangelogConfigurationLoader>();
 
 	private static readonly IDeserializer ConfigurationDeserializer =
-		new StaticDeserializerBuilder(new ChangelogYamlStaticContext())
+		new StaticDeserializerBuilder(new YamlStaticContext())
 			.WithNamingConvention(UnderscoredNamingConvention.Instance)
 			.WithTypeConverter(new YamlLenientListConverter())
 			.WithTypeConverter(new TypeEntryYamlConverter())
diff --git a/src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs
similarity index 95%
rename from src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs
rename to src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs
index 03c56e9b8e..80521f6742 100644
--- a/src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs
+++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs
@@ -2,13 +2,13 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Changelog.Serialization;
+namespace Elastic.Documentation.Configuration.Changelog;
 
 /// <summary>
 /// Internal DTO for YAML deserialization of changelog configuration.
 /// Maps directly to the YAML file structure.
 /// </summary>
-internal record ChangelogConfigurationYaml
+internal sealed record ChangelogConfigurationYaml
 {
 	/// <summary>
 	/// Filename strategy for generated changelog files (pr, issue, timestamp).
@@ -57,7 +57,7 @@ internal record ChangelogConfigurationYaml
 /// <summary>
 /// Internal DTO for rules configuration in YAML.
 /// </summary>
-internal record RulesConfigurationYaml
+internal sealed record RulesConfigurationYaml
 {
 	/// <summary>
 	/// Global match mode for multi-valued fields ("any" or "all"). Default: "any".
@@ -83,7 +83,7 @@ internal record RulesConfigurationYaml
 /// <summary>
 /// Internal DTO for bundle rules in YAML.
 /// </summary>
-internal record BundleRulesYaml
+internal sealed record BundleRulesYaml
 {
 	/// <summary>
 	/// Product IDs to exclude from the bundle (string or list). Cannot be combined with IncludeProducts.
@@ -134,7 +134,7 @@ internal record BundleRulesYaml
 /// <summary>
 /// Internal DTO for create rules in YAML.
 /// </summary>
-internal record CreateRulesYaml
+internal sealed record CreateRulesYaml
 {
 	/// <summary>
 	/// Labels to exclude (string or list). Cannot be combined with Include.
@@ -161,7 +161,7 @@ internal record CreateRulesYaml
 /// <summary>
 /// Internal DTO for publish rules in YAML.
 /// </summary>
-internal record PublishRulesYaml
+internal sealed record PublishRulesYaml
 {
 	/// <summary>
 	/// Match mode for areas ("any" or "all"). Inherits from rules.match if not set.
@@ -198,7 +198,7 @@ internal record PublishRulesYaml
 /// <summary>
 /// Internal DTO for pivot configuration in YAML.
 /// </summary>
-internal record PivotConfigurationYaml
+internal sealed record PivotConfigurationYaml
 {
 	/// <summary>
 	/// Type definitions with optional labels and subtypes.
@@ -231,7 +231,7 @@ internal record PivotConfigurationYaml
 /// <summary>
 /// Internal DTO for products configuration in YAML.
 /// </summary>
-internal record ProductsConfigYaml
+internal sealed record ProductsConfigYaml
 {
 	/// <summary>
 	/// List of available product IDs (string or list, empty = all from products.yml).
@@ -247,7 +247,7 @@ internal record ProductsConfigYaml
 /// <summary>
 /// Internal DTO for default product specification in YAML.
 /// </summary>
-internal record DefaultProductYaml
+internal sealed record DefaultProductYaml
 {
 	/// <summary>
 	/// Product ID.
@@ -263,7 +263,7 @@ internal record DefaultProductYaml
 /// <summary>
 /// Internal DTO for bundle configuration in YAML.
 /// </summary>
-internal record BundleConfigurationYaml
+internal sealed record BundleConfigurationYaml
 {
 	/// <summary>
 	/// Input directory containing changelog YAML files.
@@ -314,7 +314,7 @@ internal record BundleConfigurationYaml
 /// <summary>
 /// Internal DTO for bundle profile in YAML.
 /// </summary>
-internal record BundleProfileYaml
+internal sealed record BundleProfileYaml
 {
 	/// <summary>
 	/// Product filter pattern for input changelogs.
@@ -371,7 +371,7 @@ internal record BundleProfileYaml
 /// <summary>
 /// Internal DTO for extract configuration in YAML.
 /// </summary>
-internal record ExtractConfigurationYaml
+internal sealed record ExtractConfigurationYaml
 {
 	/// <summary>
 	/// Whether to extract release note text from PR or issue descriptions for the changelog entry description by default.
@@ -397,7 +397,7 @@ internal record ExtractConfigurationYaml
 /// Internal DTO for type entry in YAML.
 /// Can represent either a simple label string or an object with labels and subtypes.
 /// </summary>
-internal record TypeEntryYaml
+internal sealed record TypeEntryYaml
 {
 	/// <summary>
 	/// Labels for this type (comma-separated string).
diff --git a/src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs b/src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs
similarity index 98%
rename from src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs
rename to src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs
index 57c328b24f..135a5ffd78 100644
--- a/src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs
+++ b/src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs
@@ -6,7 +6,7 @@
 using YamlDotNet.Core.Events;
 using YamlDotNet.Serialization;
 
-namespace Elastic.Changelog.Serialization;
+namespace Elastic.Documentation.Configuration.Changelog;
 
 /// <summary>
 /// YAML type converter for TypeEntryYaml that handles both string and object forms.
diff --git a/src/services/Elastic.Changelog/Serialization/YamlLenientList.cs b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs
similarity index 86%
rename from src/services/Elastic.Changelog/Serialization/YamlLenientList.cs
rename to src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs
index 724f8538fb..58c77ce908 100644
--- a/src/services/Elastic.Changelog/Serialization/YamlLenientList.cs
+++ b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs
@@ -2,14 +2,14 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Changelog.Serialization;
+namespace Elastic.Documentation.Configuration.Changelog;
 
 /// <summary>
 /// Wrapper type for YAML fields that can be specified as either a comma-separated string
 /// or a YAML list/sequence. Deserialized by <see cref="YamlLenientListConverter"/>.
 /// Uses mutable property for compatibility with YamlDotNet source generator.
 /// </summary>
-internal class YamlLenientList
+internal sealed class YamlLenientList
 {
 	public List<string>? Values { get; set; }
 
diff --git a/src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs
similarity index 98%
rename from src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs
rename to src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs
index 49bbbb2d53..8facb13be3 100644
--- a/src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs
+++ b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs
@@ -6,7 +6,7 @@
 using YamlDotNet.Core.Events;
 using YamlDotNet.Serialization;
 
-namespace Elastic.Changelog.Serialization;
+namespace Elastic.Documentation.Configuration.Changelog;
 
 /// <summary>
 /// YAML type converter for <see cref="YamlLenientList"/> that accepts both string and list forms.
diff --git a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
index 8796dc5fd7..ceb479fab7 100644
--- a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
+++ b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
@@ -13,6 +13,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\Elastic.Documentation\Elastic.Documentation.csproj" />
+    <ProjectReference Include="..\Elastic.Documentation.Tooling\Elastic.Documentation.Tooling.csproj" />
   </ItemGroup>
 
   <ItemGroup>
@@ -20,7 +21,6 @@
     <PackageReference Include="NetEscapades.EnumGenerators" />
     <PackageReference Include="Nullean.Argh.Interfaces" />
     <PackageReference Include="Nullean.ScopedFileSystem" />
-    <PackageReference Include="System.IO.Abstractions.TestingHelpers" />
     <PackageReference Include="Samboy063.Tomlet" />
     <PackageReference Include="Vecc.YamlDotNet.Analyzers.StaticGenerator" />
     <PackageReference Include="YamlDotNet" />
diff --git a/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs b/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs
index 68f9b6ffd4..c7d176f59a 100644
--- a/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs
+++ b/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs
@@ -6,6 +6,7 @@
 using Elastic.Documentation.Configuration.LegacyUrlMappings;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.Configuration.Inference;
 
diff --git a/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs b/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs
index 7956d58190..9dabf2d9ed 100644
--- a/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs
+++ b/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs
@@ -5,6 +5,7 @@
 using System.IO.Abstractions;
 using System.Text.RegularExpressions;
 using Elastic.Documentation.ReleaseNotes;
+using Elastic.Documentation.Versions;
 using YamlDotNet.Core;
 
 namespace Elastic.Documentation.Configuration.ReleaseNotes;
diff --git a/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs b/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs
deleted file mode 100644
index 9b10e71934..0000000000
--- a/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using YamlDotNet.Core;
-using YamlDotNet.Core.Events;
-using YamlDotNet.Serialization;
-
-namespace Elastic.Documentation.Configuration.Serialization;
-
-/// <summary>
-/// YAML type converter for <c>List&lt;string&gt;</c> that accepts both comma-separated strings and YAML sequences.
-/// Used by the minimal changelog DTO deserializer so that publish blocker fields like <c>types</c> and <c>areas</c>
-/// can be specified as either <c>"deprecation, known-issue"</c> or a YAML list.
-/// </summary>
-public class LenientStringListConverter : IYamlTypeConverter
-{
-	public bool Accepts(Type type) => type == typeof(List<string>);
-
-	public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
-	{
-		if (parser.TryConsume<Scalar>(out var scalar))
-		{
-			if (string.IsNullOrEmpty(scalar.Value) || scalar.Value == "~")
-				return null;
-
-			var items = scalar.Value
-				.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
-				.ToList();
-
-			return items.Count > 0 ? items : null;
-		}
-
-		if (parser.TryConsume<SequenceStart>(out _))
-		{
-			var items = new List<string>();
-
-			while (!parser.TryConsume<SequenceEnd>(out _))
-			{
-				var item = parser.Consume<Scalar>();
-				if (!string.IsNullOrWhiteSpace(item.Value))
-					items.Add(item.Value.Trim());
-			}
-
-			return items.Count > 0 ? items : null;
-		}
-
-		return null;
-	}
-
-	public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
-	{
-		if (value is not List<string> { Count: > 0 } items)
-		{
-			emitter.Emit(new Scalar(null, null, string.Empty, ScalarStyle.Plain, true, false));
-			return;
-		}
-
-		emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block));
-
-		foreach (var item in items)
-			emitter.Emit(new Scalar(null, null, item, ScalarStyle.Plain, true, false));
-
-		emitter.Emit(new SequenceEnd());
-	}
-}
diff --git a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs
index 3bb5d61f93..21cab10863 100644
--- a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs
+++ b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using Elastic.Documentation.Configuration.Assembler;
+using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Configuration.Codex;
 using Elastic.Documentation.Configuration.LegacyUrlMappings;
 using Elastic.Documentation.Configuration.Products;
@@ -61,4 +62,18 @@ namespace Elastic.Documentation.Configuration.Serialization;
 [YamlSerializable(typeof(ChangelogConfigMinimalDto))]
 [YamlSerializable(typeof(RulesConfigMinimalDto))]
 [YamlSerializable(typeof(PublishRulesMinimalDto))]
+// Changelog YAML DTOs for CLI configuration (changelog.yml)
+[YamlSerializable(typeof(ChangelogConfigurationYaml))]
+[YamlSerializable(typeof(PivotConfigurationYaml))]
+[YamlSerializable(typeof(TypeEntryYaml))]
+[YamlSerializable(typeof(RulesConfigurationYaml))]
+[YamlSerializable(typeof(CreateRulesYaml))]
+[YamlSerializable(typeof(BundleRulesYaml))]
+[YamlSerializable(typeof(PublishRulesYaml))]
+[YamlSerializable(typeof(ProductsConfigYaml))]
+[YamlSerializable(typeof(DefaultProductYaml))]
+[YamlSerializable(typeof(BundleConfigurationYaml))]
+[YamlSerializable(typeof(BundleProfileYaml))]
+[YamlSerializable(typeof(ExtractConfigurationYaml))]
+[YamlSerializable(typeof(YamlLenientList))]
 public partial class YamlStaticContext;
diff --git a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs
index b8e2d010d9..c9a7228981 100644
--- a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs
+++ b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs
@@ -2,6 +2,8 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation;
+
 namespace Elastic.Documentation.Configuration;
 
 /// <summary>
diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
index 78e024151a..7e06c6c24d 100644
--- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
+++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs
@@ -10,7 +10,7 @@
 using Elastic.Documentation.Extensions;
 using Nullean.ScopedFileSystem;
 using YamlDotNet.Serialization;
-using static Elastic.Documentation.Configuration.SymlinkValidator;
+using static Elastic.Documentation.SymlinkValidator;
 
 namespace Elastic.Documentation.Configuration.Toc;
 
diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs
index f3ab2edf5d..48af422a4e 100644
--- a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs
+++ b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using System.ComponentModel.DataAnnotations;
+using Elastic.Documentation.Versions;
 using NetEscapades.EnumGenerators;
 using YamlDotNet.Serialization;
 
diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs
index 290b88c752..3458a686ea 100644
--- a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs
+++ b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs
@@ -2,6 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation.Versions;
 using YamlDotNet.Serialization;
 
 namespace Elastic.Documentation.Configuration.Versions;
diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
index 2c54f1d374..d4dc8d5cca 100644
--- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
+++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs
@@ -7,6 +7,7 @@
 using System.IO.Abstractions;
 using System.Text.Json;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Builder;
 using Elastic.Documentation.LinkIndex;
 using Elastic.Documentation.Serialization;
 using Microsoft.Extensions.Logging;
diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs
index abaf584c2b..d329393b40 100644
--- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs
+++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs
@@ -5,6 +5,7 @@
 using System.Collections.Frozen;
 using System.Diagnostics.CodeAnalysis;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Builder;
 
 namespace Elastic.Documentation.Links.CrossLinks;
 
diff --git a/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs b/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs
index 8e65d8137f..fc1ae6e2aa 100644
--- a/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs
+++ b/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs
@@ -38,7 +38,7 @@ public async Task<bool> CheckRepository(IDiagnosticsCollector collector, string?
 		var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
 		if (fromRepository == null && toRepository == null)
 		{
-			fromRepository ??= GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName;
+			fromRepository ??= GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName;
 			if (fromRepository == null)
 				throw new Exception("Unable to determine repository name");
 		}
@@ -58,7 +58,7 @@ public async Task<bool> CheckWithLocalLinksJson(IDiagnosticsCollector collector,
 	{
 		file ??= ".artifacts/docs/html/links.json";
 		var root = !string.IsNullOrEmpty(path) ? fileSystem.DirectoryInfo.New(path) : fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
-		var repository = GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
+		var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
 						?? throw new Exception("Unable to determine repository name");
 
 		var localLinksJson = fileSystem.FileInfo.New(Path.Join(root.FullName, file));
diff --git a/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj b/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj
new file mode 100644
index 0000000000..1ab221484e
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net10.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <RootNamespace>Elastic.Documentation</RootNamespace>
+    <IsAotCompatible>true</IsAotCompatible>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <NoWarn>$(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Elastic.Documentation\Elastic.Documentation.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Nullean.Argh.Interfaces" />
+    <PackageReference Include="Nullean.ScopedFileSystem" />
+    <PackageReference Include="Proc" />
+    <PackageReference Include="SoftCircuits.IniFileParser" />
+    <PackageReference Include="System.IO.Abstractions" />
+    <PackageReference Include="System.IO.Abstractions.TestingHelpers" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Elastic.Documentation/Exporter.cs b/src/Elastic.Documentation.Tooling/Exporter.cs
similarity index 56%
rename from src/Elastic.Documentation/Exporter.cs
rename to src/Elastic.Documentation.Tooling/Exporter.cs
index 25adbdf2df..6c45c60551 100644
--- a/src/Elastic.Documentation/Exporter.cs
+++ b/src/Elastic.Documentation.Tooling/Exporter.cs
@@ -3,7 +3,6 @@
 // See the LICENSE file in the project root for more information
 
 using Nullean.Argh;
-using static Elastic.Documentation.Exporter;
 
 namespace Elastic.Documentation;
 
@@ -21,6 +20,6 @@ public enum Exporter
 
 public static class ExportOptions
 {
-	public static HashSet<Exporter> Default { get; } = [Html, LLMText, Configuration, DocumentationState, LinkMetadata, Redirects];
-	public static HashSet<Exporter> MetadataOnly { get; } = [Configuration, DocumentationState, LinkMetadata, Redirects];
+	public static HashSet<Exporter> Default { get; } = [Exporter.Html, Exporter.LLMText, Exporter.Configuration, Exporter.DocumentationState, Exporter.LinkMetadata, Exporter.Redirects];
+	public static HashSet<Exporter> MetadataOnly { get; } = [Exporter.Configuration, Exporter.DocumentationState, Exporter.LinkMetadata, Exporter.Redirects];
 }
diff --git a/src/Elastic.Documentation/ExternalCommands/ExternalCommandExecutor.cs b/src/Elastic.Documentation.Tooling/ExternalCommands/ExternalCommandExecutor.cs
similarity index 100%
rename from src/Elastic.Documentation/ExternalCommands/ExternalCommandExecutor.cs
rename to src/Elastic.Documentation.Tooling/ExternalCommands/ExternalCommandExecutor.cs
diff --git a/src/Elastic.Documentation/ExternalCommands/NoopConsoleWriter.cs b/src/Elastic.Documentation.Tooling/ExternalCommands/NoopConsoleWriter.cs
similarity index 100%
rename from src/Elastic.Documentation/ExternalCommands/NoopConsoleWriter.cs
rename to src/Elastic.Documentation.Tooling/ExternalCommands/NoopConsoleWriter.cs
diff --git a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs
similarity index 98%
rename from src/Elastic.Documentation.Configuration/FileSystemFactory.cs
rename to src/Elastic.Documentation.Tooling/FileSystemFactory.cs
index 9f8b55b4cc..09cb7411ee 100644
--- a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs
+++ b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs
@@ -6,6 +6,8 @@
 using System.IO.Abstractions.TestingHelpers;
 using Nullean.ScopedFileSystem;
 
+// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes
+#pragma warning disable IDE0130
 namespace Elastic.Documentation.Configuration;
 
 public static class FileSystemFactory
diff --git a/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs
new file mode 100644
index 0000000000..db58b514b3
--- /dev/null
+++ b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs
@@ -0,0 +1,149 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.IO.Abstractions;
+using System.Text.RegularExpressions;
+using Elastic.Documentation.Extensions;
+using Microsoft.Extensions.Logging;
+using Nullean.ScopedFileSystem;
+using SoftCircuits.IniFileParser;
+
+namespace Elastic.Documentation;
+
+public static partial class GitCheckoutInformationFactory
+{
+	// manual read because libgit2sharp is not yet AOT ready
+	public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem fileSystem, ILogger? logger = null)
+	{
+		if (source is null)
+			return GitCheckoutInformation.Unavailable;
+
+		// Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType
+		// (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper
+		// rather than relying on the outer type name.
+		var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType();
+		if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
+		{
+			return new GitCheckoutInformation
+			{
+				Branch = $"test-e35fcb27-5f60-4e",
+				Remote = "elastic/docs-builder",
+				Ref = "e35fcb27-5f60-4e",
+				RepositoryName = "docs-builder"
+			};
+		}
+		var fakeRef = Guid.NewGuid().ToString()[..16];
+
+		var gitDir = GitDir(source, ".git");
+		if (!gitDir.Exists)
+		{
+			var worktreeFile = Git(source, ".git");
+			if (!worktreeFile.Exists)
+				return GitCheckoutInformation.Unavailable;
+			var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty);
+			if (workTreePath is null)
+				return GitCheckoutInformation.Unavailable;
+			gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git");
+			if (gitDir is null || !gitDir.Exists)
+				return GitCheckoutInformation.Unavailable;
+		}
+
+		var gitConfig = Git(gitDir, "config");
+		if (!gitConfig.Exists)
+		{
+			logger?.LogInformation("Git checkout information not available.");
+			return GitCheckoutInformation.Unavailable;
+		}
+
+		var head = Read(gitDir, "HEAD") ?? fakeRef;
+		var gitRef = head;
+		var branch = head.Replace("refs/heads/", string.Empty);
+		if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase))
+		{
+			head = head.Replace("ref: ", string.Empty);
+			gitRef = Read(gitDir, head) ?? fakeRef;
+			branch = branch.Replace("ref: ", string.Empty);
+		}
+		else
+			branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head";
+
+		var ini = new IniFile();
+		using var stream = gitConfig.OpenRead();
+		using var streamReader = new StreamReader(stream);
+		ini.Load(streamReader);
+
+		var remote = BranchTrackingRemote(branch, ini);
+		logger?.LogInformation("Remote from branch: {GitRemote}", remote);
+		if (string.IsNullOrEmpty(remote))
+		{
+			remote = BranchTrackingRemote("main", ini);
+			logger?.LogInformation("Remote from main branch: {GitRemote}", remote);
+		}
+
+		if (string.IsNullOrEmpty(remote))
+		{
+			remote = BranchTrackingRemote("master", ini);
+			logger?.LogInformation("Remote from master branch: {GitRemote}", remote);
+		}
+
+		if (string.IsNullOrEmpty(remote))
+		{
+			remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
+			logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote);
+		}
+
+		if (string.IsNullOrEmpty(remote))
+		{
+			remote = "elastic/docs-builder-unknown";
+			logger?.LogInformation("Remote from fallback: {GitRemote}", remote);
+		}
+		remote = CutOffGitExtension().Replace(remote, string.Empty);
+
+		var githubRef = Environment.GetEnvironmentVariable("GITHUB_REF");
+		var info = new GitCheckoutInformation
+		{
+			Ref = gitRef,
+			Branch = branch,
+			Remote = remote,
+			RepositoryName = remote.Split('/').Last(),
+			GitHubRef = string.IsNullOrEmpty(githubRef) ? null : githubRef
+		};
+
+		logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote);
+		logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName);
+		return info;
+
+		IFileInfo Git(IDirectoryInfo directoryInfo, string path) =>
+			fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path));
+
+		IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) =>
+			fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path));
+
+		string? Read(IDirectoryInfo directoryInfo, string path)
+		{
+			var gitPath = Git(directoryInfo, path).FullName;
+			return !fileSystem.File.Exists(gitPath)
+				? null
+				: fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray());
+		}
+
+		string BranchTrackingRemote(string b, IniFile c)
+		{
+			var sections = c.GetSections();
+			var branchSection = $"branch \"{b}\"";
+			if (!sections.Contains(branchSection))
+				return string.Empty;
+
+			var remoteName = ini.GetSetting(branchSection, "remote")?.Trim();
+
+			var remoteSection = $"remote \"{remoteName}\"";
+
+			remote = ini.GetSetting(remoteSection, "url");
+			return remote ?? string.Empty;
+		}
+	}
+
+	[GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)]
+	private static partial Regex CutOffGitExtension();
+}
diff --git a/src/Elastic.Documentation/GlobalCliOptions.cs b/src/Elastic.Documentation.Tooling/GlobalCliOptions.cs
similarity index 84%
rename from src/Elastic.Documentation/GlobalCliOptions.cs
rename to src/Elastic.Documentation.Tooling/GlobalCliOptions.cs
index cc0af96f8b..c6c203b8b8 100644
--- a/src/Elastic.Documentation/GlobalCliOptions.cs
+++ b/src/Elastic.Documentation.Tooling/GlobalCliOptions.cs
@@ -4,6 +4,8 @@
 
 using Microsoft.Extensions.Logging;
 
+// ReSharper disable once CheckNamespace — intentionally preserving the namespace so consumers need no using changes
+#pragma warning disable IDE0130
 namespace Elastic.Documentation;
 
 /// <summary>
diff --git a/src/Elastic.Documentation.Configuration/Paths.cs b/src/Elastic.Documentation.Tooling/Paths.cs
similarity index 98%
rename from src/Elastic.Documentation.Configuration/Paths.cs
rename to src/Elastic.Documentation.Tooling/Paths.cs
index fa24bef977..a3d7011301 100644
--- a/src/Elastic.Documentation.Configuration/Paths.cs
+++ b/src/Elastic.Documentation.Tooling/Paths.cs
@@ -6,6 +6,8 @@
 using System.IO.Abstractions;
 using Elastic.Documentation.Extensions;
 
+// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes
+#pragma warning disable IDE0130
 namespace Elastic.Documentation.Configuration;
 
 public static class Paths
diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs
index e65d174ba5..10eb1c4b74 100644
--- a/src/Elastic.Documentation/AppliesTo/Applicability.cs
+++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs
@@ -6,11 +6,10 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Text;
 using Elastic.Documentation.Diagnostics;
-using YamlDotNet.Serialization;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.AppliesTo;
 
-[YamlSerializable]
 public record AppliesCollection : IReadOnlyCollection<Applicability>
 {
 	private readonly Applicability[] _items;
@@ -169,7 +168,6 @@ public override string ToString()
 	public int Count => _items.Length;
 }
 
-[YamlSerializable]
 public record Applicability : IComparable<Applicability>, IComparable
 {
 	public ProductLifecycle Lifecycle { get; init; }
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
index eb4a0f7f36..aa70d28259 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
@@ -2,6 +2,8 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation.Versions;
+
 namespace Elastic.Documentation.AppliesTo;
 
 /// <summary>
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
index 5bee5c81f2..c5a971b9cb 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
@@ -3,10 +3,12 @@
 // See the LICENSE file in the project root for more information
 
 using System.Collections;
+using System.Reflection;
 using System.Text;
 using System.Text.Json.Serialization;
 using Elastic.Documentation.Diagnostics;
-using YamlDotNet.Serialization;
+using Elastic.Documentation.Search;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.AppliesTo;
 
@@ -34,26 +36,20 @@ public interface IApplicableToElement
 	ApplicableTo? AppliesTo { get; }
 }
 
-[YamlSerializable]
 [JsonConverter(typeof(ApplicableToJsonConverter))]
 public record ApplicableTo
 {
-	[YamlMember(Alias = "stack")]
 	public AppliesCollection? Stack { get; set; }
 
-	[YamlMember(Alias = "deployment")]
 	public DeploymentApplicability? Deployment { get; set; }
 
-	[YamlMember(Alias = "serverless")]
 	public ServerlessProjectApplicability? Serverless { get; set; }
 
-	[YamlMember(Alias = "product")]
 	public AppliesCollection? Product { get; set; }
 
 	public ProductApplicability? ProductApplicability { get; set; }
 
 	[JsonIgnore]
-	[YamlIgnore]
 	public ApplicabilityDiagnosticsCollection? Diagnostics { get; set; }
 
 	public static ApplicableTo All { get; } = new()
@@ -72,6 +68,83 @@ public record ApplicableTo
 		Serverless = ServerlessProjectApplicability.All
 	};
 
+	/// <summary>
+	/// Convert this rich applicability description into the flat wire-format entries indexed in Elasticsearch
+	/// under <c>applies_to</c>. Produces the same JSON shape as <c>ApplicableToJsonConverter</c>.
+	/// </summary>
+	public IReadOnlyCollection<AppliesToEntry> ToAppliesTo()
+	{
+		var entries = new List<AppliesToEntry>();
+
+		if (Stack is not null)
+			AddEntries(entries, "stack", "stack", Stack);
+
+		if (Deployment is not null)
+		{
+			if (Deployment.Self is not null)
+				AddEntries(entries, "deployment", "self", Deployment.Self);
+			if (Deployment.Ece is not null)
+				AddEntries(entries, "deployment", "ece", Deployment.Ece);
+			if (Deployment.Eck is not null)
+				AddEntries(entries, "deployment", "eck", Deployment.Eck);
+			if (Deployment.Ess is not null)
+				AddEntries(entries, "deployment", "ess", Deployment.Ess);
+		}
+
+		if (Serverless is not null)
+		{
+			if (Serverless.Elasticsearch is not null)
+				AddEntries(entries, "serverless", "elasticsearch", Serverless.Elasticsearch);
+			if (Serverless.Observability is not null)
+				AddEntries(entries, "serverless", "observability", Serverless.Observability);
+			if (Serverless.Security is not null)
+				AddEntries(entries, "serverless", "security", Serverless.Security);
+		}
+
+		if (Product is not null)
+			AddEntries(entries, "product", "product", Product);
+
+		if (ProductApplicability is not null)
+		{
+			foreach (var prop in typeof(ProductApplicability).GetProperties())
+			{
+				var name = prop.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name;
+				if (name is null)
+					continue;
+				if (prop.GetValue(ProductApplicability) is AppliesCollection coll)
+					AddEntries(entries, "product", name, coll);
+			}
+		}
+
+		return entries;
+	}
+
+	private static void AddEntries(List<AppliesToEntry> sink, string type, string subType, AppliesCollection coll)
+	{
+		foreach (var a in coll)
+			sink.Add(new AppliesToEntry
+			{
+				Type = type,
+				SubType = subType,
+				Lifecycle = LifecycleName(a.Lifecycle),
+				Version = a.Version?.ToString()
+			});
+	}
+
+	private static string LifecycleName(ProductLifecycle lc) => lc switch
+	{
+		ProductLifecycle.TechnicalPreview => "preview",
+		ProductLifecycle.Beta => "beta",
+		ProductLifecycle.GenerallyAvailable => "ga",
+		ProductLifecycle.Deprecated => "deprecated",
+		ProductLifecycle.Removed => "removed",
+		ProductLifecycle.Unavailable => "unavailable",
+		ProductLifecycle.Development => "development",
+		ProductLifecycle.Planned => "planned",
+		ProductLifecycle.Discontinued => "discontinued",
+		_ => "ga"
+	};
+
 	/// <inheritdoc />
 	public override string ToString()
 	{
@@ -119,19 +192,14 @@ public override string ToString()
 	}
 }
 
-[YamlSerializable]
 public record DeploymentApplicability
 {
-	[YamlMember(Alias = "self")]
 	public AppliesCollection? Self { get; set; }
 
-	[YamlMember(Alias = "ece")]
 	public AppliesCollection? Ece { get; set; }
 
-	[YamlMember(Alias = "eck")]
 	public AppliesCollection? Eck { get; set; }
 
-	[YamlMember(Alias = "ess")]
 	public AppliesCollection? Ess { get; set; }
 
 	public static DeploymentApplicability All { get; } = new()
@@ -181,16 +249,12 @@ public override string ToString()
 	}
 }
 
-[YamlSerializable]
 public record ServerlessProjectApplicability
 {
-	[YamlMember(Alias = "elasticsearch")]
 	public AppliesCollection? Elasticsearch { get; set; }
 
-	[YamlMember(Alias = "observability")]
 	public AppliesCollection? Observability { get; set; }
 
-	[YamlMember(Alias = "security")]
 	public AppliesCollection? Security { get; set; }
 
 	/// <summary>
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
index a6dba46c4d..acafa95a4e 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
@@ -5,7 +5,7 @@
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using YamlDotNet.Serialization;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.AppliesTo;
 
@@ -154,9 +154,8 @@ public class ApplicableToJsonConverter : JsonConverter<ApplicableTo>
 
 			foreach (var (key, items) in productProps)
 			{
-				// Find the property by YamlMember alias
 				var property = productType.GetProperties()
-					.FirstOrDefault(p => p.GetCustomAttribute<YamlMemberAttribute>()?.Alias == key);
+					.FirstOrDefault(p => p.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name == key);
 
 				property?.SetValue(productApplicability, new AppliesCollection(items.ToArray()));
 			}
@@ -209,11 +208,11 @@ public override void Write(Utf8JsonWriter writer, ApplicableTo value, JsonSerial
 			var productType = typeof(ProductApplicability);
 			foreach (var property in productType.GetProperties())
 			{
-				var yamlAlias = property.GetCustomAttribute<YamlMemberAttribute>()?.Alias;
-				if (yamlAlias != null)
+				var jsonName = property.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name;
+				if (jsonName != null)
 				{
 					if (property.GetValue(value.ProductApplicability) is AppliesCollection propertyValue)
-						WriteApplicabilityEntries(writer, "product", yamlAlias, propertyValue);
+						WriteApplicabilityEntries(writer, "product", jsonName, propertyValue);
 				}
 			}
 		}
diff --git a/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs b/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs
index c486bcf47a..2c0ce7bb61 100644
--- a/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs
+++ b/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs
@@ -3,83 +3,82 @@
 // See the LICENSE file in the project root for more information
 
 using System.Text;
-using YamlDotNet.Serialization;
+using System.Text.Json.Serialization;
 
 namespace Elastic.Documentation.AppliesTo;
 
-[YamlSerializable]
 public record ProductApplicability
 {
-	[YamlMember(Alias = "ecctl")]
+	[JsonPropertyName("ecctl")]
 	public AppliesCollection? Ecctl { get; set; }
 
-	[YamlMember(Alias = "curator")]
+	[JsonPropertyName("curator")]
 	public AppliesCollection? Curator { get; set; }
 
-	[YamlMember(Alias = "apm-agent-android")]
+	[JsonPropertyName("apm-agent-android")]
 	public AppliesCollection? ApmAgentAndroid { get; set; }
 
-	[YamlMember(Alias = "apm-agent-dotnet")]
+	[JsonPropertyName("apm-agent-dotnet")]
 	public AppliesCollection? ApmAgentDotnet { get; set; }
 
-	[YamlMember(Alias = "apm-agent-go")]
+	[JsonPropertyName("apm-agent-go")]
 	public AppliesCollection? ApmAgentGo { get; set; }
 
-	[YamlMember(Alias = "apm-agent-ios")]
+	[JsonPropertyName("apm-agent-ios")]
 	public AppliesCollection? ApmAgentIos { get; set; }
 
-	[YamlMember(Alias = "apm-agent-java")]
+	[JsonPropertyName("apm-agent-java")]
 	public AppliesCollection? ApmAgentJava { get; set; }
 
-	[YamlMember(Alias = "apm-agent-node")]
+	[JsonPropertyName("apm-agent-node")]
 	public AppliesCollection? ApmAgentNode { get; set; }
 
-	[YamlMember(Alias = "apm-agent-php")]
+	[JsonPropertyName("apm-agent-php")]
 	public AppliesCollection? ApmAgentPhp { get; set; }
 
-	[YamlMember(Alias = "apm-agent-python")]
+	[JsonPropertyName("apm-agent-python")]
 	public AppliesCollection? ApmAgentPython { get; set; }
 
-	[YamlMember(Alias = "apm-agent-ruby")]
+	[JsonPropertyName("apm-agent-ruby")]
 	public AppliesCollection? ApmAgentRuby { get; set; }
 
-	[YamlMember(Alias = "apm-agent-rum-js")]
+	[JsonPropertyName("apm-agent-rum-js")]
 	public AppliesCollection? ApmAgentRumJs { get; set; }
 
-	[YamlMember(Alias = "edot-ios")]
+	[JsonPropertyName("edot-ios")]
 	public AppliesCollection? EdotIos { get; set; }
 
-	[YamlMember(Alias = "edot-android")]
+	[JsonPropertyName("edot-android")]
 	public AppliesCollection? EdotAndroid { get; set; }
 
-	[YamlMember(Alias = "edot-dotnet")]
+	[JsonPropertyName("edot-dotnet")]
 	public AppliesCollection? EdotDotnet { get; set; }
 
-	[YamlMember(Alias = "edot-java")]
+	[JsonPropertyName("edot-java")]
 	public AppliesCollection? EdotJava { get; set; }
 
-	[YamlMember(Alias = "edot-node")]
+	[JsonPropertyName("edot-node")]
 	public AppliesCollection? EdotNode { get; set; }
 
-	[YamlMember(Alias = "edot-browser")]
+	[JsonPropertyName("edot-browser")]
 	public AppliesCollection? EdotBrowser { get; set; }
 
-	[YamlMember(Alias = "edot-php")]
+	[JsonPropertyName("edot-php")]
 	public AppliesCollection? EdotPhp { get; set; }
 
-	[YamlMember(Alias = "edot-python")]
+	[JsonPropertyName("edot-python")]
 	public AppliesCollection? EdotPython { get; set; }
 
-	[YamlMember(Alias = "edot-cf-aws")]
+	[JsonPropertyName("edot-cf-aws")]
 	public AppliesCollection? EdotCfAws { get; set; }
 
-	[YamlMember(Alias = "edot-cf-azure")]
+	[JsonPropertyName("edot-cf-azure")]
 	public AppliesCollection? EdotCfAzure { get; set; }
 
-	[YamlMember(Alias = "edot-cf-gcp")]
+	[JsonPropertyName("edot-cf-gcp")]
 	public AppliesCollection? EdotCfGcp { get; set; }
 
-	[YamlMember(Alias = "edot-collector")]
+	[JsonPropertyName("edot-collector")]
 	public AppliesCollection? EdotCollector { get; set; }
 
 	/// <inheritdoc />
diff --git a/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs b/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs
index 5f190b8304..c678c557d8 100644
--- a/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs
+++ b/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs
@@ -2,40 +2,39 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using YamlDotNet.Serialization;
+using System.Text.Json.Serialization;
 
 namespace Elastic.Documentation.AppliesTo;
 
-[YamlSerializable]
 public enum ProductLifecycle
 {
 	// technical preview (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#beta-dev-and-preview-experimental)
-	[YamlMember(Alias = "preview")]
+	[JsonStringEnumMemberName("preview")]
 	TechnicalPreview,
 	// beta (ditto)
-	[YamlMember(Alias = "beta")]
+	[JsonStringEnumMemberName("beta")]
 	Beta,
 	// ga (replaces "added" in the current docs system since it was not entirely clear how/if that overlapped with beta/preview states)
-	[YamlMember(Alias = "ga")]
+	[JsonStringEnumMemberName("ga")]
 	GenerallyAvailable,
 	// deprecated (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#additions-and-deprecations)
-	[YamlMember(Alias = "deprecated")]
+	[JsonStringEnumMemberName("deprecated")]
 	Deprecated,
 	// removed content
-	[YamlMember(Alias = "removed")]
+	[JsonStringEnumMemberName("removed")]
 	Removed,
 	// unavailable (for content that doesn't exist in a specific context and is never coming or not coming anytime soon)
-	[YamlMember(Alias = "unavailable")]
+	[JsonStringEnumMemberName("unavailable")]
 	Unavailable,
 
 	// TODO remove these enum members in a future version when docs have been cleaned up
 	// discontinued (historically we've immediately removed content when the feature ceases to be supported, but this might not be the case with pages that contain information that spans versions)
-	[YamlMember(Alias = "discontinued")]
+	[JsonStringEnumMemberName("discontinued")]
 	Discontinued,
 	// coming (ditto)
-	[YamlMember(Alias = "planned")]
+	[JsonStringEnumMemberName("planned")]
 	Planned,
 	// dev (ditto, though it's uncertain whether it's ever used or still needed)
-	[YamlMember(Alias = "development")]
+	[JsonStringEnumMemberName("development")]
 	Development,
 }
diff --git a/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs b/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs
index ec3fc38f32..156fc94e6f 100644
--- a/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs
+++ b/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs
@@ -5,7 +5,6 @@
 using System.Collections.Concurrent;
 using System.Diagnostics;
 using System.IO.Abstractions;
-using Microsoft.Extensions.Hosting;
 
 namespace Elastic.Documentation.Diagnostics;
 
@@ -33,11 +32,13 @@ public class DiagnosticsCollector(IReadOnlyCollection<IDiagnosticsOutput> output
 
 	public virtual DiagnosticsCollector StartAsync(Cancel ctx)
 	{
-		_ = ((IHostedService)this).StartAsync(ctx);
+		_ = EnsureStarted(ctx);
 		return this;
 	}
 
-	Task IHostedService.StartAsync(Cancel cancellationToken)
+	Task IDiagnosticsCollector.StartAsync(Cancel cancellationToken) => EnsureStarted(cancellationToken);
+
+	private Task EnsureStarted(Cancel cancellationToken)
 	{
 		if (_started is not null)
 			return _started;
diff --git a/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs b/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs
index 6e804ad76e..7a8203fa93 100644
--- a/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs
+++ b/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs
@@ -4,11 +4,10 @@
 
 using System.Collections.Concurrent;
 using System.IO.Abstractions;
-using Microsoft.Extensions.Hosting;
 
 namespace Elastic.Documentation.Diagnostics;
 
-public interface IDiagnosticsCollector : IAsyncDisposable, IHostedService
+public interface IDiagnosticsCollector : IAsyncDisposable
 {
 	int Warnings { get; }
 	int Errors { get; }
@@ -21,6 +20,9 @@ public interface IDiagnosticsCollector : IAsyncDisposable, IHostedService
 	HashSet<string> OffendingFiles { get; }
 	ConcurrentDictionary<string, bool> InUseSubstitutionKeys { get; }
 
+	Task StartAsync(Cancel cancellationToken);
+	Task StopAsync(Cancel cancellationToken);
+
 	void Emit(Severity severity, string file, string message);
 	void EmitError(string file, string message, Exception? e = null);
 	void EmitError(string file, string message, string specificErrorMessage);
diff --git a/src/Elastic.Documentation/Elastic.Documentation.csproj b/src/Elastic.Documentation/Elastic.Documentation.csproj
index 3fe487e15c..cf4182443a 100644
--- a/src/Elastic.Documentation/Elastic.Documentation.csproj
+++ b/src/Elastic.Documentation/Elastic.Documentation.csproj
@@ -1,4 +1,4 @@
-﻿<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>net10.0</TargetFramework>
@@ -11,18 +11,13 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="DotNet.Glob" />
-    <PackageReference Include="Elastic.Mapping" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
+    <ProjectReference Include="..\services\search\Elastic.Documentation.Search.Contract\Elastic.Documentation.Search.Contract.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
     <PackageReference Include="Microsoft.Extensions.Logging"/>
     <PackageReference Include="NetEscapades.EnumGenerators"/>
-    <PackageReference Include="Nullean.Argh.Interfaces" />
-    <PackageReference Include="Proc" />
-    <PackageReference Include="SoftCircuits.IniFileParser" />
     <PackageReference Include="Nullean.ScopedFileSystem" />
-    <PackageReference Include="System.IO.Abstractions" />
-    <PackageReference Include="Vecc.YamlDotNet.Analyzers.StaticGenerator"/>
-    <PackageReference Include="YamlDotNet"/>
   </ItemGroup>
 
 </Project>
diff --git a/src/Elastic.Documentation/GitCheckoutInformation.cs b/src/Elastic.Documentation/GitCheckoutInformation.cs
index a0307b267e..d1714428cd 100644
--- a/src/Elastic.Documentation/GitCheckoutInformation.cs
+++ b/src/Elastic.Documentation/GitCheckoutInformation.cs
@@ -2,17 +2,11 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-using System.IO.Abstractions;
 using System.Text.Json.Serialization;
-using System.Text.RegularExpressions;
-using Elastic.Documentation.Extensions;
-using Microsoft.Extensions.Logging;
-using Nullean.ScopedFileSystem;
-using SoftCircuits.IniFileParser;
 
 namespace Elastic.Documentation;
 
-public partial record GitCheckoutInformation
+public record GitCheckoutInformation
 {
 	public static GitCheckoutInformation Unavailable { get; } = new()
 	{
@@ -68,11 +62,9 @@ public partial record GitCheckoutInformation
 		if (path is null)
 			return null;
 
-		// Strip trailing .git
 		if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
 			path = path[..^4];
 
-		// Validate: must be exactly org/repo — two non-empty segments, no extra slashes
 		var parts = path.Split('/');
 		if (parts.Length != 2 || string.IsNullOrEmpty(parts[0]) || string.IsNullOrEmpty(parts[1]))
 			return null;
@@ -85,161 +77,20 @@ public partial record GitCheckoutInformation
 	{
 		const string githubHost = "github.com";
 
-		// git@github.com:org/repo.git  →  org/repo.git
 		if (remote.StartsWith("git@github.com:", StringComparison.OrdinalIgnoreCase))
 			return remote["git@github.com:".Length..].TrimStart('/');
 
-		// ssh://git@github.com/org/repo.git  →  org/repo.git
 		if (remote.StartsWith("ssh://git@github.com/", StringComparison.OrdinalIgnoreCase))
 			return remote["ssh://git@github.com/".Length..].TrimStart('/');
 
-		// https://github.com/org/repo.git  or  http://github.com/org/repo.git  →  org/repo.git
 		if (remote.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase))
 			return remote["https://github.com/".Length..].TrimStart('/');
 		if (remote.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase))
 			return remote["http://github.com/".Length..].TrimStart('/');
 
-		// Bare org/repo (e.g. GITHUB_REPOSITORY env var) — must not contain "://" or "@" (i.e. not a URL)
 		if (!remote.Contains("://") && !remote.Contains('@') && !remote.Contains(githubHost, StringComparison.OrdinalIgnoreCase))
 			return remote.TrimStart('/');
 
 		return null;
 	}
-
-	// manual read because libgit2sharp is not yet AOT ready
-	public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem fileSystem, ILogger? logger = null)
-	{
-		if (source is null)
-			return Unavailable;
-
-		// Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType
-		// (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper
-		// rather than relying on the outer type name.
-		var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType();
-		if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase))
-		{
-			return new GitCheckoutInformation
-			{
-				Branch = $"test-e35fcb27-5f60-4e",
-				Remote = "elastic/docs-builder",
-				Ref = "e35fcb27-5f60-4e",
-				RepositoryName = "docs-builder"
-			};
-		}
-		var fakeRef = Guid.NewGuid().ToString()[..16];
-
-		var gitDir = GitDir(source, ".git");
-		if (!gitDir.Exists)
-		{
-			// try a worktree .git file
-			var worktreeFile = Git(source, ".git");
-			if (!worktreeFile.Exists)
-				return Unavailable;
-			var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty);
-			if (workTreePath is null)
-				return Unavailable;
-			//TODO read branch info from worktree do not fall through
-			gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git");
-			if (gitDir is null || !gitDir.Exists)
-				return Unavailable;
-		}
-
-		var gitConfig = Git(gitDir, "config");
-		if (!gitConfig.Exists)
-		{
-			logger?.LogInformation("Git checkout information not available.");
-			return Unavailable;
-		}
-
-		var head = Read(gitDir, "HEAD") ?? fakeRef;
-		var gitRef = head;
-		var branch = head.Replace("refs/heads/", string.Empty);
-		//not detached HEAD
-		if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase))
-		{
-			head = head.Replace("ref: ", string.Empty);
-			gitRef = Read(gitDir, head) ?? fakeRef;
-			branch = branch.Replace("ref: ", string.Empty);
-		}
-		else
-			branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head";
-
-		var ini = new IniFile();
-		using var stream = gitConfig.OpenRead();
-		using var streamReader = new StreamReader(stream);
-		ini.Load(streamReader);
-
-		var remote = BranchTrackingRemote(branch, ini);
-		logger?.LogInformation("Remote from branch: {GitRemote}", remote);
-		if (string.IsNullOrEmpty(remote))
-		{
-			remote = BranchTrackingRemote("main", ini);
-			logger?.LogInformation("Remote from main branch: {GitRemote}", remote);
-		}
-
-		if (string.IsNullOrEmpty(remote))
-		{
-			remote = BranchTrackingRemote("master", ini);
-			logger?.LogInformation("Remote from master branch: {GitRemote}", remote);
-		}
-
-		if (string.IsNullOrEmpty(remote))
-		{
-			remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
-			logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote);
-		}
-
-		if (string.IsNullOrEmpty(remote))
-		{
-			remote = "elastic/docs-builder-unknown";
-			logger?.LogInformation("Remote from fallback: {GitRemote}", remote);
-		}
-		remote = CutOffGitExtension().Replace(remote, string.Empty);
-
-		var githubRef = Environment.GetEnvironmentVariable("GITHUB_REF");
-		var info = new GitCheckoutInformation
-		{
-			Ref = gitRef,
-			Branch = branch,
-			Remote = remote,
-			RepositoryName = remote.Split('/').Last(),
-			GitHubRef = string.IsNullOrEmpty(githubRef) ? null : githubRef
-		};
-
-		logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote);
-		logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName);
-		return info;
-
-		IFileInfo Git(IDirectoryInfo directoryInfo, string path) =>
-			fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path));
-
-		IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) =>
-			fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path));
-
-		string? Read(IDirectoryInfo directoryInfo, string path)
-		{
-			var gitPath = Git(directoryInfo, path).FullName;
-			return !fileSystem.File.Exists(gitPath)
-				? null
-				: fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray());
-		}
-
-		string BranchTrackingRemote(string b, IniFile c)
-		{
-			var sections = c.GetSections();
-			var branchSection = $"branch \"{b}\"";
-			if (!sections.Contains(branchSection))
-				return string.Empty;
-
-			var remoteName = ini.GetSetting(branchSection, "remote")?.Trim();
-
-			var remoteSection = $"remote \"{remoteName}\"";
-
-			remote = ini.GetSetting(remoteSection, "url");
-			return remote ?? string.Empty;
-		}
-	}
-
-	[GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)]
-	private static partial Regex CutOffGitExtension();
 }
diff --git a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs b/src/Elastic.Documentation/IEnvironmentVariables.cs
similarity index 98%
rename from src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs
rename to src/Elastic.Documentation/IEnvironmentVariables.cs
index 1781abe626..b79944b8bc 100644
--- a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs
+++ b/src/Elastic.Documentation/IEnvironmentVariables.cs
@@ -2,7 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
-namespace Elastic.Documentation.Configuration;
+namespace Elastic.Documentation;
 
 /// <summary>
 /// Abstracts access to environment variables for testability.
diff --git a/src/Elastic.Documentation/ChangelogEntrySubtype.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs
similarity index 96%
rename from src/Elastic.Documentation/ChangelogEntrySubtype.cs
rename to src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs
index dc51d8ce6f..f59d52833b 100644
--- a/src/Elastic.Documentation/ChangelogEntrySubtype.cs
+++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs
@@ -5,7 +5,7 @@
 using System.ComponentModel.DataAnnotations;
 using NetEscapades.EnumGenerators;
 
-namespace Elastic.Documentation;
+namespace Elastic.Documentation.ReleaseNotes;
 
 /// <summary>
 /// Enum representing changelog entry subtypes (only applicable to breaking changes)
diff --git a/src/Elastic.Documentation/ChangelogEntryType.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs
similarity index 97%
rename from src/Elastic.Documentation/ChangelogEntryType.cs
rename to src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs
index 73c3bf9df6..32ce2ae362 100644
--- a/src/Elastic.Documentation/ChangelogEntryType.cs
+++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs
@@ -5,7 +5,7 @@
 using System.ComponentModel.DataAnnotations;
 using NetEscapades.EnumGenerators;
 
-namespace Elastic.Documentation;
+namespace Elastic.Documentation.ReleaseNotes;
 
 /// <summary>
 /// Enum representing changelog entry types
diff --git a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
index cde2ac16ee..40595dd689 100644
--- a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
+++ b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
@@ -7,6 +7,7 @@
 using Elastic.Documentation.Links;
 using Elastic.Documentation.Search;
 using Elastic.Documentation.State;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.Serialization;
 
@@ -19,6 +20,7 @@ namespace Elastic.Documentation.Serialization;
 [JsonSerializable(typeof(LinkRegistry))]
 [JsonSerializable(typeof(LinkRegistryEntry))]
 [JsonSerializable(typeof(DocumentationDocument))]
+[JsonSerializable(typeof(AppliesToEntry))]
 [JsonSerializable(typeof(Dictionary<string, string>))]
 [JsonSerializable(typeof(ApplicableTo))]
 [JsonSerializable(typeof(AppliesCollection))]
diff --git a/src/Elastic.Documentation.Configuration/SymlinkValidator.cs b/src/Elastic.Documentation/SymlinkValidator.cs
similarity index 98%
rename from src/Elastic.Documentation.Configuration/SymlinkValidator.cs
rename to src/Elastic.Documentation/SymlinkValidator.cs
index 0cbc6747ba..5612c577ab 100644
--- a/src/Elastic.Documentation.Configuration/SymlinkValidator.cs
+++ b/src/Elastic.Documentation/SymlinkValidator.cs
@@ -6,7 +6,7 @@
 using System.Security;
 using Elastic.Documentation.Extensions;
 
-namespace Elastic.Documentation.Configuration;
+namespace Elastic.Documentation;
 
 /// <summary>
 /// Provides validation to ensure control files (docset.yml, toc.yml, redirects.yml)
diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/Versions/SemVersion.cs
similarity index 99%
rename from src/Elastic.Documentation/SemVersion.cs
rename to src/Elastic.Documentation/Versions/SemVersion.cs
index 6784bb4848..89e853f392 100644
--- a/src/Elastic.Documentation/SemVersion.cs
+++ b/src/Elastic.Documentation/Versions/SemVersion.cs
@@ -6,7 +6,7 @@
 using System.Globalization;
 using System.Text.RegularExpressions;
 
-namespace Elastic.Documentation;
+namespace Elastic.Documentation.Versions;
 
 public class AllVersions() : SemVersion(99999, 0, 0)
 {
@@ -307,4 +307,3 @@ private static int CompareComponent(string a, string b, bool lower = false)
 	[GeneratedRegex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")]
 	private static partial Regex MyRegex();
 }
-
diff --git a/src/Elastic.Documentation/VersionOrDate.cs b/src/Elastic.Documentation/Versions/VersionOrDate.cs
similarity index 99%
rename from src/Elastic.Documentation/VersionOrDate.cs
rename to src/Elastic.Documentation/Versions/VersionOrDate.cs
index 7325e3c9d9..513c28c8d5 100644
--- a/src/Elastic.Documentation/VersionOrDate.cs
+++ b/src/Elastic.Documentation/Versions/VersionOrDate.cs
@@ -4,7 +4,7 @@
 
 using System.Globalization;
 
-namespace Elastic.Documentation;
+namespace Elastic.Documentation.Versions;
 
 /// <summary>
 /// A version representation that can be either a semantic version or a date.
diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/Versions/VersionSpec.cs
similarity index 99%
rename from src/Elastic.Documentation/VersionSpec.cs
rename to src/Elastic.Documentation/Versions/VersionSpec.cs
index 67881a2168..0fdf7f36ff 100644
--- a/src/Elastic.Documentation/VersionSpec.cs
+++ b/src/Elastic.Documentation/Versions/VersionSpec.cs
@@ -4,7 +4,7 @@
 
 using System.Diagnostics.CodeAnalysis;
 
-namespace Elastic.Documentation;
+namespace Elastic.Documentation.Versions;
 
 public sealed class AllVersionsSpec : VersionSpec
 {
diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj
index ed5f7a3a37..64abeb65e1 100644
--- a/src/Elastic.Markdown/Elastic.Markdown.csproj
+++ b/src/Elastic.Markdown/Elastic.Markdown.csproj
@@ -38,6 +38,7 @@
     <ProjectReference Include="..\Elastic.Documentation.Site\Elastic.Documentation.Site.csproj" />
     <ProjectReference Include="..\Elastic.Documentation.LinkIndex\Elastic.Documentation.LinkIndex.csproj" />
     <ProjectReference Include="..\Elastic.Documentation\Elastic.Documentation.csproj" />
+    <ProjectReference Include="..\Elastic.Documentation.Tooling\Elastic.Documentation.Tooling.csproj" />
     <ProjectReference Include="..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
     <ProjectReference Include="..\Elastic.Documentation.Navigation\Elastic.Documentation.Navigation.csproj" />
   </ItemGroup>
diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
index 1725c842a0..b44d144135 100644
--- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
+++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
@@ -127,7 +127,7 @@ public async ValueTask<bool> ExportAsync(MarkdownExportFileContext fileContext,
 			StrippedBody = strippedBody,
 			Description = fileContext.SourceFile.YamlFrontMatter?.Description,
 			Abstract = @abstract,
-			Applies = appliesTo,
+			Applies = appliesTo.ToAppliesTo(),
 			Parents = navigation.GetParentsOfMarkdownFile(file).Select(i => new ParentDocument
 			{
 				Title = i.NavigationTitle,
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs
similarity index 98%
rename from src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs
rename to src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs
index bd1fd63222..d8523168a4 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs
+++ b/src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs
@@ -4,10 +4,13 @@
 
 using System.Diagnostics.CodeAnalysis;
 using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.Versions;
 using YamlDotNet.Core;
 using YamlDotNet.Core.Events;
 using YamlDotNet.Serialization;
 
+// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes
+#pragma warning disable IDE0130
 namespace Elastic.Documentation.AppliesTo;
 
 public class ApplicableToYamlConverter(IReadOnlyCollection<string> productKeys) : IYamlTypeConverter
diff --git a/src/Elastic.Documentation/AppliesTo/AllVersions.cs b/src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs
similarity index 87%
rename from src/Elastic.Documentation/AppliesTo/AllVersions.cs
rename to src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs
index 39b9e7325a..aecb1a3d5d 100644
--- a/src/Elastic.Documentation/AppliesTo/AllVersions.cs
+++ b/src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs
@@ -2,10 +2,13 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation.Versions;
 using YamlDotNet.Core;
 using YamlDotNet.Core.Events;
 using YamlDotNet.Serialization;
 
+// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes
+#pragma warning disable IDE0130
 namespace Elastic.Documentation.AppliesTo;
 
 public class SemVersionConverter : IYamlTypeConverter
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
index a6a3dd4a6d..dcfcee4639 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
@@ -6,6 +6,7 @@
 using Elastic.Documentation;
 using Elastic.Documentation.AppliesTo;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Myst.Components;
 
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
index 82afa2ce5f..c7b858d396 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
@@ -5,6 +5,7 @@
 using Elastic.Documentation;
 using Elastic.Documentation.AppliesTo;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Myst.Components;
 
diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs
index 954c932d5a..ae1d2f67c4 100644
--- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs
@@ -9,6 +9,7 @@
 using Elastic.Documentation.Configuration.ReleaseNotes;
 using Elastic.Documentation.Extensions;
 using Elastic.Documentation.ReleaseNotes;
+using Elastic.Documentation.Versions;
 using Elastic.Markdown.Diagnostics;
 using Elastic.Markdown.Helpers;
 
diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs
index f58e3f9f30..114f402bfe 100644
--- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs
@@ -6,6 +6,7 @@
 using System.Text;
 using Elastic.Documentation;
 using Elastic.Documentation.ReleaseNotes;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Myst.Directives.Changelog;
 
diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs
index 56e2d10416..4d4d7fcc6d 100644
--- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using System.IO.Abstractions;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Extensions;
 using Elastic.Markdown.Diagnostics;
diff --git a/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs
index 6f1a8efe41..3f5a4e81d5 100644
--- a/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using System.IO.Abstractions;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Extensions;
 using Elastic.Markdown.Diagnostics;
diff --git a/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs b/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs
index cff7043561..69ca93a98d 100644
--- a/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using System.IO.Abstractions;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Extensions;
 using Elastic.Markdown.Diagnostics;
diff --git a/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs b/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs
index 87bab6f033..e01293cbd5 100644
--- a/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs
@@ -4,6 +4,7 @@
 
 using System.Globalization;
 using Elastic.Documentation;
+using Elastic.Documentation.Versions;
 using Elastic.Markdown.Diagnostics;
 using static System.StringSplitOptions;
 
diff --git a/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs b/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs
index f5e696a9c3..6383623d98 100644
--- a/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs
+++ b/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs
@@ -4,6 +4,7 @@
 
 using System.Text.Json;
 using Elastic.Documentation;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Myst.InlineParsers.Substitution;
 
diff --git a/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs b/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs
index d65d9dd06c..a2803228be 100644
--- a/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs
+++ b/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs
@@ -6,6 +6,7 @@
 using Elastic.Documentation;
 using Elastic.Documentation.AppliesTo;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Versions;
 using Elastic.Markdown.Diagnostics;
 using Markdig;
 using Markdig.Parsers;
diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs
index 4f8502e75f..7e38e24f33 100644
--- a/src/Elastic.Markdown/Myst/YamlSerialization.cs
+++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs
@@ -38,7 +38,6 @@ public static T Deserialize<T>(string yaml, ProductsConfiguration products)
 [YamlSerializable(typeof(Setting))]
 [YamlSerializable(typeof(AllowedValue))]
 [YamlSerializable(typeof(SettingMutability))]
-[YamlSerializable(typeof(ApplicableTo))]
 [YamlSerializable(typeof(ContributorEntry))]
 [YamlSerializable(typeof(ChangelogDirectiveConfigYaml))]
 [YamlSerializable(typeof(ChangelogDirectiveBundleConfigYaml))]
diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs
index 90b6e420c3..aba730b558 100644
--- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs
@@ -12,7 +12,7 @@
 
 namespace Elastic.Documentation.Api.Adapters.AskAi;
 
-public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger<AgentBuilderAskAiGateway> logger) : IAskAiGateway
+public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger<AgentBuilderAskAiGateway> logger) : IAskAiService
 {
 	/// <summary>
 	/// Model name used by Agent Builder (from AgentId)
diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs
index 26d280eaa9..4414370c3e 100644
--- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs
@@ -9,18 +9,18 @@
 namespace Elastic.Documentation.Api.Adapters.AskAi;
 
 /// <summary>
-/// Factory that creates the appropriate IAskAiGateway based on the resolved provider
+/// Factory that creates the appropriate IAskAiService based on the resolved provider
 /// </summary>
 public class AskAiGatewayFactory(
 	IServiceProvider serviceProvider,
 	AskAiProviderResolver providerResolver,
-	ILogger<AskAiGatewayFactory> logger) : IAskAiGateway
+	ILogger<AskAiGatewayFactory> logger) : IAskAiService
 {
 	public async Task<AskAiGatewayResponse> AskAi(AskAiRequest askAiRequest, Cancel ctx = default)
 	{
 		var provider = providerResolver.ResolveProvider();
 
-		IAskAiGateway gateway = provider switch
+		IAskAiService gateway = provider switch
 		{
 			"LlmGateway" => serviceProvider.GetRequiredService<LlmGatewayAskAiGateway>(),
 			"AgentBuilder" => serviceProvider.GetRequiredService<AgentBuilderAskAiGateway>(),
diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
index f7b898aa9d..d01d751b4e 100644
--- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs
@@ -16,7 +16,7 @@ namespace Elastic.Documentation.Api.Adapters.AskAi;
 /// <summary>
 /// Records Ask AI message feedback to Elasticsearch.
 /// </summary>
-public sealed class ElasticsearchAskAiMessageFeedbackGateway : IAskAiMessageFeedbackGateway, IDisposable
+public sealed class ElasticsearchAskAiMessageFeedbackGateway : IAskAiMessageFeedbackService, IDisposable
 {
 	private readonly ElasticsearchClient _client;
 	private readonly string _indexName;
diff --git a/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs
index 8807a79f24..fa8c90213e 100644
--- a/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs
@@ -10,7 +10,7 @@
 
 namespace Elastic.Documentation.Api.Adapters.AskAi;
 
-public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiGateway
+public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiService
 {
 	/// <summary>
 	/// Model name used by LLM Gateway (from PlatformContext.UseCase)
diff --git a/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs
deleted file mode 100644
index 7f08de6ab1..0000000000
--- a/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackUsecase.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using System.Diagnostics;
-using Microsoft.Extensions.Logging;
-
-namespace Elastic.Documentation.Api.AskAi;
-
-/// <summary>
-/// Use case for handling Ask AI message feedback submissions.
-/// </summary>
-public class AskAiMessageFeedbackUsecase(
-	IAskAiMessageFeedbackGateway feedbackGateway,
-	ILogger<AskAiMessageFeedbackUsecase> logger)
-{
-	private static readonly ActivitySource FeedbackActivitySource = new(TelemetryConstants.AskAiFeedbackSourceName);
-
-	public async Task SubmitFeedback(AskAiMessageFeedbackRequest request, string? euid, CancellationToken ctx)
-	{
-		using var activity = FeedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal);
-		_ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId); // correlation with chat traces
-		_ = activity?.SetTag("ask_ai.message.id", request.MessageId);
-		_ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant());
-		// Note: user.euid is automatically added to spans by EuidSpanProcessor
-
-		// MessageId and ConversationId are Guid types, so no sanitization needed
-		logger.LogInformation(
-			"Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}",
-			request.MessageId,
-			request.ConversationId,
-			request.Reaction);
-
-		var record = new AskAiMessageFeedbackRecord(
-			request.MessageId,
-			request.ConversationId,
-			request.Reaction,
-			euid
-		);
-
-		await feedbackGateway.RecordFeedbackAsync(record, ctx);
-	}
-}
diff --git a/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs
deleted file mode 100644
index 96efb4a9d1..0000000000
--- a/src/api/Elastic.Documentation.Api/AskAi/AskAiUsecase.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using System.Diagnostics;
-using System.Text.Json;
-using Microsoft.Extensions.Logging;
-
-namespace Elastic.Documentation.Api.AskAi;
-
-public class AskAiUsecase(
-	IAskAiGateway askAiGateway,
-	IStreamTransformer streamTransformer,
-	ILogger<AskAiUsecase> logger)
-{
-	private static readonly ActivitySource AskAiActivitySource = new(TelemetryConstants.AskAiSourceName);
-
-	public async Task<Stream> AskAi(AskAiRequest askAiRequest, Cancel ctx)
-	{
-		logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId);
-		var activity = AskAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client);
-		_ = activity?.SetTag("gen_ai.operation.name", "chat");
-		_ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); // agent-builder or llm-gateway
-		_ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); // docs-agent or docs_assistant
-		if (askAiRequest.ConversationId is not null)
-			_ = activity?.SetTag("gen_ai.conversation.id", askAiRequest.ConversationId.ToString());
-
-		var inputMessages = new[]
-		{
-			new InputMessage("user", [new MessagePart("text", askAiRequest.Message)])
-		};
-		var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray);
-		_ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson);
-		var sanitizedMessage = askAiRequest.Message?.Replace("\r", "").Replace("\n", "");
-		logger.LogInformation("AskAI input message: <{ask_ai.input.message}>", sanitizedMessage);
-		logger.LogInformation("Streaming AskAI response");
-
-		// Gateway handles conversation ID generation if needed
-		var response = await askAiGateway.AskAi(askAiRequest, ctx);
-
-		// Use generated ID if available, otherwise use the original request ID
-		var conversationId = response.GeneratedConversationId ?? askAiRequest.ConversationId;
-		if (conversationId is not null)
-			_ = activity?.SetTag("gen_ai.conversation.id", conversationId.ToString());
-
-		// The stream transformer takes ownership of the activity and disposes it when streaming completes.
-		// This is necessary because streaming happens asynchronously after this method returns.
-		var transformedStream = await streamTransformer.TransformAsync(
-			response.Stream,
-			response.GeneratedConversationId,
-			activity,
-			ctx);
-		return transformedStream;
-	}
-}
-
-public record AskAiRequest(string Message, Guid? ConversationId);
diff --git a/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs
index d1bb5c20da..4ab3e1f2f8 100644
--- a/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs
@@ -15,7 +15,9 @@ namespace Elastic.Documentation.Api.AskAi;
 /// </param>
 public record AskAiGatewayResponse(Stream Stream, Guid? GeneratedConversationId);
 
-public interface IAskAiGateway
+public record AskAiRequest(string Message, Guid? ConversationId);
+
+public interface IAskAiService
 {
 	Task<AskAiGatewayResponse> AskAi(AskAiRequest askAiRequest, Cancel ctx = default);
 }
diff --git a/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs
index ab815decd5..9f3ca07407 100644
--- a/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs
+++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs
@@ -5,10 +5,10 @@
 namespace Elastic.Documentation.Api.AskAi;
 
 /// <summary>
-/// Gateway interface for recording Ask AI message feedback.
+/// Service interface for recording Ask AI message feedback.
 /// Infrastructure implementations may use different storage backends (Elasticsearch, database, etc.)
 /// </summary>
-public interface IAskAiMessageFeedbackGateway
+public interface IAskAiMessageFeedbackService
 {
 	/// <summary>
 	/// Records feedback for a specific Ask AI message.
diff --git a/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs b/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs
deleted file mode 100644
index 797a7b66bb..0000000000
--- a/src/api/Elastic.Documentation.Api/Changes/ChangesUsecase.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using System.Buffers;
-using System.Text;
-using System.Text.Json;
-using Elastic.Documentation.Search;
-using Microsoft.Extensions.Logging;
-
-namespace Elastic.Documentation.Api.Changes;
-
-/// <summary>Use case for the documentation changes feed.</summary>
-public partial class ChangesUsecase(IChangesGateway changesGateway, ILogger<ChangesUsecase> logger)
-{
-	public async Task<ChangesApiResponse> GetChangesAsync(ChangesApiRequest request, Cancel ctx = default)
-	{
-		var cursor = DecodeCursor(request.Cursor);
-		var pageSize = Math.Clamp(request.PageSize, 1, ChangesDefaults.MaxPageSize);
-
-		var result = await changesGateway.GetChangesAsync(
-			new ChangesRequest
-			{
-				Since = request.Since,
-				PageSize = pageSize,
-				Cursor = cursor
-			},
-			ctx
-		);
-
-		var nextCursor = result.NextCursor is not null
-			? EncodeCursor(result.NextCursor)
-			: null;
-
-		var hasMore = nextCursor is not null;
-
-		LogChanges(logger, request.Since, result.Pages.Count, hasMore);
-
-		return new ChangesApiResponse
-		{
-			Pages = result.Pages,
-			HasMore = hasMore,
-			NextCursor = nextCursor
-		};
-	}
-
-	private static ChangesPageCursor? DecodeCursor(string? cursor)
-	{
-		if (string.IsNullOrWhiteSpace(cursor))
-			return null;
-
-		try
-		{
-			var remainder = cursor.Length % 4;
-			var paddingLength = (4 - remainder) % 4;
-			var base64 = cursor
-				.Replace('-', '+')
-				.Replace('_', '/')
-				+ new string('=', paddingLength);
-
-			var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64));
-			using var doc = JsonDocument.Parse(json);
-			var root = doc.RootElement;
-			var arrayLength = root.GetArrayLength();
-			if (root.ValueKind != JsonValueKind.Array || arrayLength < 2)
-				return null;
-
-			var epochEl = root[0];
-			var urlEl = root[1];
-			if (epochEl.ValueKind != JsonValueKind.Number || urlEl.ValueKind != JsonValueKind.String)
-				return null;
-
-			return new ChangesPageCursor(epochEl.GetInt64(), urlEl.GetString()!);
-		}
-		catch (Exception ex) when (ex is FormatException or JsonException or InvalidOperationException)
-		{
-			return null;
-		}
-	}
-
-	private static string EncodeCursor(ChangesPageCursor cursor)
-	{
-		var buffer = new ArrayBufferWriter<byte>();
-		using var writer = new Utf8JsonWriter(buffer);
-		writer.WriteStartArray();
-		writer.WriteNumberValue(cursor.ContentLastUpdatedEpochMs);
-		writer.WriteStringValue(cursor.Url);
-		writer.WriteEndArray();
-		writer.Flush();
-
-		return Convert.ToBase64String(buffer.WrittenSpan)
-			.TrimEnd('=')
-			.Replace('+', '-')
-			.Replace('/', '_');
-	}
-
-	[LoggerMessage(Level = LogLevel.Information,
-		Message = "Changes feed returned {Count} pages since {Since} (hasMore: {HasMore})")]
-	private static partial void LogChanges(ILogger logger, DateTimeOffset since, int count, bool hasMore);
-}
-
-/// <summary>API request for the changes feed endpoint.</summary>
-public record ChangesApiRequest
-{
-	public required DateTimeOffset Since { get; init; }
-	public int PageSize { get; init; } = ChangesDefaults.PageSize;
-	public string? Cursor { get; init; }
-}
-
-/// <summary>API response for the changes feed endpoint.</summary>
-public record ChangesApiResponse
-{
-	public required IReadOnlyList<ChangedPageDto> Pages { get; init; }
-	public required bool HasMore { get; init; }
-	public string? NextCursor { get; init; }
-}
diff --git a/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj
index 1c60dcab23..26d0be4ac5 100644
--- a/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj
+++ b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj
@@ -20,7 +20,7 @@
     <ProjectReference Include="..\..\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
     <ProjectReference Include="..\..\Elastic.Documentation\Elastic.Documentation.csproj" />
     <ProjectReference Include="..\..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
-    <ProjectReference Include="..\..\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
+    <ProjectReference Include="..\..\services\search\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="AWSSDK.DynamoDBv2" />
diff --git a/src/api/Elastic.Documentation.Api/MappingsExtensions.cs b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs
index 493b9a1d67..8491c755bb 100644
--- a/src/api/Elastic.Documentation.Api/MappingsExtensions.cs
+++ b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs
@@ -2,16 +2,16 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using System.Diagnostics;
+using System.Text.Json;
 using Elastic.Documentation.Api.AskAi;
-using Elastic.Documentation.Api.Changes;
-using Elastic.Documentation.Api.Search;
 using Elastic.Documentation.Api.Telemetry;
-using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Search;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Logging;
 
 namespace Elastic.Documentation.Api;
 
@@ -33,23 +33,71 @@ public static void MapElasticDocsApiEndpoints(this IEndpointRouteBuilder group,
 	private static void MapAskAiEndpoint(IEndpointRouteBuilder group)
 	{
 		var askAiGroup = group.MapGroup("/ask-ai");
-		_ = askAiGroup.MapPost("/stream", async (HttpContext context, AskAiRequest askAiRequest, AskAiUsecase askAiUsecase, Cancel ctx) =>
+		_ = askAiGroup.MapPost("/stream", async (HttpContext context, AskAiRequest askAiRequest, IAskAiService askAiService, IStreamTransformer streamTransformer, ILogger<Program> logger, Cancel ctx) =>
 		{
 			context.Response.ContentType = "text/event-stream";
 			context.Response.Headers.CacheControl = "no-cache";
 			context.Response.Headers.Connection = "keep-alive";
 
-			var stream = await askAiUsecase.AskAi(askAiRequest, ctx);
-			await stream.CopyToAsync(context.Response.Body, ctx);
+			var askAiActivitySource = new ActivitySource(TelemetryConstants.AskAiSourceName);
+			logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId);
+			var activity = askAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client);
+			_ = activity?.SetTag("gen_ai.operation.name", "chat");
+			_ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider);
+			_ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId);
+			if (askAiRequest.ConversationId is not null)
+				_ = activity?.SetTag("gen_ai.conversation.id", askAiRequest.ConversationId.ToString());
+
+			var inputMessages = new[]
+			{
+				new InputMessage("user", [new MessagePart("text", askAiRequest.Message)])
+			};
+			var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray);
+			_ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson);
+			var sanitizedMessage = askAiRequest.Message?.Replace("\r", "").Replace("\n", "");
+			logger.LogInformation("AskAI input message: <{ask_ai.input.message}>", sanitizedMessage);
+			logger.LogInformation("Streaming AskAI response");
+
+			var response = await askAiService.AskAi(askAiRequest, ctx);
+
+			var conversationId = response.GeneratedConversationId ?? askAiRequest.ConversationId;
+			if (conversationId is not null)
+				_ = activity?.SetTag("gen_ai.conversation.id", conversationId.ToString());
+
+			var transformedStream = await streamTransformer.TransformAsync(
+				response.Stream,
+				response.GeneratedConversationId,
+				activity,
+				ctx);
+			await transformedStream.CopyToAsync(context.Response.Body, ctx);
 		});
 
 		// UUID validation is automatic via Guid type deserialization (returns 400 if invalid)
-		_ = askAiGroup.MapPost("/message-feedback", async (HttpContext context, AskAiMessageFeedbackRequest request, AskAiMessageFeedbackUsecase feedbackUsecase, Cancel ctx) =>
+		_ = askAiGroup.MapPost("/message-feedback", async (HttpContext context, AskAiMessageFeedbackRequest request, IAskAiMessageFeedbackService feedbackService, ILogger<Program> logger, Cancel ctx) =>
 		{
 			// Extract euid cookie for user tracking
 			_ = context.Request.Cookies.TryGetValue("euid", out var euid);
 
-			await feedbackUsecase.SubmitFeedback(request, euid, ctx);
+			var feedbackActivitySource = new ActivitySource(TelemetryConstants.AskAiFeedbackSourceName);
+			using var activity = feedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal);
+			_ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId);
+			_ = activity?.SetTag("ask_ai.message.id", request.MessageId);
+			_ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant());
+
+			logger.LogInformation(
+				"Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}",
+				request.MessageId,
+				request.ConversationId,
+				request.Reaction);
+
+			var record = new AskAiMessageFeedbackRecord(
+				request.MessageId,
+				request.ConversationId,
+				request.Reaction,
+				euid
+			);
+
+			await feedbackService.RecordFeedbackAsync(record, ctx);
 			return Results.NoContent();
 		}).DisableAntiforgery();
 	}
@@ -62,17 +110,17 @@ private static void MapNavigationSearch(IEndpointRouteBuilder group)
 				[FromQuery(Name = "q")] string query,
 				[FromQuery(Name = "page")] int? pageNumber,
 				[FromQuery(Name = "type")] string? typeFilter,
-				NavigationSearchUsecase navigationSearchUsecase,
+				INavigationSearchService navigationSearchService,
 				Cancel ctx
 			) =>
 			{
-				var request = new NavigationSearchApiRequest
+				var request = new NavigationSearchRequest
 				{
 					Query = query,
 					PageNumber = pageNumber ?? 1,
 					TypeFilter = typeFilter
 				};
-				var response = await navigationSearchUsecase.NavigationSearchAsync(request, ctx);
+				var response = await navigationSearchService.NavigationSearchAsync(request, ctx);
 				return Results.Ok(response);
 			});
 	}
@@ -91,11 +139,11 @@ private static void MapFullSearch(IEndpointRouteBuilder group)
 				[FromQuery(Name = "product")] string[]? productFilter,
 				[FromQuery(Name = "version")] string? versionFilter,
 				[FromQuery(Name = "sort")] string? sortBy,
-				FullSearchUsecase searchUsecase,
+				IFullSearchService searchService,
 				Cancel ctx
 			) =>
 			{
-				var request = new FullSearchApiRequest
+				var request = new FullSearchRequest
 				{
 					Query = query,
 					PageNumber = pageNumber ?? 1,
@@ -107,7 +155,7 @@ Cancel ctx
 					VersionFilter = versionFilter,
 					SortBy = sortBy ?? "relevance"
 				};
-				var response = await searchUsecase.SearchAsync(request, ctx);
+				var response = await searchService.SearchAsync(request, ctx);
 				return Results.Ok(response);
 			});
 	}
@@ -118,17 +166,17 @@ private static void MapChanges(IEndpointRouteBuilder group) =>
 				[FromQuery(Name = "since")] DateTimeOffset since,
 				[FromQuery(Name = "cursor")] string? cursor,
 				[FromQuery(Name = "size")] int? pageSize,
-				ChangesUsecase changesUsecase,
+				IChangesService changesService,
 				Cancel ctx
 			) =>
 			{
-				var request = new ChangesApiRequest
+				var request = new ChangesRequest
 				{
 					Since = since,
 					PageSize = pageSize ?? ChangesDefaults.PageSize,
 					Cursor = cursor
 				};
-				var response = await changesUsecase.GetChangesAsync(request, ctx);
+				var response = await changesService.GetChangesAsync(request, ctx);
 				return Results.Ok(response);
 			});
 
@@ -147,10 +195,10 @@ private static void MapOtlpSignalEndpoint(
 		string path,
 		OtlpSignalType signalType) =>
 		group.MapPost(path,
-			async (HttpContext context, OtlpProxyUsecase proxyUsecase, Cancel ctx) =>
+			async (HttpContext context, IOtlpService otlpService, Cancel ctx) =>
 			{
 				var contentType = context.Request.ContentType ?? "application/json";
-				var result = await proxyUsecase.ProxyOtlp(
+				var result = await otlpService.ForwardOtlp(
 					signalType,
 					context.Request.Body,
 					contentType,
diff --git a/src/api/Elastic.Documentation.Api/Program.cs b/src/api/Elastic.Documentation.Api/Program.cs
index 98dec28313..bbee8a9201 100644
--- a/src/api/Elastic.Documentation.Api/Program.cs
+++ b/src/api/Elastic.Documentation.Api/Program.cs
@@ -31,7 +31,7 @@
 	var environment = Environment.GetEnvironmentVariable("ENVIRONMENT");
 	Console.WriteLine($"Docs Environment: {environment}");
 
-	builder.Services.AddElasticDocsApiUsecases(environment);
+	builder.Services.AddElasticDocsApiServices(environment);
 	var app = builder.Build();
 
 	var logger = app.Services.GetRequiredService<ILogger<Program>>();
diff --git a/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs b/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs
deleted file mode 100644
index 3eb6e84586..0000000000
--- a/src/api/Elastic.Documentation.Api/Search/FullSearchUsecase.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using Elastic.Documentation.Search;
-using Microsoft.Extensions.Logging;
-
-namespace Elastic.Documentation.Api.Search;
-
-/// <summary>
-/// Use case for full-page search operations.
-/// </summary>
-public partial class FullSearchUsecase(IFullSearchGateway fullSearchGateway, ILogger<FullSearchUsecase> logger)
-{
-	public async Task<FullSearchApiResponse> SearchAsync(FullSearchApiRequest request, Cancel ctx = default)
-	{
-		var result = await fullSearchGateway.SearchAsync(
-			new FullSearchRequest
-			{
-				Query = request.Query,
-				PageNumber = request.PageNumber,
-				PageSize = request.PageSize,
-				TypeFilter = request.TypeFilter,
-				SectionFilter = request.SectionFilter,
-				DeploymentFilter = request.DeploymentFilter,
-				ProductFilter = request.ProductFilter,
-				VersionFilter = request.VersionFilter,
-				SortBy = request.SortBy
-			},
-			ctx
-		);
-
-		var response = new FullSearchApiResponse
-		{
-			Results = result.Results,
-			TotalResults = result.TotalHits,
-			PageNumber = request.PageNumber,
-			PageSize = request.PageSize,
-			Aggregations = result.Aggregations,
-			IsSemanticQuery = result.IsSemanticQuery
-		};
-
-		LogFullSearchResults(
-			logger,
-			response.PageSize,
-			response.PageNumber,
-			request.Query,
-			result.IsSemanticQuery,
-			new FullSearchResultsLogProperties(result.Results.Select(i => i.Url).ToArray())
-		);
-
-		return response;
-	}
-
-	[LoggerMessage(Level = LogLevel.Information, Message = "Full search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}' (semantic: {IsSemantic})")]
-	private static partial void LogFullSearchResults(
-		ILogger logger,
-		int pageSize,
-		int pageNumber,
-		string searchQuery,
-		bool isSemantic,
-		[LogProperties] FullSearchResultsLogProperties result);
-
-	private sealed record FullSearchResultsLogProperties(string[] Urls);
-}
-
-/// <summary>
-/// API request model for full-page search.
-/// </summary>
-public record FullSearchApiRequest
-{
-	public required string Query { get; init; }
-	public int PageNumber { get; init; } = 1;
-	public int PageSize { get; init; } = 20;
-	public string[]? TypeFilter { get; init; }
-	public string[]? SectionFilter { get; init; }
-	public string[]? DeploymentFilter { get; init; }
-	public string[]? ProductFilter { get; init; }
-	public string? VersionFilter { get; init; }
-	public string SortBy { get; init; } = "relevance";
-}
-
-/// <summary>
-/// API response model for full-page search.
-/// </summary>
-public record FullSearchApiResponse
-{
-	public required IEnumerable<FullSearchResultItem> Results { get; init; }
-	public required int TotalResults { get; init; }
-	public required int PageNumber { get; init; }
-	public required int PageSize { get; init; }
-	public FullSearchAggregations Aggregations { get; init; } = new();
-	public bool IsSemanticQuery { get; init; }
-	public int PageCount => TotalResults > 0
-		? (int)Math.Ceiling((double)TotalResults / PageSize)
-		: 0;
-}
diff --git a/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs b/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs
deleted file mode 100644
index 751a8182a3..0000000000
--- a/src/api/Elastic.Documentation.Api/Search/NavigationSearchUsecase.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using Elastic.Documentation.Search;
-using Microsoft.Extensions.Logging;
-
-namespace Elastic.Documentation.Api.Search;
-
-// note still called SearchUseCase because we'll re-add Search() and ensure both share the same client.
-public partial class NavigationSearchUsecase(INavigationSearchGateway navigationSearchGateway, ILogger<NavigationSearchUsecase> logger)
-{
-	public async Task<NavigationSearchApiResponse> NavigationSearchAsync(NavigationSearchApiRequest request, Cancel ctx = default)
-	{
-		var result = await navigationSearchGateway.NavigationSearchAsync(
-			request.Query,
-			request.PageNumber,
-			request.PageSize,
-			request.TypeFilter,
-			ctx
-		);
-
-		var response = new NavigationSearchApiResponse
-		{
-			Results = result.Results,
-			TotalResults = result.TotalHits,
-			PageNumber = request.PageNumber,
-			PageSize = request.PageSize,
-			Aggregations = new NavigationSearchAggregations { Type = result.Aggregations }
-		};
-
-		LogNavigationSearchResults(
-			logger,
-			response.PageSize,
-			response.PageNumber,
-			request.Query,
-			new AutoCompleteResultsLogProperties(result.Results.Select(i => i.Url).ToArray())
-		);
-
-		return response;
-	}
-
-	[LoggerMessage(Level = LogLevel.Information, Message = "Navigation search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}'")]
-	private static partial void LogNavigationSearchResults(ILogger logger, int pageSize, int pageNumber, string searchQuery, [LogProperties] AutoCompleteResultsLogProperties result);
-
-	private sealed record AutoCompleteResultsLogProperties(string[] Urls);
-}
-
-public record NavigationSearchApiRequest
-{
-	public required string Query { get; init; }
-	public int PageNumber { get; init; } = 1;
-	public int PageSize { get; init; } = 20;
-	public string? TypeFilter { get; init; }
-}
-
-public record NavigationSearchApiResponse
-{
-	public required IEnumerable<NavigationSearchResultItem> Results { get; init; }
-	public required int TotalResults { get; init; }
-	public required int PageNumber { get; init; }
-	public required int PageSize { get; init; }
-	public NavigationSearchAggregations Aggregations { get; init; } = new();
-	public int PageCount => TotalResults > 0
-				? (int)Math.Ceiling((double)TotalResults / PageSize)
-				: 0;
-}
-
-public record NavigationSearchAggregations
-{
-	public IReadOnlyDictionary<string, long> Type { get; init; } = new Dictionary<string, long>();
-}
-
diff --git a/src/api/Elastic.Documentation.Api/SerializationContext.cs b/src/api/Elastic.Documentation.Api/SerializationContext.cs
index b293ee9975..d5474683b9 100644
--- a/src/api/Elastic.Documentation.Api/SerializationContext.cs
+++ b/src/api/Elastic.Documentation.Api/SerializationContext.cs
@@ -4,8 +4,6 @@
 
 using System.Text.Json.Serialization;
 using Elastic.Documentation.Api.AskAi;
-using Elastic.Documentation.Api.Changes;
-using Elastic.Documentation.Api.Search;
 using Elastic.Documentation.Search;
 
 namespace Elastic.Documentation.Api;
@@ -22,19 +20,19 @@ public record OutputMessage(string Role, MessagePart[] Parts, string FinishReaso
 [JsonSerializable(typeof(AskAiRequest))]
 [JsonSerializable(typeof(AskAiMessageFeedbackRequest))]
 [JsonSerializable(typeof(Reaction))]
-[JsonSerializable(typeof(NavigationSearchApiRequest))]
-[JsonSerializable(typeof(NavigationSearchApiResponse))]
+[JsonSerializable(typeof(NavigationSearchRequest))]
+[JsonSerializable(typeof(NavigationSearchResponse))]
 [JsonSerializable(typeof(NavigationSearchAggregations))]
 [JsonSerializable(typeof(InputMessage))]
 [JsonSerializable(typeof(OutputMessage[]))]
 [JsonSerializable(typeof(MessagePart))]
 [JsonSerializable(typeof(InputMessage[]))]
 
-[JsonSerializable(typeof(FullSearchApiRequest))]
-[JsonSerializable(typeof(FullSearchApiResponse))]
+[JsonSerializable(typeof(FullSearchRequest))]
+[JsonSerializable(typeof(FullSearchResponse))]
 [JsonSerializable(typeof(FullSearchAggregations))]
 
-[JsonSerializable(typeof(ChangesApiResponse))]
+[JsonSerializable(typeof(ChangesResponse))]
 [JsonSerializable(typeof(ChangedPageDto))]
 [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
 public partial class ApiJsonContext : JsonSerializerContext;
diff --git a/src/api/Elastic.Documentation.Api/ServicesExtension.cs b/src/api/Elastic.Documentation.Api/ServicesExtension.cs
index 520df0e6b9..4e846b57d7 100644
--- a/src/api/Elastic.Documentation.Api/ServicesExtension.cs
+++ b/src/api/Elastic.Documentation.Api/ServicesExtension.cs
@@ -6,12 +6,9 @@
 using Amazon.DynamoDBv2;
 using Elastic.Documentation.Api;
 using Elastic.Documentation.Api.Adapters.AskAi;
-using Elastic.Documentation.Api.Adapters.Telemetry;
 using Elastic.Documentation.Api.AskAi;
 using Elastic.Documentation.Api.Caching;
-using Elastic.Documentation.Api.Changes;
 using Elastic.Documentation.Api.Gcp;
-using Elastic.Documentation.Api.Search;
 using Elastic.Documentation.Api.Telemetry;
 using Elastic.Documentation.Search;
 using Microsoft.Extensions.Configuration;
@@ -44,20 +41,20 @@ public static class ServicesExtension
 		return loggerFactory?.CreateLogger(typeof(ServicesExtension));
 	}
 
-	public static void AddElasticDocsApiUsecases(this IServiceCollection services, string? appEnvironment)
+	public static void AddElasticDocsApiServices(this IServiceCollection services, string? appEnvironment)
 	{
 		if (AppEnvExtensions.TryParse(appEnvironment, out var parsedEnvironment, true))
-			AddElasticDocsApiUsecases(services, parsedEnvironment);
+			AddElasticDocsApiServices(services, parsedEnvironment);
 		else
 		{
 			var logger = GetLogger(services);
 			logger?.LogWarning("Unable to parse environment {AppEnvironment} into AppEnvironment. Using default AppEnvironment.Dev", appEnvironment);
-			AddElasticDocsApiUsecases(services, AppEnv.Dev);
+			AddElasticDocsApiServices(services, AppEnv.Dev);
 		}
 	}
 
 
-	private static void AddElasticDocsApiUsecases(this IServiceCollection services, AppEnv appEnv)
+	private static void AddElasticDocsApiServices(this IServiceCollection services, AppEnv appEnv)
 	{
 		_ = services.ConfigureHttpJsonOptions(options =>
 		{
@@ -74,9 +71,9 @@ private static void AddElasticDocsApiUsecases(this IServiceCollection services,
 		// Register AppEnvironment as a singleton for dependency injection
 		_ = services.AddSingleton(new AppEnvironment { Current = appEnv });
 		AddDistributedCache(services, appEnv);
-		AddAskAiUsecase(services, appEnv);
-		AddSearchUsecase(services, appEnv);
-		AddOtlpProxyUsecase(services, appEnv);
+		AddAskAiServices(services, appEnv);
+		AddSearchServices(services, appEnv);
+		AddOtlpProxyService(services, appEnv);
 	}
 
 	// Note: IParameterProvider is no longer needed - all options now read from IConfiguration (env vars)
@@ -134,10 +131,10 @@ private static void AddDistributedCache(IServiceCollection services, AppEnv appE
 		}
 	}
 
-	private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv)
+	private static void AddAskAiServices(IServiceCollection services, AppEnv appEnv)
 	{
 		var logger = GetLogger(services);
-		logger?.LogInformation("Configuring AskAi use case for environment {AppEnvironment}", appEnv);
+		logger?.LogInformation("Configuring AskAi services for environment {AppEnvironment}", appEnv);
 
 		try
 		{
@@ -153,9 +150,6 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv)
 			_ = services.AddSingleton<KibanaOptions>();
 			logger?.LogInformation("KibanaOptions registered successfully");
 
-			_ = services.AddScoped<AskAiUsecase>();
-			logger?.LogInformation("AskAiUsecase registered successfully");
-
 			// Register HttpContextAccessor for provider resolution
 			_ = services.AddHttpContextAccessor();
 			logger?.LogInformation("HttpContextAccessor registered successfully");
@@ -164,51 +158,46 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv)
 			_ = services.AddScoped<AskAiProviderResolver>();
 			logger?.LogInformation("AskAiProviderResolver registered successfully");
 
-			// Register both gateways as concrete types
+			// Register both service implementations as concrete types
 			_ = services.AddScoped<LlmGatewayAskAiGateway>();
 			_ = services.AddScoped<AgentBuilderAskAiGateway>();
-			logger?.LogInformation("Both AI gateways registered as concrete types");
+			logger?.LogInformation("Both AI service implementations registered as concrete types");
 
 			// Register both transformers as concrete types
 			_ = services.AddScoped<LlmGatewayStreamTransformer>();
 			_ = services.AddScoped<AgentBuilderStreamTransformer>();
 			logger?.LogInformation("Both stream transformers registered as concrete types");
 
-			// Register factories as interface implementations
-			_ = services.AddScoped<IAskAiGateway, AskAiGatewayFactory>();
+			// Register factory as interface implementation
+			_ = services.AddScoped<IAskAiService, AskAiGatewayFactory>();
 			_ = services.AddScoped<IStreamTransformer, StreamTransformerFactory>();
-			logger?.LogInformation("Gateway and transformer factories registered successfully - provider switchable via X-AI-Provider header");
+			logger?.LogInformation("Service and transformer factories registered successfully - provider switchable via X-AI-Provider header");
 
-			// Register message feedback components (gateway is singleton for connection reuse)
-			_ = services.AddSingleton<IAskAiMessageFeedbackGateway, ElasticsearchAskAiMessageFeedbackGateway>();
-			_ = services.AddScoped<AskAiMessageFeedbackUsecase>();
-			logger?.LogInformation("AskAiMessageFeedbackUsecase and Elasticsearch gateway registered successfully");
+			// Register message feedback service (singleton for connection reuse)
+			_ = services.AddSingleton<IAskAiMessageFeedbackService, ElasticsearchAskAiMessageFeedbackGateway>();
+			logger?.LogInformation("AskAiMessageFeedbackService (Elasticsearch) registered successfully");
 		}
 		catch (Exception ex)
 		{
-			logger?.LogError(ex, "Failed to configure AskAi use case for environment {AppEnvironment}", appEnv);
+			logger?.LogError(ex, "Failed to configure AskAi services for environment {AppEnvironment}", appEnv);
 			throw;
 		}
 	}
-	private static void AddSearchUsecase(IServiceCollection services, AppEnv appEnv)
+
+	private static void AddSearchServices(IServiceCollection services, AppEnv appEnv)
 	{
 		var logger = GetLogger(services);
-		logger?.LogInformation("Configuring Search use case for environment {AppEnvironment}", appEnv);
+		logger?.LogInformation("Configuring Search services for environment {AppEnvironment}", appEnv);
 
-		// Use the shared search service for DI registration (registers gateways)
+		// Use the shared search service for DI registration
 		_ = services.AddSearchServices();
-
-		// Register usecases (depend on the gateways from search service)
-		_ = services.AddScoped<NavigationSearchUsecase>();
-		_ = services.AddScoped<FullSearchUsecase>();
-		logger?.LogInformation("Full search use case registered with hybrid RRF support");
-		_ = services.AddScoped<ChangesUsecase>();
+		logger?.LogInformation("Full search service registered with hybrid RRF support");
 	}
 
-	private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appEnv)
+	private static void AddOtlpProxyService(IServiceCollection services, AppEnv appEnv)
 	{
 		var logger = GetLogger(services);
-		logger?.LogInformation("Configuring OTLP proxy use case for environment {AppEnvironment}", appEnv);
+		logger?.LogInformation("Configuring OTLP proxy service for environment {AppEnvironment}", appEnv);
 
 		_ = services.AddSingleton(sp =>
 		{
@@ -217,14 +206,13 @@ private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appE
 		});
 
 		// Register named HttpClient for OTLP proxy
-		_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
+		_ = services.AddHttpClient(AdotOtlpService.HttpClientName)
 			.ConfigureHttpClient(client =>
 			{
 				client.Timeout = TimeSpan.FromSeconds(30);
 			});
 
-		_ = services.AddScoped<IOtlpGateway, AdotOtlpGateway>();
-		_ = services.AddScoped<OtlpProxyUsecase>();
+		_ = services.AddScoped<IOtlpService, AdotOtlpService>();
 		logger?.LogInformation("OTLP proxy configured to forward to ADOT Lambda Layer collector");
 	}
 }
diff --git a/src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs
similarity index 87%
rename from src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs
index 7f9414fc0a..0eede2c9a3 100644
--- a/src/api/Elastic.Documentation.Api/Adapters/Telemetry/AdotOtlpGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs
@@ -2,21 +2,22 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using System.Diagnostics;
 using System.Net.Sockets;
-using Elastic.Documentation.Api.Telemetry;
 using Microsoft.Extensions.Logging;
 
-namespace Elastic.Documentation.Api.Adapters.Telemetry;
+namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
-/// Gateway that forwards OTLP telemetry to the ADOT Lambda Layer collector.
+/// Service that forwards OTLP telemetry to the ADOT Lambda Layer collector.
 /// </summary>
-public class AdotOtlpGateway(
+public class AdotOtlpService(
 	IHttpClientFactory httpClientFactory,
 	OtlpProxyOptions options,
-	ILogger<AdotOtlpGateway> logger) : IOtlpGateway
+	ILogger<AdotOtlpService> logger) : IOtlpService
 {
 	public const string HttpClientName = "OtlpProxy";
+	private static readonly ActivitySource ActivitySource = new(TelemetryConstants.OtlpProxySourceName);
 	private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
 
 	/// <inheritdoc />
@@ -26,6 +27,8 @@ public async Task<OtlpForwardResult> ForwardOtlp(
 		string contentType,
 		Cancel ctx = default)
 	{
+		using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client);
+
 		try
 		{
 			var targetUrl = $"{options.Endpoint.TrimEnd('/')}/v1/{signalType.ToStringFast(true)}";
diff --git a/src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs
similarity index 90%
rename from src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs
rename to src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs
index 5c00f37ca2..77fcb9c19e 100644
--- a/src/api/Elastic.Documentation.Api/Telemetry/IOtlpGateway.cs
+++ b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs
@@ -5,9 +5,9 @@
 namespace Elastic.Documentation.Api.Telemetry;
 
 /// <summary>
-/// Gateway for forwarding OTLP telemetry to a collector.
+/// Service for forwarding OTLP telemetry to a collector.
 /// </summary>
-public interface IOtlpGateway
+public interface IOtlpService
 {
 	/// <summary>
 	/// Forwards OTLP telemetry data to the collector.
diff --git a/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs
deleted file mode 100644
index 5ac9963547..0000000000
--- a/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyUsecase.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using System.Diagnostics;
-
-namespace Elastic.Documentation.Api.Telemetry;
-
-/// <summary>
-/// Proxies OTLP telemetry from the frontend to the local ADOT Lambda Layer collector.
-/// The ADOT layer handles authentication and forwarding to the backend.
-/// </summary>
-public class OtlpProxyUsecase(IOtlpGateway gateway)
-{
-	private static readonly ActivitySource ActivitySource = new(TelemetryConstants.OtlpProxySourceName);
-
-	/// <summary>
-	/// Proxies OTLP data from the frontend to the local ADOT collector.
-	/// </summary>
-	/// <param name="signalType">The OTLP signal type (traces, logs, or metrics)</param>
-	/// <param name="requestBody">The raw OTLP payload (JSON or protobuf)</param>
-	/// <param name="contentType">Content-Type header from the original request</param>
-	/// <param name="ctx">Cancellation token</param>
-	/// <returns>Result containing HTTP status code and response content</returns>
-	public async Task<OtlpForwardResult> ProxyOtlp(
-		OtlpSignalType signalType,
-		Stream requestBody,
-		string contentType,
-		Cancel ctx = default)
-	{
-		using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client);
-
-		// Forward to gateway
-		return await gateway.ForwardOtlp(signalType, requestBody, contentType, ctx);
-	}
-}
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj
index 9fc53017f1..f5a5b5bd5d 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj
@@ -21,7 +21,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
-    <ProjectReference Include="..\..\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
+    <ProjectReference Include="..\..\services\search\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
     <ProjectReference Include="..\..\services\Elastic.Documentation.Assembler\Elastic.Documentation.Assembler.csproj" />
   </ItemGroup>
 
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs
index c994a65ba5..2549f5b6ca 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs
@@ -4,6 +4,7 @@
 
 using System.IdentityModel.Tokens.Jwt;
 using System.Security.Cryptography;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Microsoft.IdentityModel.Tokens;
 
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs
index 3a0ec7e6ef..74130afdcd 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs
@@ -4,6 +4,7 @@
 
 using System.Security.Cryptography;
 using System.Text.Json.Serialization;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
index fd61e92110..49a6ede81b 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs
@@ -18,7 +18,7 @@ namespace Elastic.Documentation.Mcp.Remote.Tools;
 /// MCP tools for checking documentation coherence and finding inconsistencies.
 /// </summary>
 [McpServerToolType]
-public class CoherenceTools(IFullSearchGateway fullSearchGateway, ILogger<CoherenceTools> logger)
+public class CoherenceTools(IFullSearchService fullSearchGateway, ILogger<CoherenceTools> logger)
 {
 	/// <summary>
 	/// Checks documentation coherence for a given topic.
@@ -73,13 +73,13 @@ public async Task<string> CheckCoherence(
 			var response = new CoherenceCheckResponse
 			{
 				Topic = topic,
-				TotalDocuments = result.TotalHits,
+				TotalDocuments = result.TotalResults,
 				AnalyzedDocuments = result.Results.Count,
 				SectionCoverage = navigationSections,
 				ProductCoverage = products,
 				DocsWithAiSummary = docsWithSummaries,
 				DocsWithRagSummary = docsWithRagSummaries,
-				CoverageScore = CalculateCoverageScore(result.TotalHits, navigationSections.Count, products.Count),
+				CoverageScore = CalculateCoverageScore(result.TotalResults, navigationSections.Count, products.Count),
 				TopDocuments = result.Results.Take(5).Select(r => new CoherenceDocDto
 				{
 					Url = r.Url,
@@ -203,7 +203,7 @@ public async Task<string> FindInconsistencies(
 			{
 				Topic = topic,
 				FocusArea = focusArea,
-				TotalDocuments = result.TotalHits,
+				TotalDocuments = result.TotalResults,
 				PotentialInconsistencies = potentialOverlaps.Take(10).ToList(),
 				ProductBreakdown = byProduct.ToDictionary(g => g.Key, g => g.Value.Count)
 			};
diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
index 94778a98a2..021c245a20 100644
--- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
+++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs
@@ -18,7 +18,7 @@ namespace Elastic.Documentation.Mcp.Remote.Tools;
 /// MCP tools for semantic search operations on Elastic documentation.
 /// </summary>
 [McpServerToolType]
-public class SearchTools(IFullSearchGateway fullSearchGateway, ILogger<SearchTools> logger)
+public class SearchTools(IFullSearchService fullSearchGateway, ILogger<SearchTools> logger)
 {
 	/// <summary>
 	/// Performs semantic search across all Elastic documentation.
@@ -70,7 +70,7 @@ public async Task<string> SemanticSearch(
 			var response = new SemanticSearchResponse
 			{
 				Query = query,
-				TotalHits = result.TotalHits,
+				TotalHits = result.TotalResults,
 				IsSemanticQuery = result.IsSemanticQuery,
 				Results = result.Results.Select(r => new SearchResultDto
 				{
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
index 4120b13a25..cf171799a8 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs
@@ -6,7 +6,6 @@
 using System.IO.Abstractions;
 using System.Text;
 using System.Text.RegularExpressions;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.Utilities;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Assembler;
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
index bd1adbe4e8..cf0b061e21 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs
@@ -6,7 +6,6 @@
 using System.Security.Cryptography;
 using System.Text;
 using System.Text.RegularExpressions;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.GitHub;
 using Elastic.Changelog.Rendering;
 using Elastic.Changelog.Utilities;
diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
index 3f66938565..2f867e3f1a 100644
--- a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
+++ b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs
@@ -4,7 +4,6 @@
 
 using System.IO.Abstractions;
 using System.Linq;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.GitHub;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Changelog;
diff --git a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
index 6d247f2a1a..7462239ee1 100644
--- a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
+++ b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs
@@ -3,8 +3,8 @@
 // See the LICENSE file in the project root for more information
 
 using System.IO.Abstractions;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.GitHub;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Configuration.Inference;
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
index a6edeb8368..f140088ad8 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs
@@ -5,7 +5,6 @@
 using System.Globalization;
 using System.IO.Abstractions;
 using Actions.Core.Services;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.Creation;
 using Elastic.Changelog.GitHub;
 using Elastic.Documentation.Configuration;
diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs
index 4308bf1e2c..aa932859dd 100644
--- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs
+++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs
@@ -6,9 +6,9 @@
 using System.Text;
 using System.Text.Json;
 using Actions.Core.Services;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.Utilities;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.Services;
 using Microsoft.Extensions.Logging;
diff --git a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
index 2351414d40..1784f98dcb 100644
--- a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
+++ b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs
@@ -6,7 +6,6 @@
 using System.IO.Abstractions;
 using System.Text;
 using Elastic.Changelog.Bundling;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.GitHub;
 using Elastic.Changelog.Utilities;
 using Elastic.Documentation;
diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs
index c5d400f046..f59297ac40 100644
--- a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs
@@ -7,7 +7,7 @@
 using System.IO.Abstractions;
 using System.Text;
 using static System.Globalization.CultureInfo;
-using static Elastic.Documentation.ChangelogEntryType;
+using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
 
 namespace Elastic.Changelog.Rendering.Asciidoc;
 
diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
index 0c8188b41b..81a895618f 100644
--- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
+++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs
@@ -5,13 +5,13 @@
 using System.ComponentModel.DataAnnotations;
 using System.IO.Abstractions;
 using System.Text.Json.Serialization;
-using Elastic.Changelog.Configuration;
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.ReleaseNotes;
 using Elastic.Documentation.Services;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging;
 using NetEscapades.EnumGenerators;
 using Nullean.ScopedFileSystem;
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
index 0434f2c902..1eae9745d4 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs
@@ -8,7 +8,7 @@
 using Elastic.Documentation.ReleaseNotes;
 using Nullean.ScopedFileSystem;
 using static System.Globalization.CultureInfo;
-using static Elastic.Documentation.ChangelogEntryType;
+using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
 
 namespace Elastic.Changelog.Rendering.Markdown;
 
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs
index 38a4afe3e5..9d0e40cfdd 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs
@@ -8,7 +8,7 @@
 using Elastic.Documentation.ReleaseNotes;
 using Nullean.ScopedFileSystem;
 using static System.Globalization.CultureInfo;
-using static Elastic.Documentation.ChangelogEntryType;
+using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
 
 namespace Elastic.Changelog.Rendering.Markdown;
 
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
index 048612f32d..43843594d8 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs
@@ -7,7 +7,7 @@
 using Elastic.Documentation.ReleaseNotes;
 using Nullean.ScopedFileSystem;
 using static System.Globalization.CultureInfo;
-using static Elastic.Documentation.ChangelogEntryType;
+using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
 
 namespace Elastic.Changelog.Rendering.Markdown;
 
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
index 73f92dee39..4531e47f9a 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs
@@ -8,7 +8,7 @@
 using Elastic.Documentation.ReleaseNotes;
 using Nullean.ScopedFileSystem;
 using static System.Globalization.CultureInfo;
-using static Elastic.Documentation.ChangelogEntryType;
+using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
 
 namespace Elastic.Changelog.Rendering.Markdown;
 
diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
index 5b9dbe521a..b84e4945e7 100644
--- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
+++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs
@@ -7,7 +7,7 @@
 using Elastic.Documentation.ReleaseNotes;
 using Nullean.ScopedFileSystem;
 using static System.Globalization.CultureInfo;
-using static Elastic.Documentation.ChangelogEntryType;
+using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType;
 
 namespace Elastic.Changelog.Rendering.Markdown;
 
diff --git a/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs b/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs
deleted file mode 100644
index 1599702cfb..0000000000
--- a/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Licensed to Elasticsearch B.V under one or more agreements.
-// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
-// See the LICENSE file in the project root for more information
-
-using YamlDotNet.Serialization;
-
-namespace Elastic.Changelog.Serialization;
-
-[YamlStaticContext]
-// YAML DTOs for CLI configuration (changelog.yml)
-[YamlSerializable(typeof(ChangelogConfigurationYaml))]
-[YamlSerializable(typeof(PivotConfigurationYaml))]
-[YamlSerializable(typeof(TypeEntryYaml))]
-[YamlSerializable(typeof(RulesConfigurationYaml))]
-[YamlSerializable(typeof(CreateRulesYaml))]
-[YamlSerializable(typeof(BundleRulesYaml))]
-[YamlSerializable(typeof(PublishRulesYaml))]
-[YamlSerializable(typeof(ProductsConfigYaml))]
-[YamlSerializable(typeof(DefaultProductYaml))]
-[YamlSerializable(typeof(BundleConfigurationYaml))]
-[YamlSerializable(typeof(BundleProfileYaml))]
-[YamlSerializable(typeof(ExtractConfigurationYaml))]
-[YamlSerializable(typeof(YamlLenientList))]
-public partial class ChangelogYamlStaticContext;
diff --git a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs
index b0baa6c81f..2c7f1bc838 100644
--- a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs
+++ b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs
@@ -5,7 +5,7 @@
 using System.IO.Abstractions;
 using System.Text.RegularExpressions;
 using Amazon.S3;
-using Elastic.Changelog.Configuration;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Configuration.ReleaseNotes;
diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
index bc9615e18d..73b60ff407 100644
--- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
@@ -5,6 +5,7 @@
 using System.IO.Abstractions;
 using System.Text;
 using Actions.Core.Services;
+using Elastic.Documentation;
 using Elastic.Documentation.Assembler.Navigation;
 using Elastic.Documentation.Assembler.Sourcing;
 using Elastic.Documentation.Configuration;
diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
index 56da04d68b..18c621c8ad 100644
--- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
+++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs
@@ -43,7 +43,7 @@ public async Task<bool> ValidateLocalLinkReference(IDiagnosticsCollector collect
 		var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null);
 
 		var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
-		var repository = GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
+		var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
 						 ?? throw new Exception("Unable to determine repository name");
 
 		var namespaceChecker = new NavigationPrefixChecker(logFactory, assembleContext);
diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
index 4bb4bcbc63..204292009f 100644
--- a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
+++ b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
@@ -5,6 +5,7 @@
 using System.IO.Abstractions;
 using Actions.Core.Services;
 using Elastic.ApiExplorer;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Builder;
 using Elastic.Documentation.Configuration.Inference;
diff --git a/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs b/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs
new file mode 100644
index 0000000000..93eb0bdf59
--- /dev/null
+++ b/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs
@@ -0,0 +1,28 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Text.Json.Serialization;
+
+namespace Elastic.Documentation.Search;
+
+/// <summary>
+/// Flat wire-format entry for the <c>applies_to</c> array indexed in Elasticsearch.
+/// One entry per (type, sub_type, lifecycle, version) tuple, preserving the exact JSON shape
+/// produced by the applicability converter in docs-builder.
+/// </summary>
+public record AppliesToEntry
+{
+	[JsonPropertyName("type")]
+	public required string Type { get; init; }
+
+	[JsonPropertyName("sub_type")]
+	public required string SubType { get; init; }
+
+	[JsonPropertyName("lifecycle")]
+	public required string Lifecycle { get; init; }
+
+	[JsonPropertyName("version")]
+	[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+	public string? Version { get; init; }
+}
diff --git a/src/Elastic.Documentation/Search/DocumentationDocument.cs b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs
similarity index 98%
rename from src/Elastic.Documentation/Search/DocumentationDocument.cs
rename to src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs
index 77e8f17da6..75244d6856 100644
--- a/src/Elastic.Documentation/Search/DocumentationDocument.cs
+++ b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs
@@ -3,7 +3,6 @@
 // See the LICENSE file in the project root for more information
 
 using System.Text.Json.Serialization;
-using Elastic.Documentation.AppliesTo;
 using Elastic.Mapping;
 
 namespace Elastic.Documentation.Search;
@@ -134,7 +133,7 @@ public string ContentType
 
 	[Nested]
 	[JsonPropertyName("applies_to")]
-	public ApplicableTo? Applies { get; set; }
+	public IReadOnlyCollection<AppliesToEntry>? Applies { get; set; }
 
 	[JsonPropertyName("body")]
 	public string? Body { get; set; }
diff --git a/src/Elastic.Documentation/Search/DocumentationMappingConfig.cs b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs
similarity index 100%
rename from src/Elastic.Documentation/Search/DocumentationMappingConfig.cs
rename to src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs
diff --git a/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj b/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj
new file mode 100644
index 0000000000..03a2f9155d
--- /dev/null
+++ b/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net10.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <RootNamespace>Elastic.Documentation.Search</RootNamespace>
+    <IsAotCompatible>true</IsAotCompatible>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <NoWarn>$(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Elastic.Mapping" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Elastic.Documentation/Search/IndexedProduct.cs b/src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs
similarity index 100%
rename from src/Elastic.Documentation/Search/IndexedProduct.cs
rename to src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs
diff --git a/src/services/Elastic.Documentation.Search/ChangesGateway.cs b/src/services/search/Elastic.Documentation.Search/ChangesService.cs
similarity index 60%
rename from src/services/Elastic.Documentation.Search/ChangesGateway.cs
rename to src/services/search/Elastic.Documentation.Search/ChangesService.cs
index 1e065a64dd..14e915ecb1 100644
--- a/src/services/Elastic.Documentation.Search/ChangesGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/ChangesService.cs
@@ -2,6 +2,9 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using System.Buffers;
+using System.Text;
+using System.Text.Json;
 using Elastic.Clients.Elasticsearch;
 using Elastic.Documentation.Search.Common;
 using Microsoft.Extensions.Logging;
@@ -9,17 +12,47 @@
 namespace Elastic.Documentation.Search;
 
 /// <summary>
-/// Elasticsearch gateway for the documentation changes feed.
+/// Elasticsearch service for the documentation changes feed.
 /// Queries content_last_updated > since with search_after cursor pagination.
 /// Uses a shared Point In Time (PIT) for consistent pagination across requests.
 /// </summary>
-public partial class ChangesGateway(
+public partial class ChangesService(
 	ElasticsearchClientAccessor clientAccessor,
 	SharedPointInTimeManager pitManager,
-	ILogger<ChangesGateway> logger
-) : IChangesGateway
+	ILogger<ChangesService> logger
+) : IChangesService
 {
-	public async Task<ChangesResult> GetChangesAsync(ChangesRequest request, Cancel ctx = default)
+	public async Task<ChangesResponse> GetChangesAsync(ChangesRequest request, Cancel ctx = default)
+	{
+		var cursor = DecodeCursor(request.Cursor);
+		var pageSize = Math.Clamp(request.PageSize, 1, ChangesDefaults.MaxPageSize);
+
+		var internalRequest = new ChangesInternalRequest
+		{
+			Since = request.Since,
+			PageSize = pageSize,
+			Cursor = cursor
+		};
+
+		var result = await GetChangesInternalAsync(internalRequest, ctx);
+
+		var nextCursor = result.NextCursor is not null
+			? EncodeCursor(result.NextCursor)
+			: null;
+
+		var hasMore = nextCursor is not null;
+
+		LogChanges(logger, request.Since, result.Pages.Count, hasMore);
+
+		return new ChangesResponse
+		{
+			Pages = result.Pages,
+			HasMore = hasMore,
+			NextCursor = nextCursor
+		};
+	}
+
+	private async Task<ChangesResult> GetChangesInternalAsync(ChangesInternalRequest request, Cancel ctx = default)
 	{
 		var fetchSize = request.PageSize + 1;
 
@@ -56,7 +89,7 @@ public async Task<ChangesResult> GetChangesAsync(ChangesRequest request, Cancel
 	}
 
 	private async Task<SearchResponse<DocumentationDocument>> Search(
-		ChangesRequest request, string pitId, int fetchSize, Cancel ctx
+		ChangesInternalRequest request, string pitId, int fetchSize, Cancel ctx
 	) =>
 		await clientAccessor.Client.SearchAsync<DocumentationDocument>(s =>
 		{
@@ -146,6 +179,60 @@ private static ChangesResult BuildResult(SearchResponse<DocumentationDocument> r
 		};
 	}
 
+	private static ChangesPageCursor? DecodeCursor(string? cursor)
+	{
+		if (string.IsNullOrWhiteSpace(cursor))
+			return null;
+
+		try
+		{
+			var remainder = cursor.Length % 4;
+			var paddingLength = (4 - remainder) % 4;
+			var base64 = cursor
+				.Replace('-', '+')
+				.Replace('_', '/')
+				+ new string('=', paddingLength);
+
+			var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64));
+			using var doc = JsonDocument.Parse(json);
+			var root = doc.RootElement;
+			var arrayLength = root.GetArrayLength();
+			if (root.ValueKind != JsonValueKind.Array || arrayLength < 2)
+				return null;
+
+			var epochEl = root[0];
+			var urlEl = root[1];
+			if (epochEl.ValueKind != JsonValueKind.Number || urlEl.ValueKind != JsonValueKind.String)
+				return null;
+
+			return new ChangesPageCursor(epochEl.GetInt64(), urlEl.GetString()!);
+		}
+		catch (Exception ex) when (ex is FormatException or JsonException or InvalidOperationException)
+		{
+			return null;
+		}
+	}
+
+	private static string EncodeCursor(ChangesPageCursor cursor)
+	{
+		var buffer = new ArrayBufferWriter<byte>();
+		using var writer = new Utf8JsonWriter(buffer);
+		writer.WriteStartArray();
+		writer.WriteNumberValue(cursor.ContentLastUpdatedEpochMs);
+		writer.WriteStringValue(cursor.Url);
+		writer.WriteEndArray();
+		writer.Flush();
+
+		return Convert.ToBase64String(buffer.WrittenSpan)
+			.TrimEnd('=')
+			.Replace('+', '-')
+			.Replace('/', '_');
+	}
+
+	[LoggerMessage(Level = LogLevel.Information,
+		Message = "Changes feed returned {Count} pages since {Since} (hasMore: {HasMore})")]
+	private static partial void LogChanges(ILogger logger, DateTimeOffset since, int count, bool hasMore);
+
 	[LoggerMessage(Level = LogLevel.Warning, Message = "PIT expired or not found, opening a new one and retrying with existing search_after position")]
 	private static partial void LogPitExpired(ILogger logger);
 }
diff --git a/src/services/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs
similarity index 100%
rename from src/services/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs
rename to src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs
diff --git a/src/services/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs b/src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs
similarity index 100%
rename from src/services/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs
rename to src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs
diff --git a/src/services/Elastic.Documentation.Search/Common/SearchResultProcessor.cs b/src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs
similarity index 100%
rename from src/services/Elastic.Documentation.Search/Common/SearchResultProcessor.cs
rename to src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs
diff --git a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
similarity index 74%
rename from src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
rename to src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
index 5b7ce28433..7287902825 100644
--- a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
+++ b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
@@ -9,8 +9,8 @@
     </PropertyGroup>
 
     <ItemGroup>
-      <ProjectReference Include="..\..\Elastic.Documentation\Elastic.Documentation.csproj" />
-      <ProjectReference Include="..\..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
+      <ProjectReference Include="..\..\..\Elastic.Documentation\Elastic.Documentation.csproj" />
+      <ProjectReference Include="..\..\..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
     </ItemGroup>
 
     <ItemGroup>
diff --git a/src/services/Elastic.Documentation.Search/EsJsonContext.cs b/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs
similarity index 100%
rename from src/services/Elastic.Documentation.Search/EsJsonContext.cs
rename to src/services/search/Elastic.Documentation.Search/EsJsonContext.cs
diff --git a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/FullSearchService.cs
similarity index 91%
rename from src/services/Elastic.Documentation.Search/FullSearchGateway.cs
rename to src/services/search/Elastic.Documentation.Search/FullSearchService.cs
index 5eb7bd8931..7d78e4ec1e 100644
--- a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/FullSearchService.cs
@@ -13,14 +13,14 @@
 namespace Elastic.Documentation.Search;
 
 /// <summary>
-/// Full-page search gateway implementation.
+/// Full-page search service implementation.
 /// Uses hybrid RRF search for semantic queries, lexical-only for keyword queries.
 /// </summary>
-public partial class FullSearchGateway(
+public partial class FullSearchService(
 	ElasticsearchClientAccessor clientAccessor,
 	ProductsConfiguration productsConfiguration,
-	ILogger<FullSearchGateway> logger)
-	: IFullSearchGateway, IDisposable
+	ILogger<FullSearchService> logger)
+	: IFullSearchService, IDisposable
 {
 	/// <summary>
 	/// Regex pattern to detect semantic/question queries.
@@ -60,17 +60,47 @@ private static bool IsSemanticQuery(string query)
 			wordCount > 3;
 	}
 
-	public async Task<FullSearchResult> SearchAsync(FullSearchRequest request, Cancel ctx = default)
+	public async Task<FullSearchResponse> SearchAsync(FullSearchRequest request, Cancel ctx = default)
 	{
 		var isSemantic = IsSemanticQuery(request.Query);
 
 		logger.LogDebug("Full search for query '{Query}' - semantic: {IsSemantic}", request.Query, isSemantic);
 
-		return isSemantic
+		var result = isSemantic
 			? await SearchWithHybridRrf(request, ctx)
 			: await SearchLexicalOnly(request, ctx);
+
+		var response = new FullSearchResponse
+		{
+			Results = result.Results,
+			TotalResults = result.TotalHits,
+			PageNumber = request.PageNumber,
+			PageSize = request.PageSize,
+			Aggregations = result.Aggregations,
+			IsSemanticQuery = result.IsSemanticQuery
+		};
+
+		LogFullSearchResults(
+			logger,
+			response.PageSize,
+			response.PageNumber,
+			request.Query,
+			result.IsSemanticQuery,
+			result.Results.Select(i => i.Url).ToArray()
+		);
+
+		return response;
 	}
 
+	[LoggerMessage(Level = LogLevel.Information, Message = "Full search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}' (semantic: {IsSemantic}): {Urls}")]
+	private static partial void LogFullSearchResults(
+		ILogger logger,
+		int pageSize,
+		int pageNumber,
+		string searchQuery,
+		bool isSemantic,
+		string[] urls);
+
 	/// <summary>
 	/// Performs hybrid RRF search combining lexical and semantic queries.
 	/// Used when the query is detected as a semantic/question query.
diff --git a/src/services/Elastic.Documentation.Search/IChangesGateway.cs b/src/services/search/Elastic.Documentation.Search/IChangesService.cs
similarity index 68%
rename from src/services/Elastic.Documentation.Search/IChangesGateway.cs
rename to src/services/search/Elastic.Documentation.Search/IChangesService.cs
index 63127c7b2d..73c67703a0 100644
--- a/src/services/Elastic.Documentation.Search/IChangesGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/IChangesService.cs
@@ -5,13 +5,29 @@
 namespace Elastic.Documentation.Search;
 
 /// <summary>Gateway interface for querying documentation page changes.</summary>
-public interface IChangesGateway
+public interface IChangesService
 {
-	Task<ChangesResult> GetChangesAsync(ChangesRequest request, Cancel ctx = default);
+	Task<ChangesResponse> GetChangesAsync(ChangesRequest request, Cancel ctx = default);
 }
 
-/// <summary>Internal request for the changes gateway.</summary>
+/// <summary>API request for the changes feed endpoint.</summary>
 public record ChangesRequest
+{
+	public required DateTimeOffset Since { get; init; }
+	public int PageSize { get; init; } = ChangesDefaults.PageSize;
+	public string? Cursor { get; init; }
+}
+
+/// <summary>API response for the changes feed endpoint.</summary>
+public record ChangesResponse
+{
+	public required IReadOnlyList<ChangedPageDto> Pages { get; init; }
+	public required bool HasMore { get; init; }
+	public string? NextCursor { get; init; }
+}
+
+/// <summary>Internal request for the changes gateway.</summary>
+public record ChangesInternalRequest
 {
 	public required DateTimeOffset Since { get; init; }
 	public int PageSize { get; init; } = ChangesDefaults.PageSize;
diff --git a/src/services/Elastic.Documentation.Search/IFullSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/IFullSearchService.cs
similarity index 79%
rename from src/services/Elastic.Documentation.Search/IFullSearchGateway.cs
rename to src/services/search/Elastic.Documentation.Search/IFullSearchService.cs
index 876ca4aee7..59af09bd69 100644
--- a/src/services/Elastic.Documentation.Search/IFullSearchGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/IFullSearchService.cs
@@ -5,12 +5,12 @@
 namespace Elastic.Documentation.Search;
 
 /// <summary>
-/// Gateway interface for full-page search operations.
+/// Service interface for full-page search operations.
 /// Supports hybrid RRF search with semantic query detection.
 /// </summary>
-public interface IFullSearchGateway
+public interface IFullSearchService
 {
-	Task<FullSearchResult> SearchAsync(FullSearchRequest request, Cancel ctx = default);
+	Task<FullSearchResponse> SearchAsync(FullSearchRequest request, Cancel ctx = default);
 }
 
 /// <summary>
@@ -31,7 +31,23 @@ public record FullSearchRequest
 }
 
 /// <summary>
-/// Result model for full-page search.
+/// Response model for full-page search (unified API+service response).
+/// </summary>
+public record FullSearchResponse
+{
+	public required IReadOnlyList<FullSearchResultItem> Results { get; init; }
+	public required int TotalResults { get; init; }
+	public required int PageNumber { get; init; }
+	public required int PageSize { get; init; }
+	public FullSearchAggregations Aggregations { get; init; } = new();
+	public bool IsSemanticQuery { get; init; }
+	public int PageCount => TotalResults > 0
+		? (int)Math.Ceiling((double)TotalResults / PageSize)
+		: 0;
+}
+
+/// <summary>
+/// Result model for full-page search (used internally by the service implementation).
 /// </summary>
 public record FullSearchResult
 {
diff --git a/src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs
similarity index 50%
rename from src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs
rename to src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs
index 8e172d1834..9fdcf6feb3 100644
--- a/src/services/Elastic.Documentation.Search/INavigationSearchGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs
@@ -4,15 +4,34 @@
 
 namespace Elastic.Documentation.Search;
 
-public interface INavigationSearchGateway
+public interface INavigationSearchService
 {
-	Task<NavigationSearchResult> NavigationSearchAsync(
-		string query,
-		int pageNumber,
-		int pageSize,
-		string? filter = null,
-		Cancel ctx = default
-	);
+	Task<NavigationSearchResponse> NavigationSearchAsync(NavigationSearchRequest request, Cancel ctx = default);
+}
+
+public record NavigationSearchRequest
+{
+	public required string Query { get; init; }
+	public int PageNumber { get; init; } = 1;
+	public int PageSize { get; init; } = 20;
+	public string? TypeFilter { get; init; }
+}
+
+public record NavigationSearchResponse
+{
+	public required IEnumerable<NavigationSearchResultItem> Results { get; init; }
+	public required int TotalResults { get; init; }
+	public required int PageNumber { get; init; }
+	public required int PageSize { get; init; }
+	public NavigationSearchAggregations Aggregations { get; init; } = new();
+	public int PageCount => TotalResults > 0
+				? (int)Math.Ceiling((double)TotalResults / PageSize)
+				: 0;
+}
+
+public record NavigationSearchAggregations
+{
+	public IReadOnlyDictionary<string, long> Type { get; init; } = new Dictionary<string, long>();
 }
 
 public record NavigationSearchResult
diff --git a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/MockSearchService.cs
similarity index 73%
rename from src/services/Elastic.Documentation.Search/MockSearchGateway.cs
rename to src/services/search/Elastic.Documentation.Search/MockSearchService.cs
index 74ced72ee9..a3dffb9672 100644
--- a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/MockSearchService.cs
@@ -4,7 +4,7 @@
 
 namespace Elastic.Documentation.Search;
 
-public class MockNavigationSearchGateway : INavigationSearchGateway
+public class MockNavigationSearchService : INavigationSearchService
 {
 	private static readonly List<NavigationSearchResultItem> Results =
 	[
@@ -80,39 +80,53 @@ public class MockNavigationSearchGateway : INavigationSearchGateway
 		}
 	];
 
-	public async Task<NavigationSearchResult> NavigationSearchAsync(string query, int pageNumber, int pageSize, string? filter = null, CancellationToken ctx = default)
+	public async Task<NavigationSearchResponse> NavigationSearchAsync(NavigationSearchRequest request, CancellationToken ctx = default)
 	{
 		var filteredResults = Results
 			.Where(item =>
-				item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
-				item.Description?.Contains(query, StringComparison.OrdinalIgnoreCase) == true)
+				item.Title.Contains(request.Query, StringComparison.OrdinalIgnoreCase) ||
+				item.Description?.Contains(request.Query, StringComparison.OrdinalIgnoreCase) == true)
 			.ToList();
 
 		// Apply type filter if specified
-		if (!string.IsNullOrWhiteSpace(filter))
-			filteredResults = filteredResults.Where(item => item.Type == filter).ToList();
+		if (!string.IsNullOrWhiteSpace(request.TypeFilter))
+			filteredResults = filteredResults.Where(item => item.Type == request.TypeFilter).ToList();
 
 		// Calculate aggregations before filtering
 		var aggregations = Results
 			.Where(item =>
-				item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
-				item.Description?.Contains(query, StringComparison.OrdinalIgnoreCase) == true)
+				item.Title.Contains(request.Query, StringComparison.OrdinalIgnoreCase) ||
+				item.Description?.Contains(request.Query, StringComparison.OrdinalIgnoreCase) == true)
 			.GroupBy(item => item.Type)
 			.ToDictionary(g => g.Key, g => (long)g.Count());
 
 		var pagedResults = filteredResults
-			.Skip((pageNumber - 1) * pageSize)
-			.Take(pageSize)
+			.Skip((request.PageNumber - 1) * request.PageSize)
+			.Take(request.PageSize)
 			.ToList();
 
-		Console.WriteLine($"MockSearchGateway: Paged results count: {pagedResults.Count}");
+		Console.WriteLine($"MockSearchService: Paged results count: {pagedResults.Count}");
 
 		await Task.Delay(1000, ctx);
-		return new NavigationSearchResult
+		return new NavigationSearchResponse
 		{
-			TotalHits = filteredResults.Count,
+			TotalResults = filteredResults.Count,
 			Results = pagedResults,
-			Aggregations = aggregations
+			PageNumber = request.PageNumber,
+			PageSize = request.PageSize,
+			Aggregations = new NavigationSearchAggregations { Type = aggregations }
 		};
 	}
 }
+
+public class MockFullSearchService : IFullSearchService
+{
+	public Task<FullSearchResponse> SearchAsync(FullSearchRequest request, Cancel ctx = default) =>
+		Task.FromResult(new FullSearchResponse
+		{
+			Results = [],
+			TotalResults = 0,
+			PageNumber = request.PageNumber,
+			PageSize = request.PageSize
+		});
+}
diff --git a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs
similarity index 87%
rename from src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs
rename to src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs
index 67f3814c0c..49d3267a21 100644
--- a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs
+++ b/src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs
@@ -12,16 +12,40 @@
 namespace Elastic.Documentation.Search;
 
 /// <summary>
-/// Elasticsearch gateway for Navigation Search (autocomplete/navigation search).
+/// Elasticsearch service for Navigation Search (autocomplete/navigation search).
 /// Uses shared lexical query optimized for autocomplete.
 /// </summary>
-public class NavigationSearchGateway(ElasticsearchClientAccessor clientAccessor, ILogger<NavigationSearchGateway> logger)
-	: INavigationSearchGateway, IDisposable
+public partial class NavigationSearchService(ElasticsearchClientAccessor clientAccessor, ILogger<NavigationSearchService> logger)
+	: INavigationSearchService, IDisposable
 {
 	public async Task<bool> CanConnect(Cancel ctx) => await clientAccessor.CanConnect(ctx);
 
-	public async Task<NavigationSearchResult> NavigationSearchAsync(string query, int pageNumber, int pageSize, string? filter = null, Cancel ctx = default) =>
-		await SearchImplementation(query, pageNumber, pageSize, filter, ctx);
+	public async Task<NavigationSearchResponse> NavigationSearchAsync(NavigationSearchRequest request, Cancel ctx = default)
+	{
+		var result = await SearchImplementation(request.Query, request.PageNumber, request.PageSize, request.TypeFilter, ctx);
+
+		var response = new NavigationSearchResponse
+		{
+			Results = result.Results,
+			TotalResults = result.TotalHits,
+			PageNumber = request.PageNumber,
+			PageSize = request.PageSize,
+			Aggregations = new NavigationSearchAggregations { Type = result.Aggregations }
+		};
+
+		LogNavigationSearchResults(
+			logger,
+			response.PageSize,
+			response.PageNumber,
+			request.Query,
+			result.Results.Select(i => i.Url).ToArray()
+		);
+
+		return response;
+	}
+
+	[LoggerMessage(Level = LogLevel.Information, Message = "Navigation search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}': {Urls}")]
+	private static partial void LogNavigationSearchResults(ILogger logger, int pageSize, int pageNumber, string searchQuery, string[] urls);
 
 	public async Task<NavigationSearchResult> SearchImplementation(string query, int pageNumber, int pageSize, string? filter = null, Cancel ctx = default)
 	{
diff --git a/src/services/Elastic.Documentation.Search/ServicesExtension.cs b/src/services/search/Elastic.Documentation.Search/ServicesExtension.cs
similarity index 85%
rename from src/services/Elastic.Documentation.Search/ServicesExtension.cs
rename to src/services/search/Elastic.Documentation.Search/ServicesExtension.cs
index 373b5a1c3a..b6f92c4133 100644
--- a/src/services/Elastic.Documentation.Search/ServicesExtension.cs
+++ b/src/services/search/Elastic.Documentation.Search/ServicesExtension.cs
@@ -27,15 +27,15 @@ public static IServiceCollection AddSearchServices(this IServiceCollection servi
 		_ = services.AddSingleton<ElasticsearchClientAccessor>();
 
 		// Navigation Search (autocomplete/navigation search)
-		_ = services.AddScoped<INavigationSearchGateway, NavigationSearchGateway>();
+		_ = services.AddScoped<INavigationSearchService, NavigationSearchService>();
 
 		// FullSearch (full-page search with hybrid RRF)
-		_ = services.AddScoped<IFullSearchGateway, FullSearchGateway>();
-		logger?.LogInformation("Full search gateways registered with hybrid RRF support");
+		_ = services.AddScoped<IFullSearchService, FullSearchService>();
+		logger?.LogInformation("Full search services registered with hybrid RRF support");
 
 		// Changes feed (cursor-paginated changes since a given date)
 		_ = services.AddSingleton<SharedPointInTimeManager>();
-		_ = services.AddScoped<IChangesGateway, ChangesGateway>();
+		_ = services.AddScoped<IChangesService, ChangesService>();
 
 		return services;
 	}
diff --git a/src/services/Elastic.Documentation.Search/SharedPointInTimeManager.cs b/src/services/search/Elastic.Documentation.Search/SharedPointInTimeManager.cs
similarity index 100%
rename from src/services/Elastic.Documentation.Search/SharedPointInTimeManager.cs
rename to src/services/search/Elastic.Documentation.Search/SharedPointInTimeManager.cs
diff --git a/src/services/Elastic.Documentation.Search/StringHighlightExtensions.cs b/src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs
similarity index 100%
rename from src/services/Elastic.Documentation.Search/StringHighlightExtensions.cs
rename to src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs
diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs
index 909c56823e..3c6a2c5f70 100644
--- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs
+++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs
@@ -11,7 +11,6 @@
 using Documentation.Builder.Arguments;
 using Elastic.Changelog;
 using Elastic.Changelog.Bundling;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.Creation;
 using Elastic.Changelog.Evaluation;
 using Elastic.Changelog.GitHub;
@@ -19,7 +18,9 @@
 using Elastic.Changelog.Rendering;
 using Elastic.Changelog.Uploading;
 using Elastic.Changelog.Utilities;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.ReleaseNotes;
 using Elastic.Documentation.Services;
diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
index cf89234236..b48d33b9a0 100644
--- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs
+++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs
@@ -10,6 +10,7 @@
 using System.Text.Json;
 using Documentation.Builder.Diagnostics.LiveMode;
 using Elastic.Documentation;
+using Elastic.Documentation.Diagnostics;
 #if DEBUG
 using Elastic.Documentation.Api;
 #endif
@@ -34,7 +35,7 @@ public class DocumentationWebHost
 {
 	private readonly WebApplication _webApplication;
 
-	private readonly IHostedService _hostedService;
+	private readonly IDiagnosticsCollector _hostedService;
 	private readonly ScopedFileSystem _writeFileSystem;
 
 	public InMemoryBuildState InMemoryBuildState { get; }
@@ -53,7 +54,7 @@ bool isWatchBuild
 		_ = builder.AddDocumentationServiceDefaults();
 
 #if DEBUG
-		builder.Services.AddElasticDocsApiUsecases("dev");
+		builder.Services.AddElasticDocsApiServices("dev");
 #endif
 
 		_ = builder.Logging
diff --git a/src/tooling/docs-builder/Http/StaticWebHost.cs b/src/tooling/docs-builder/Http/StaticWebHost.cs
index 539ed4e8ac..250d772184 100644
--- a/src/tooling/docs-builder/Http/StaticWebHost.cs
+++ b/src/tooling/docs-builder/Http/StaticWebHost.cs
@@ -39,7 +39,7 @@ public StaticWebHost(int port, string? path)
 
 		_ = builder.AddDocumentationServiceDefaults();
 #if DEBUG
-		builder.Services.AddElasticDocsApiUsecases("dev");
+		builder.Services.AddElasticDocsApiServices("dev");
 #endif
 
 		_ = builder.Logging
diff --git a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
index 34f41953ff..35670eadd2 100644
--- a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
+++ b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
@@ -6,6 +6,7 @@
 using System.Reflection;
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging;
 using Nullean.Argh.Middleware;
 
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs
index 4e3e212695..4f351d3927 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs
@@ -4,7 +4,7 @@
 
 using System.Net.Http.Json;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Search;
+using Elastic.Documentation.Search;
 
 namespace Elastic.Assembler.IntegrationTests.Search;
 
@@ -55,18 +55,18 @@ public async Task SearchEndpointReturnsExpectedFirstResult(string query, string
 		// Assert - Response should be successful
 		response.EnsureSuccessStatusCode();
 
-		var searchResponse = await response.Content.ReadFromJsonAsync<NavigationSearchApiResponse>(cancellationToken: TestContext.Current.CancellationToken);
+		var searchResponse = await response.Content.ReadFromJsonAsync<FullSearchResponse>(cancellationToken: TestContext.Current.CancellationToken);
 		searchResponse.Should().NotBeNull("Search response should be deserialized");
 
 		// Log results for debugging
 		output.WriteLine($"Query: {query}");
 		output.WriteLine($"Total results: {searchResponse.TotalResults}");
-		output.WriteLine($"Results returned: {searchResponse.Results.Count()}");
+		output.WriteLine($"Results returned: {searchResponse.Results.Count}");
 
 		if (searchResponse.Results.Any())
 		{
 			output.WriteLine("First result:");
-			var firstResult = searchResponse.Results.First();
+			var firstResult = searchResponse.Results[0];
 			output.WriteLine($"  Title: {firstResult.Title}");
 			output.WriteLine($"  URL: {firstResult.Url}");
 			output.WriteLine($"  Score: {firstResult.Score}");
@@ -76,7 +76,7 @@ public async Task SearchEndpointReturnsExpectedFirstResult(string query, string
 		searchResponse.Results.Should().NotBeEmpty($"Search for '{query}' should return results");
 
 		// Assert - First result should match expected URL
-		var actualFirstResultUrl = searchResponse.Results.First().Url;
+		var actualFirstResultUrl = searchResponse.Results[0].Url;
 		actualFirstResultUrl.Should().Be(expectedFirstResultUrl,
 			$"First result for query '{query}' should be the expected documentation page");
 	}
@@ -93,12 +93,12 @@ public async Task SearchEndpointWithPaginationReturnsCorrectPage()
 		// Act - Get first page
 		var page1Response = await searchFixture.HttpClient.GetAsync($"/docs/_api/v1/search?q={Uri.EscapeDataString(query)}&page=1", TestContext.Current.CancellationToken);
 		page1Response.EnsureSuccessStatusCode();
-		var page1Data = await page1Response.Content.ReadFromJsonAsync<NavigationSearchApiResponse>(cancellationToken: TestContext.Current.CancellationToken);
+		var page1Data = await page1Response.Content.ReadFromJsonAsync<FullSearchResponse>(cancellationToken: TestContext.Current.CancellationToken);
 
 		// Act - Get second page
 		var page2Response = await searchFixture.HttpClient.GetAsync($"/docs/_api/v1/search?q={Uri.EscapeDataString(query)}&page=2", TestContext.Current.CancellationToken);
 		page2Response.EnsureSuccessStatusCode();
-		var page2Data = await page2Response.Content.ReadFromJsonAsync<NavigationSearchApiResponse>(cancellationToken: TestContext.Current.CancellationToken);
+		var page2Data = await page2Response.Content.ReadFromJsonAsync<FullSearchResponse>(cancellationToken: TestContext.Current.CancellationToken);
 
 		// Assert
 		page1Data.Should().NotBeNull();
diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs
index 95a250c570..01bb997ef0 100644
--- a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs
+++ b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs
@@ -10,6 +10,7 @@
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Elastic.Assembler.IntegrationTests;
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs
index c2a28743bc..c555951748 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs
@@ -50,8 +50,8 @@ public async Task AskAiEndpointPropagatatesEuidToAllSpansAndLogs()
 		// Create factory with mocked AskAi services
 		using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
 		{
-			// Mock IAskAiGateway to avoid external AI service calls
-			var mockAskAiGateway = A.Fake<IAskAiGateway>();
+			// Mock IAskAiService to avoid external AI service calls
+			var mockAskAiGateway = A.Fake<IAskAiService>();
 			A.CallTo(() => mockAskAiGateway.AskAi(A<AskAiRequest>._, A<Cancel>._))
 				.ReturnsLazily(() =>
 				{
@@ -113,11 +113,10 @@ public async Task AskAiEndpointPropagatatesEuidToAllSpansAndLogs()
 		var logRecords = factory.ExportedLogRecords;
 		logRecords.Should().NotBeEmpty("Should have captured log records");
 
-		// Find a log entry from AskAiUsecase
+		// Find a log entry from the AskAI endpoint handler
 		var askAiLogRecord = logRecords.FirstOrDefault(r =>
-			string.Equals(r.CategoryName, typeof(AskAiUsecase).FullName, StringComparison.OrdinalIgnoreCase) &&
 			r.FormattedMessage?.Contains("Starting AskAI", StringComparison.OrdinalIgnoreCase) == true);
-		askAiLogRecord.Should().NotBeNull("Should have logged from AskAiUsecase");
+		askAiLogRecord.Should().NotBeNull("Should have logged from AskAI endpoint handler");
 
 		// Verify euid is present in OTEL log attributes (mirrors production exporter behavior)
 		var euidAttribute = askAiLogRecord.Attributes?.FirstOrDefault(a => a.Key == TelemetryConstants.UserEuidAttributeName) ?? default;
diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs
index 714ba2b7ac..99db7df7da 100644
--- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs
+++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs
@@ -5,8 +5,8 @@
 using System.Net;
 using System.Text;
 using AwesomeAssertions;
-using Elastic.Documentation.Api.Adapters.Telemetry;
 using Elastic.Documentation.Api.IntegrationTests.Fixtures;
+using Elastic.Documentation.Api.Telemetry;
 using FakeItEasy;
 using Microsoft.Extensions.DependencyInjection;
 using Xunit;
@@ -52,7 +52,7 @@ public async Task OtlpProxyTracesEndpointForwardsToCorrectUrl()
 		using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
 		{
 			// Replace the named HttpClient with our mock
-			_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
+			_ = services.AddHttpClient(AdotOtlpService.HttpClientName)
 				.ConfigurePrimaryHttpMessageHandler(() => mockHandler);
 		});
 
@@ -116,7 +116,7 @@ public async Task OtlpProxyLogsEndpointForwardsToCorrectUrl()
 
 		using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
 		{
-			_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
+			_ = services.AddHttpClient(AdotOtlpService.HttpClientName)
 				.ConfigurePrimaryHttpMessageHandler(() => mockHandler);
 		});
 
@@ -173,7 +173,7 @@ public async Task OtlpProxyMetricsEndpointForwardsToCorrectUrl()
 
 		using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
 		{
-			_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
+			_ = services.AddHttpClient(AdotOtlpService.HttpClientName)
 				.ConfigurePrimaryHttpMessageHandler(() => mockHandler);
 		});
 
@@ -225,7 +225,7 @@ public async Task OtlpProxyReturnsCollectorErrorStatusCode()
 		using var factory = ApiWebApplicationFactory.WithMockedServices(services =>
 		{
 #pragma warning disable EXTEXP0001 // Experimental API - needed for test to bypass resilience handlers
-			_ = services.AddHttpClient(AdotOtlpGateway.HttpClientName)
+			_ = services.AddHttpClient(AdotOtlpService.HttpClientName)
 				.ConfigurePrimaryHttpMessageHandler(() => mockHandler)
 				.RemoveAllResilienceHandlers();
 #pragma warning restore EXTEXP0001
diff --git a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj
index e18027f7c3..0b21ba1622 100644
--- a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj
+++ b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj
@@ -8,7 +8,7 @@
 
     <ItemGroup>
       <ProjectReference Include="..\..\src\api\Elastic.Documentation.Mcp.Remote\Elastic.Documentation.Mcp.Remote.csproj" />
-      <ProjectReference Include="..\..\src\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
+      <ProjectReference Include="..\..\src\services\search\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
       <ProjectReference Include="..\..\src\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
     </ItemGroup>
 
diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
index 7dc58a73b8..7c00de5d58 100644
--- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
+++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
@@ -47,10 +47,10 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C
 		var clientAccessor = CreateElasticsearchClientAccessor();
 
 		var productsConfig = CreateProductsConfiguration();
-		var fullSearchGateway = new FullSearchGateway(
+		var fullSearchGateway = new FullSearchService(
 			clientAccessor,
 			productsConfig,
-			NullLogger<FullSearchGateway>.Instance
+			NullLogger<FullSearchService>.Instance
 		);
 
 		var searchTools = new SearchTools(fullSearchGateway, NullLogger<SearchTools>.Instance);
@@ -77,7 +77,7 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C
 		var clientAccessor = CreateElasticsearchClientAccessor();
 
 		var productsConfig = CreateProductsConfiguration();
-		var fullSearchGateway = new FullSearchGateway(clientAccessor, productsConfig, NullLogger<FullSearchGateway>.Instance);
+		var fullSearchGateway = new FullSearchService(clientAccessor, productsConfig, NullLogger<FullSearchService>.Instance);
 		var coherenceTools = new CoherenceTools(fullSearchGateway, NullLogger<CoherenceTools>.Instance);
 		return (coherenceTools, clientAccessor);
 	}
diff --git a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj
index c7dcf104d1..e2367742fd 100644
--- a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj
+++ b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj
@@ -8,7 +8,7 @@
 
     <ItemGroup>
       <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
-      <ProjectReference Include="..\..\src\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
+      <ProjectReference Include="..\..\src\services\search\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
       <ProjectReference Include="..\..\src\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
     </ItemGroup>
 
diff --git a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
index 4f25592bf8..060a75e7ff 100644
--- a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
+++ b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
@@ -239,14 +239,14 @@ public async Task ExplainTopResultAndExpectedAsyncReturnsDetailedScoring()
 	/// <summary>
 	/// Creates an ElasticsearchGateway instance using configuration from the distributed application.
 	/// </summary>
-	private static (NavigationSearchGateway Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway()
+	private static (NavigationSearchService Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway()
 	{
 		var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler", environment: "dev");
 		var configProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new FileSystem(), configurationSource: ConfigurationSource.Embedded);
 		var searchConfig = configProvider.CreateSearchConfiguration();
 
 		var clientAccessor = new ElasticsearchClientAccessor(endpoints, searchConfig);
-		var gateway = new NavigationSearchGateway(clientAccessor, NullLogger<NavigationSearchGateway>.Instance);
+		var gateway = new NavigationSearchService(clientAccessor, NullLogger<NavigationSearchService>.Instance);
 		return (gateway, clientAccessor);
 	}
 }
diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs
index a2ed6c78ae..86433fd286 100644
--- a/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs
+++ b/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs
@@ -8,6 +8,7 @@
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration.Versions;
 using Elastic.Documentation.Search;
+using Elastic.Documentation.Versions;
 using static System.StringComparison;
 
 namespace Elastic.ApiExplorer.Tests;
diff --git a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs
index eed99c1bcb..26727feb30 100644
--- a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs
+++ b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs
@@ -12,6 +12,7 @@
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Elastic.ApiExplorer.Tests;
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs
index 712b379e35..3bfe1528c2 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs
@@ -3,11 +3,11 @@
 // See the LICENSE file in the project root for more information
 
 using AwesomeAssertions;
-using Elastic.Changelog.Configuration;
-using Elastic.Changelog.Serialization;
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Configuration.Changelog;
+using Elastic.Documentation.Configuration.Changelog;
+using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.ReleaseNotes;
 
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs
index 5e3b93a57d..832d20ab2c 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs
@@ -11,6 +11,7 @@
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Nullean.ScopedFileSystem;
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs
index e00e7ddb75..1c485eedfb 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs
@@ -4,6 +4,7 @@
 
 using Elastic.Changelog.Creation;
 using Elastic.Changelog.GitHub;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using FakeItEasy;
 
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs
index 0f031aacb7..900013ac71 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs
@@ -5,6 +5,7 @@
 using AwesomeAssertions;
 using Elastic.Changelog.Creation;
 using Elastic.Changelog.GitHub;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using FakeItEasy;
 
diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs
index bd72bf9c67..03f0c54020 100644
--- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs
+++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs
@@ -4,9 +4,9 @@
 
 using AwesomeAssertions;
 using Elastic.Changelog.Bundling;
-using Elastic.Changelog.Configuration;
 using Elastic.Changelog.Rendering;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Changelog;
 using Elastic.Documentation.Diagnostics;
 
 namespace Elastic.Changelog.Tests.Changelogs.Render;
diff --git a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
index bba674d2c4..ab38e30eb0 100644
--- a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
+++ b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
@@ -5,6 +5,7 @@
 using AwesomeAssertions;
 using Elastic.Changelog.Creation;
 using Elastic.Changelog.Tests.Changelogs;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using FakeItEasy;
 
diff --git a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs
index b4f7e4fc30..60480bbc4f 100644
--- a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs
+++ b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs
@@ -9,6 +9,7 @@
 using Elastic.Changelog.GitHub;
 using Elastic.Changelog.Tests.Changelogs;
 using Elastic.Changelog.Utilities;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 using FakeItEasy;
 
diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj
index 150e8f13af..67afbae2e4 100644
--- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj
+++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj
@@ -6,7 +6,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
-    <ProjectReference Include="..\..\src\services\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
+    <ProjectReference Include="..\..\src\services\search\Elastic.Documentation.Search\Elastic.Documentation.Search.csproj" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs
index 45cb7deaee..284b950c4a 100644
--- a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs
+++ b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs
@@ -2,6 +2,7 @@
 // Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
 // See the LICENSE file in the project root for more information
 
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 
 namespace Elastic.Documentation.Build.Tests;
diff --git a/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs b/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs
index e165b09555..87bc8daec9 100644
--- a/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs
+++ b/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs
@@ -5,6 +5,7 @@
 using System.IO.Abstractions.TestingHelpers;
 using System.Security;
 using AwesomeAssertions;
+using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
 
 namespace Elastic.Documentation.Build.Tests;
diff --git a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs
index 51b84cd905..804502ebca 100644
--- a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs
+++ b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs
@@ -15,6 +15,7 @@
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Configuration.Versions;
 using Elastic.Documentation.Diagnostics;
+using Elastic.Documentation.Versions;
 using FakeItEasy;
 using Microsoft.Extensions.Logging;
 
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs
index f9c9a6ddff..7f27e0838a 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs
@@ -458,6 +458,6 @@ private sealed class MockDocumentationSetContext(
 		public IFileInfo ConfigurationPath => configurationPath;
 		public BuildType BuildType => BuildType.Isolated;
 		public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory;
-		public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem);
+		public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem);
 	}
 }
diff --git a/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs b/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs
index 35a6845c14..fbece05e0c 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs
@@ -6,6 +6,7 @@
 using Elastic.Documentation.Configuration.Assembler;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using MatchResult = Elastic.Documentation.Configuration.Assembler.AssemblyConfiguration.ContentSourceMatch;
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs
index 6c31693f72..1cb21c1e3e 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs
@@ -93,6 +93,6 @@ private sealed class MockDocumentationSetContext(
 		public IFileInfo ConfigurationPath => configurationPath;
 		public BuildType BuildType => BuildType.Isolated;
 		public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory;
-		public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem);
+		public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem);
 	}
 }
diff --git a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs
index ed224f2ab8..5e34366c80 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs
@@ -139,6 +139,6 @@ private sealed class MockDocumentationSetContext(
 		public IFileInfo ConfigurationPath => configurationPath;
 		public BuildType BuildType => BuildType.Isolated;
 		public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory;
-		public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem);
+		public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem);
 	}
 }
diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs
index 00a5796e9b..dd4268bb79 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs
@@ -9,6 +9,7 @@
 using Elastic.Documentation.Configuration.LegacyUrlMappings;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.Configuration.Tests;
 
diff --git a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj
index 0dc80a953d..fc32e597e0 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj
+++ b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj
@@ -6,6 +6,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\src\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj"/>
+    <ProjectReference Include="..\..\src\Elastic.Documentation.Tooling\Elastic.Documentation.Tooling.csproj"/>
   </ItemGroup>
 
 </Project>
diff --git a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs
index 4d6a17173d..172c628b19 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using AwesomeAssertions;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Documentation.Configuration.Tests.ReleaseNotes;
 
diff --git a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs
index 0198135682..4ea6c22fd2 100644
--- a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs
+++ b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs
@@ -9,6 +9,7 @@
 using Elastic.Documentation.Configuration.Inference;
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Elastic.Documentation.Configuration.Tests;
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs
index 66aaf303e6..d71b39d32d 100644
--- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs
+++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs
@@ -6,6 +6,7 @@
 using AwesomeAssertions;
 using Elastic.Documentation;
 using Elastic.Documentation.AppliesTo;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Tests.AppliesTo;
 
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs
index fd5ab13d01..376f7748ba 100644
--- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs
+++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs
@@ -7,6 +7,7 @@
 using AwesomeAssertions;
 using Elastic.Documentation;
 using Elastic.Documentation.AppliesTo;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Tests.AppliesTo;
 
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs
index c8700f00c6..34dbf86941 100644
--- a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs
+++ b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs
@@ -3,10 +3,11 @@
 // See the LICENSE file in the project root for more information
 
 using System.Reflection;
+using System.Text.Json.Serialization;
 using AwesomeAssertions;
 using Elastic.Documentation;
 using Elastic.Documentation.AppliesTo;
-using YamlDotNet.Serialization;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Tests.AppliesTo;
 
@@ -19,7 +20,7 @@ public void ProductApplicabilityToStringIncludesAllProperties()
 		var productApplicability = new ProductApplicability();
 		var productType = typeof(ProductApplicability);
 		var properties = productType.GetProperties()
-			.Where(p => p.GetCustomAttribute<YamlMemberAttribute>() != null)
+			.Where(p => p.GetCustomAttribute<JsonPropertyNameAttribute>() != null)
 			.ToList();
 
 		// Set all properties to a test value
@@ -35,9 +36,9 @@ public void ProductApplicabilityToStringIncludesAllProperties()
 		// Verify that each property's YAML alias appears in the output
 		foreach (var property in properties)
 		{
-			var yamlAlias = property.GetCustomAttribute<YamlMemberAttribute>()!.Alias;
-			result.Should().Contain($"{yamlAlias}=",
-				$"ToString should include the property {property.Name} with alias '{yamlAlias}'");
+			var jsonName = property.GetCustomAttribute<JsonPropertyNameAttribute>()!.Name;
+			result.Should().Contain($"{jsonName}=",
+				$"ToString should include the property {property.Name} with alias '{jsonName}'");
 		}
 
 		// Verify we have the expected number of properties
@@ -90,8 +91,8 @@ public void ProductApplicabilityToStringPropertyOrderMatchesReflectionOrder()
 		// Get the properties in reflection order
 		var productType = typeof(ProductApplicability);
 		var properties = productType.GetProperties()
-			.Where(p => p.GetCustomAttribute<YamlMemberAttribute>() != null)
-			.Select(p => p.GetCustomAttribute<YamlMemberAttribute>()!.Alias)
+			.Where(p => p.GetCustomAttribute<JsonPropertyNameAttribute>() != null)
+			.Select(p => p.GetCustomAttribute<JsonPropertyNameAttribute>()!.Name)
 			.ToList();
 
 		// Find positions in the string
diff --git a/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs b/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs
index 8072ebba37..74185b46ce 100644
--- a/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs
+++ b/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs
@@ -6,6 +6,7 @@
 using AwesomeAssertions;
 using Elastic.Documentation;
 using Elastic.Documentation.Configuration;
+using Elastic.Documentation.Configuration.Builder;
 using Elastic.Documentation.Links;
 using Elastic.Documentation.Links.CrossLinks;
 
diff --git a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs
index 88f247d8bd..4b41e3e982 100644
--- a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs
@@ -4,6 +4,7 @@
 
 using AwesomeAssertions;
 using Elastic.Documentation;
+using Elastic.Documentation.Versions;
 using Elastic.Markdown.Myst.Directives.Version;
 
 namespace Elastic.Markdown.Tests.Directives;
diff --git a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs
index f09101d480..21ccbf2707 100644
--- a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs
@@ -35,7 +35,7 @@ public class GitCheckoutInformationTests(ITestOutputHelper output) : NavigationT
 	public void Create()
 	{
 		var root = ReadFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
-		var git = GitCheckoutInformation.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation)));
+		var git = GitCheckoutInformationFactory.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation)));
 
 		git.Should().NotBeNull();
 		git.Branch.Should().NotBeNullOrWhiteSpace();
diff --git a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj
index a2a90aa4d3..5d1439526f 100644
--- a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj
+++ b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj
@@ -13,6 +13,7 @@
   <ItemGroup>
     <ProjectReference Include="..\..\src\authoring\Elastic.Documentation.Refactor\Elastic.Documentation.Refactor.csproj" />
     <ProjectReference Include="..\..\src\Elastic.Markdown\Elastic.Markdown.csproj"/>
+    <ProjectReference Include="..\..\src\Elastic.Documentation.Tooling\Elastic.Documentation.Tooling.csproj"/>
   </ItemGroup>
 
 </Project>
diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs
index b85bc26db0..8381feb999 100644
--- a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs
+++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs
@@ -9,6 +9,7 @@
 using Elastic.Documentation.AppliesTo;
 using Elastic.Documentation.Search;
 using Elastic.Documentation.Serialization;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Tests.Search;
 
@@ -28,7 +29,7 @@ public void SerializeDocumentWithStackAppliesToProducesCorrectJson()
 			Applies = new ApplicableTo
 			{
 				Stack = AppliesCollection.GenerallyAvailable
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -66,7 +67,7 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson()
 					Ess = AppliesCollection.GenerallyAvailable,
 					Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"3.5.0" }])
 				}
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -108,7 +109,7 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson()
 					Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]),
 					Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"1.0.0" }])
 				}
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -146,7 +147,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson()
 			Applies = new ApplicableTo
 			{
 				Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }])
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -180,7 +181,7 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson()
 					ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.5.0" }]),
 					ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"2.0.0" }])
 				}
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -226,7 +227,7 @@ public void SerializeDocumentWithComplexAppliesToProducesCorrectJson()
 				{
 					Elasticsearch = AppliesCollection.GenerallyAvailable
 				}
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -279,7 +280,7 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray()
 			Url = "/test/empty-applies",
 			Title = "Empty Applies Test",
 			SearchTitle = "Empty Applies Test",
-			Applies = new ApplicableTo()
+			Applies = new ApplicableTo().ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
@@ -294,6 +295,15 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray()
 	[Fact]
 	public void RoundTripDocumentWithAppliesToPreservesData()
 	{
+		var originalApplies = new ApplicableTo
+		{
+			Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]),
+			Deployment = new DeploymentApplicability
+			{
+				Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }])
+			}
+		}.ToAppliesTo();
+
 		var original = new DocumentationDocument
 		{
 			Type = "doc",
@@ -305,14 +315,7 @@ public void RoundTripDocumentWithAppliesToPreservesData()
 			LastUpdated = DateTimeOffset.Parse("2024-01-15T09:00:00Z", CultureInfo.InvariantCulture),
 			ContentLastUpdated = DateTimeOffset.Parse("2024-01-14T08:00:00Z", CultureInfo.InvariantCulture),
 			ContentBodyHash = "abc123def456abc1",
-			Applies = new ApplicableTo
-			{
-				Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]),
-				Deployment = new DeploymentApplicability
-				{
-					Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }])
-				}
-			},
+			Applies = originalApplies,
 			Headings = ["Introduction", "Getting Started"],
 			Links = ["/link1", "/link2"],
 			Body = "Test body content",
@@ -327,9 +330,9 @@ public void RoundTripDocumentWithAppliesToPreservesData()
 		deserialized.Url.Should().Be(original.Url);
 		deserialized.Title.Should().Be(original.Title);
 		deserialized.Applies.Should().NotBeNull();
-		deserialized.Applies.Stack.Should().BeEquivalentTo(original.Applies.Stack);
-		deserialized.Applies.Deployment.Should().NotBeNull();
-		deserialized.Applies.Deployment.Ess.Should().BeEquivalentTo(original.Applies.Deployment.Ess);
+		deserialized.Applies.Should().HaveCount(2);
+		deserialized.Applies.Should().Contain(e => e.Type == "stack" && e.Lifecycle == "ga" && e.Version == "8.5+");
+		deserialized.Applies.Should().Contain(e => e.Type == "deployment" && e.SubType == "ess" && e.Lifecycle == "beta" && e.Version == "8.6+");
 		deserialized.ContentLastUpdated.Should().Be(original.ContentLastUpdated);
 		deserialized.ContentBodyHash.Should().Be(original.ContentBodyHash);
 		deserialized.ContentType.Should().Be(original.ContentType);
@@ -395,7 +398,7 @@ public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleA
 					new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" },
 					new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"7.0.0" }
 				])
-			}
+			}.ToAppliesTo()
 		};
 
 		var json = JsonSerializer.Serialize(doc, _options);
diff --git a/tests/Elastic.Markdown.Tests/TestHelpers.cs b/tests/Elastic.Markdown.Tests/TestHelpers.cs
index e8d9aac96a..2aa37abeab 100644
--- a/tests/Elastic.Markdown.Tests/TestHelpers.cs
+++ b/tests/Elastic.Markdown.Tests/TestHelpers.cs
@@ -10,6 +10,7 @@
 using Elastic.Documentation.Configuration.Products;
 using Elastic.Documentation.Configuration.Search;
 using Elastic.Documentation.Configuration.Versions;
+using Elastic.Documentation.Versions;
 
 namespace Elastic.Markdown.Tests;
 
diff --git a/tests/authoring/Framework/Setup.fs b/tests/authoring/Framework/Setup.fs
index d89f426d12..9f79710fca 100644
--- a/tests/authoring/Framework/Setup.fs
+++ b/tests/authoring/Framework/Setup.fs
@@ -13,6 +13,7 @@ open System.IO.Abstractions.TestingHelpers
 open System.Threading.Tasks
 open YamlDotNet.RepresentationModel
 open Elastic.Documentation
+open Elastic.Documentation.Versions
 open Elastic.Documentation.Configuration
 open Elastic.Documentation.Configuration.LegacyUrlMappings
 open Elastic.Documentation.Configuration.Versions
