File: AtsGoCodeGenerator.cs
Web Access
Project: src\src\Aspire.Hosting.CodeGeneration.Go\Aspire.Hosting.CodeGeneration.Go.csproj (Aspire.Hosting.CodeGeneration.Go)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Shared.CodeGeneration;
using Aspire.Shared.Json;
using Aspire.TypeSystem;
 
namespace Aspire.Hosting.CodeGeneration.Go;
 
internal sealed class GoExportedValueTreeNode
{
    public Dictionary<string, GoExportedValueTreeNode> Children { get; } = new(StringComparer.Ordinal);
 
    public AtsExportedValueInfo? Value { get; set; }
}
 
/// <summary>
/// Generates a Go SDK using the ATS (Aspire Type System) capability-based API.
///
/// Architecture (mirrors the TypeScript generator):
///   - Public surface is interfaces; concrete impls are unexported structs.
///   - Add* / With* methods are non-blocking: they submit to the builder's
///     builderContext and return a lazy proxy immediately.
///   - Build() is the single flush point; it waits for all submitted goroutines
///     and aggregates errors via errors.Join.
///   - No init() functions; per-client wrapper / callback registries are
///     populated inside CreateBuilder via registerWrappers(c).
///   - Optional parameters use grpc-go-style functional options that apply to
///     generated options structs.
///   - Union parameters use Go 1.18+ type-set generics — no AspireUnion wrapper.
/// </summary>
internal sealed partial class AtsGoCodeGenerator : ICodeGenerator
{
    private static readonly HashSet<string> s_goKeywords = new(StringComparer.Ordinal)
    {
        "break", "case", "chan", "const", "continue", "default", "defer", "else",
        "fallthrough", "for", "func", "go", "goto", "if", "import", "interface",
        "map", "package", "range", "return", "select", "struct", "switch", "type", "var",
        "any", "true", "false", "nil", "iota",
    };
 
    // byte and rune are aliases of uint8 and int32; listing them too would be a duplicate case in a type switch.
    private static readonly string[] s_goNumericTypes =
    [
        "int", "int8", "int16", "int32", "int64",
        "uint", "uint8", "uint16", "uint32", "uint64",
        "float32",
    ];
 
    private TextWriter _writer = null!;
 
    // typeId → exported Go interface name (e.g., "Aspire.Hosting/...IResource" → "Resource")
    private readonly Dictionary<string, string> _interfaceNames = new(StringComparer.Ordinal);
 
    // typeId → unexported Go struct name (concrete types only; e.g., "redisResource")
    private readonly Dictionary<string, string> _implNames = new(StringComparer.Ordinal);
 
    // typeId → bool: is this type an interface in metadata?
    private readonly Dictionary<string, bool> _isInterfaceType = new(StringComparer.Ordinal);
 
    // typeId → bool: is this concrete type a resource builder?
    private readonly Dictionary<string, bool> _isResourceBuilder = new(StringComparer.Ordinal);
 
    private readonly Dictionary<string, string> _dtoNames = new(StringComparer.Ordinal);
    private readonly Dictionary<string, string> _enumNames = new(StringComparer.Ordinal);
 
    // capabilityId → resolved Options type name (qualified when simple name collides with a DTO or another capability's Options)
    private readonly Dictionary<string, string> _optionsTypeNames = new(StringComparer.Ordinal);
    private readonly Dictionary<string, string> _optionInterfaceNames = new(StringComparer.Ordinal);
    private readonly Dictionary<(string CapabilityId, string ParameterName), string> _optionValueNames = new();
    private string? _createBuilderOptionInterfaceName;
    private readonly Dictionary<string, string> _createBuilderOptionValueNames = new(StringComparer.Ordinal);
 
    // typeId → documentation captured for the handle type
    private readonly Dictionary<string, AtsDocumentationInfo> _handleDocumentation = new(StringComparer.Ordinal);
 
    // Short C# type name used in documentation text → generated Go type name. A short name shared by two
    // generated types maps to null so it is never turned into a doc link to the wrong type.
    private readonly Dictionary<string, string?> _docTypeNames = new(StringComparer.Ordinal);
 
    private GoDocComment _docs = null!;
 
    // typeId → accepting interface name (e.g., "AnyKubernetesNodePoolResource"). Present only for
    // concrete handle types that at least one other concrete handle type derives from.
    private readonly Dictionary<string, string> _acceptingNames = new(StringComparer.Ordinal);
 
    // typeId → marker method names (e.g., "isKubernetesNodePoolResource") that the concrete type
    // declares and implements: one per accepting interface of the type itself or of a base type.
    // The accepting interface of the type declares the same set.
    private readonly Dictionary<string, List<string>> _acceptingMarkers = new(StringComparer.Ordinal);
 
    /// <inheritdoc />
    public string Language => "Go";
 
    /// <inheritdoc />
    public Dictionary<string, string> GenerateDistributedApplication(AtsContext context)
    {
        context = FilterObsoleteSymbols(context);
 
        var files = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["go.mod"] = """
                module apphost/modules/aspire
 
                go 1.26
                """,
            ["transport.go"] = GetEmbeddedResource("transport.go"),
            ["base.go"] = GetEmbeddedResource("base.go"),
            ["aspire.go"] = GenerateAspireSdk(context)
        };
 
        foreach (var fileName in files.Keys.Where(fileName => fileName.EndsWith(".go", StringComparison.Ordinal)).ToList())
        {
            files[fileName] = FormatGoSource(fileName, files[fileName]);
        }
 
        // gofmt writes LF, and non-Go module files must use the same deterministic line endings.
        return files.ToDictionary(file => file.Key, file => file.Value.Replace("\r\n", "\n"), StringComparer.Ordinal);
    }
 
    private static AtsContext FilterObsoleteSymbols(AtsContext context)
    {
            var capabilities = context.Capabilities.Where(capability => !IsObsoleteCapability(context, capability)).ToList();
            var exportedValues = context.ExportedValues.Where(value => !AtsTypeSystemCompatibility.IsObsolete(value)).ToList();
            var requiredTypeIds = new HashSet<string>(StringComparer.Ordinal);
 
            foreach (var capability in capabilities)
            {
                CollectRequiredType(capability.TargetType, requiredTypeIds);
                CollectRequiredType(capability.ReturnType, requiredTypeIds);
                foreach (var expandedTarget in capability.ExpandedTargetTypes)
                {
                    CollectRequiredType(expandedTarget, requiredTypeIds);
                }
                foreach (var parameter in capability.Parameters)
                {
                    CollectRequiredType(parameter.Type, requiredTypeIds);
                    CollectRequiredType(parameter.CallbackReturnType, requiredTypeIds);
                    foreach (var callbackParameter in parameter.CallbackParameters ?? [])
                    {
                        CollectRequiredType(callbackParameter.Type, requiredTypeIds);
                    }
                }
            }
 
            foreach (var value in exportedValues)
            {
                CollectRequiredType(value.Type, requiredTypeIds);
            }
 
            foreach (var dto in context.DtoTypes.Where(dto => !AtsTypeSystemCompatibility.IsObsolete(dto)))
            {
                foreach (var property in dto.Properties.Where(property => !AtsTypeSystemCompatibility.IsObsolete(dto, property)))
                {
                    CollectRequiredType(property.Type, requiredTypeIds);
                    CollectRequiredType(property.CallbackReturnType, requiredTypeIds);
                    foreach (var callbackParameter in property.CallbackParameters ?? [])
                    {
                        CollectRequiredType(callbackParameter.Type, requiredTypeIds);
                    }
                }
            }
 
            var requiredDtoIds = new HashSet<string>(requiredTypeIds, StringComparer.Ordinal);
            var changed = true;
            while (changed)
            {
                changed = false;
                foreach (var dto in context.DtoTypes.Where(dto => requiredDtoIds.Contains(dto.TypeId)))
                {
                    var before = requiredTypeIds.Count;
                    var properties = AtsTypeSystemCompatibility.IsObsolete(dto)
                        ? dto.Properties
                        : dto.Properties.Where(property => !AtsTypeSystemCompatibility.IsObsolete(dto, property));
                    foreach (var property in properties)
                    {
                        CollectRequiredType(property.Type, requiredTypeIds);
                        CollectRequiredType(property.CallbackReturnType, requiredTypeIds);
                        foreach (var callbackParameter in property.CallbackParameters ?? [])
                        {
                            CollectRequiredType(callbackParameter.Type, requiredTypeIds);
                        }
                    }
                    if (requiredTypeIds.Count != before)
                    {
                        requiredDtoIds.UnionWith(requiredTypeIds);
                        changed = true;
                    }
                }
            }
 
            var dtoTypes = context.DtoTypes
                .Where(dto => !AtsTypeSystemCompatibility.IsObsolete(dto) || requiredTypeIds.Contains(dto.TypeId))
                .Select(dto => AtsTypeSystemCompatibility.IsObsolete(dto)
                    ? dto
                    : new AtsDtoTypeInfo
                    {
                        TypeId = dto.TypeId,
                        Name = dto.Name,
                        Description = dto.Description,
                        Documentation = dto.Documentation,
                        ClrType = dto.ClrType,
                        Properties = dto.Properties.Where(property => !AtsTypeSystemCompatibility.IsObsolete(dto, property)).ToList()
                    })
                .ToList();
 
            var enumTypes = context.EnumTypes
                .Where(enumType => !AtsTypeSystemCompatibility.IsObsolete(enumType) || requiredTypeIds.Contains(enumType.TypeId))
                .Select(enumType =>
                {
                    if (AtsTypeSystemCompatibility.IsObsolete(enumType))
                    {
                        return enumType;
                    }
 
                    var obsoleteValues = enumType.ValueInfos
                        .Where(value => AtsTypeSystemCompatibility.IsObsolete(enumType, value))
                        .Select(value => value.Name)
                        .ToHashSet(StringComparer.Ordinal);
                    return new AtsEnumTypeInfo
                    {
                        TypeId = enumType.TypeId,
                        Name = enumType.Name,
                        ClrType = enumType.ClrType,
                        Values = enumType.Values.Where(value => !obsoleteValues.Contains(value)).ToList(),
                        ValueInfos = enumType.ValueInfos.Where(value => !AtsTypeSystemCompatibility.IsObsolete(enumType, value)).ToList(),
                        Documentation = enumType.Documentation
                    };
                })
                .ToList();
 
        var filtered = new AtsContext
        {
            Capabilities = capabilities,
            HandleTypes = context.HandleTypes.Where(type => !AtsTypeSystemCompatibility.IsObsolete(type) || requiredTypeIds.Contains(type.AtsTypeId)).ToList(),
            DtoTypes = dtoTypes,
            EnumTypes = enumTypes,
            ExportedValues = exportedValues,
            Diagnostics = context.Diagnostics
        };
        var capabilityIds = capabilities.Select(capability => capability.CapabilityId).ToHashSet(StringComparer.Ordinal);
        foreach (var (capabilityId, method) in context.Methods)
        {
            if (capabilityIds.Contains(capabilityId))
            {
                filtered.Methods[capabilityId] = method;
            }
        }
        foreach (var (capabilityId, property) in context.Properties)
        {
            if (capabilityIds.Contains(capabilityId))
            {
                filtered.Properties[capabilityId] = property;
            }
        }
 
        return filtered;
    }
 
    private static void CollectRequiredType(AtsTypeRef? type, HashSet<string> requiredTypeIds)
    {
        if (type is null)
        {
            return;
        }
 
        requiredTypeIds.Add(type.TypeId);
        CollectRequiredType(type.ElementType, requiredTypeIds);
        CollectRequiredType(type.KeyType, requiredTypeIds);
        CollectRequiredType(type.ValueType, requiredTypeIds);
        CollectRequiredType(type.BaseType, requiredTypeIds);
        foreach (var unionType in type.UnionTypes ?? [])
        {
            CollectRequiredType(unionType, requiredTypeIds);
        }
        foreach (var implementedInterface in type.ImplementedInterfaces)
        {
            CollectRequiredType(implementedInterface, requiredTypeIds);
        }
    }
 
    private static bool IsObsoleteCapability(AtsContext context, AtsCapabilityInfo capability)
    {
        return capability.IsObsolete
            || context.Properties.TryGetValue(capability.CapabilityId, out var property)
                && AttributeDataReader.GetObsoleteData(property) is not null;
    }
 
    private static string FormatGoSource(string fileName, string source)
    {
        using var process = new Process
        {
            StartInfo = new ProcessStartInfo("gofmt")
            {
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true,
                StandardInputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
                StandardOutputEncoding = Encoding.UTF8,
                StandardErrorEncoding = Encoding.UTF8,
                ArgumentList = { "-s" }
            }
        };
 
        process.Start();
        var formattedTask = process.StandardOutput.ReadToEndAsync();
        var errorTask = process.StandardError.ReadToEndAsync();
        process.StandardInput.Write(source);
        process.StandardInput.Close();
 
        process.WaitForExit();
        var formatted = formattedTask.GetAwaiter().GetResult();
        var error = errorTask.GetAwaiter().GetResult();
 
        if (process.ExitCode != 0)
        {
            throw new InvalidOperationException($"gofmt failed for generated file '{fileName}': {error.Trim()}");
        }
 
        return formatted;
    }
 
    private static string GetEmbeddedResource(string name)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = $"Aspire.Hosting.CodeGeneration.Go.Resources.{name}";
 
        using var stream = assembly.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException($"Embedded resource '{name}' not found.");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
 
    private string GenerateAspireSdk(AtsContext context)
    {
        using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
        _writer = stringWriter;
 
        BuildNameMaps(context);
 
        var capabilitiesByDirectTarget = GroupCapabilitiesByDirectTarget(context.Capabilities);
        var capabilitiesByConcreteImpl = GroupCapabilitiesByConcreteImpl(context.Capabilities);
 
        WriteHeader();
        GenerateEnumTypes(context.EnumTypes);
        GenerateDtoTypes(context.DtoTypes);
        GenerateExportedValues(context.ExportedValues, context.DtoTypes.ToDictionary(dto => dto.TypeId, StringComparer.Ordinal));
        GenerateMarkerInterfaces(capabilitiesByDirectTarget);
        GenerateAcceptingInterfaces();
        GenerateConcreteHandleTypes(capabilitiesByConcreteImpl);
        GenerateOptionsStructs(context.Capabilities);
        GenerateCreateBuilderOptions(context);
        GenerateRegisterWrappers();
        GenerateCreateBuilder(context);
 
        return stringWriter.ToString();
    }
 
    // ── Name resolution ──────────────────────────────────────────────────────
 
    private void BuildNameMaps(AtsContext context)
    {
        _interfaceNames.Clear();
        _implNames.Clear();
        _isInterfaceType.Clear();
        _isResourceBuilder.Clear();
        _dtoNames.Clear();
        _enumNames.Clear();
        _acceptingNames.Clear();
        _acceptingMarkers.Clear();
        _reservedNames.Clear();
 
        foreach (var resourceName in new[] { "base.go", "transport.go" })
        {
            foreach (var declaration in GoDeclarationScanner.ScanRuntime(GetEmbeddedResource(resourceName)))
            {
                if (declaration.Kind is GoDeclarationKind.Type or GoDeclarationKind.Function or GoDeclarationKind.Constant)
                {
                    _reservedNames.Add(declaration.Name);
                }
            }
        }
 
        foreach (var enumType in context.EnumTypes)
        {
            _enumNames[enumType.TypeId] = ReserveName(SanitizeIdentifier(enumType.Name), takenLowercase: false);
        }
 
        foreach (var dto in context.DtoTypes)
        {
            if (dto.TypeId == AtsConstants.ReferenceExpressionTypeId)
            {
                continue; // ReferenceExpression is provided by base.go
            }
            _dtoNames[dto.TypeId] = ReserveName(SanitizeIdentifier(dto.Name), takenLowercase: false);
        }
 
        // Walk metadata to discover all handle type IDs we need to emit. We
        // include both context.HandleTypes (the directly-discovered handles) and
        // any types referenced by capabilities (target / return / parameter /
        // expanded targets / callback parameters).
        var handleTypeIds = new HashSet<string>(StringComparer.Ordinal);
        foreach (var handleType in context.HandleTypes)
        {
            if (handleType.AtsTypeId == AtsConstants.ReferenceExpressionTypeId
                || IsCancellationTokenTypeId(handleType.AtsTypeId))
            {
                continue;
            }
            handleTypeIds.Add(handleType.AtsTypeId);
            _isInterfaceType[handleType.AtsTypeId] = handleType.IsInterface;
            _isResourceBuilder[handleType.AtsTypeId] = handleType.IsResourceBuilder;
        }
 
        // Type IDs that appear in any ExpandedTargetTypes. Even if the
        // metadata classifies them as interfaces (e.g. IDistributedApplicationBuilder
        // whose ExpandedTargetTypes is just [itself]), they need a private impl
        // struct so capability methods have somewhere to live.
        var expansionTargetIds = new HashSet<string>(StringComparer.Ordinal);
        foreach (var capability in context.Capabilities)
        {
            CollectHandleTypeIds(handleTypeIds, capability.TargetType);
            CollectHandleTypeIds(handleTypeIds, capability.ReturnType);
            foreach (var parameter in capability.Parameters)
            {
                CollectHandleTypeIds(handleTypeIds, parameter.Type);
                if (parameter.IsCallback && parameter.CallbackParameters is not null)
                {
                    foreach (var cbParam in parameter.CallbackParameters)
                    {
                        CollectHandleTypeIds(handleTypeIds, cbParam.Type);
                    }
                }
            }
            foreach (var expanded in capability.ExpandedTargetTypes)
            {
                CollectHandleTypeIds(handleTypeIds, expanded);
                if (!string.IsNullOrEmpty(expanded.TypeId))
                {
                    expansionTargetIds.Add(expanded.TypeId);
                }
            }
        }
 
        // Sort to get deterministic output. Reserve interface names first (they
        // get the bare name); concrete types fall back to suffix-based
        // disambiguation when a collision occurs.
        var ordered = handleTypeIds
            .OrderBy(id => _isInterfaceType.TryGetValue(id, out var isI) && isI ? 0 : 1)
            .ThenBy(id => id, StringComparer.Ordinal)
            .ToList();
 
        foreach (var typeId in ordered)
        {
            var isInterface = _isInterfaceType.TryGetValue(typeId, out var ii) && ii;
            var needsImpl = !isInterface || expansionTargetIds.Contains(typeId);
            var rawName = ExtractTypeName(typeId);
            var stripped = StripInterfacePrefix(rawName, isInterface);
            var sanitized = SanitizeIdentifier(stripped);
 
            string interfaceName;
            if (isInterface && !needsImpl)
            {
                interfaceName = ReserveName(sanitized, takenLowercase: false);
            }
            else
            {
                // Need both an interface and an impl: try bare, then
                // assembly-prefixed, then numeric.
                if (!IsNameTaken(sanitized))
                {
                    interfaceName = ReserveName(sanitized, takenLowercase: true);
                }
                else
                {
                    var assemblyPrefix = SanitizeIdentifier(typeId.Split('/')[0]);
                    var prefixed = $"{assemblyPrefix}{sanitized}";
                    interfaceName = !IsNameTaken(prefixed)
                        ? ReserveName(prefixed, takenLowercase: true)
                        : ReserveName(sanitized, takenLowercase: true);
                }
            }
 
            _interfaceNames[typeId] = interfaceName;
            if (needsImpl)
            {
                _implNames[typeId] = ToCamelCase(interfaceName);
            }
        }
 
        // Resolve Options struct names for all capabilities that have optional params.
        // Must run after DTOs, enums, and handle types are all reserved so collision
        // detection via IsNameTaken() is accurate.
        _optionsTypeNames.Clear();
        var optionsNamesInUse = new HashSet<string>(StringComparer.Ordinal);
        // Tracks the first capability assigned to each Options name so subsequent
        // capabilities can compare field signatures before sharing.
        var optionsNameFirstCapability = new Dictionary<string, AtsCapabilityInfo>(StringComparer.Ordinal);
        foreach (var capability in context.Capabilities)
        {
            var targetParamName = capability.TargetParameterName ?? "builder";
            var reservationOptionals = capability.Parameters
                .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
                .Where(p => IsCancellationToken(p) || p.IsOptional)
                .ToList();
            if (reservationOptionals.Count == 0)
            {
                continue;
            }
 
            var simpleName = $"{ToPascalCase(capability.MethodName)}Options";
            string resolvedName;
 
            if (!IsNameTaken(simpleName) && !optionsNamesInUse.Contains(simpleName))
            {
                // Name is free everywhere — use simple name and mark it used.
                resolvedName = simpleName;
                optionsNamesInUse.Add(simpleName);
                optionsNameFirstCapability[simpleName] = capability;
            }
            else if (optionsNamesInUse.Contains(simpleName) && !IsNameTaken(simpleName))
            {
                // Already claimed by a previous Options struct (same method name on a different
                // type). Reuse ONLY if the optional-param field signatures are compatible.
                // Incompatible signatures (e.g. RunAsEmulator on Storage vs EventHubs both have
                // configureContainer but with different emulator callback types) must get
                // separate qualified structs — sharing would produce a type mismatch in the
                // generated callback shim.
                if (optionsNameFirstCapability.TryGetValue(simpleName, out var firstCap)
                    && AreOptionsCompatible(capability, firstCap))
                {
                    resolvedName = simpleName; // same field shapes — safe to share
                }
                else
                {
                    // Incompatible callback types — qualify with target interface name.
                    var qualifiedName = GetOptionsQualifiedName(capability, simpleName);
                    resolvedName = qualifiedName;
                    optionsNamesInUse.Add(resolvedName);
                    optionsNameFirstCapability[resolvedName] = capability;
                }
            }
            else
            {
                // Collision with a DTO, enum, or handle type. Qualify with the target interface name.
                var qualifiedName = GetOptionsQualifiedName(capability, simpleName);
                resolvedName = qualifiedName;
                optionsNamesInUse.Add(resolvedName);
                optionsNameFirstCapability[resolvedName] = capability;
            }
 
            _optionsTypeNames[capability.CapabilityId] = resolvedName;
        }
 
        // Generated option interfaces and values are package-level declarations too. Reserve all names
        // once, before emission, so independently derived names cannot collide with each other or with
        // exported values and enum constants.
        _reservedNames.UnionWith(_optionsTypeNames.Values);
        _reservedNames.UnionWith(BuildExportedValueTree(context.ExportedValues).Children.Keys);
        foreach (var enumType in context.EnumTypes)
        {
            if (enumType.ClrType is null)
            {
                continue;
            }
 
            foreach (var member in enumType.Values)
            {
                _reservedNames.Add(GetEnumMemberConstantName(_enumNames[enumType.TypeId], member));
            }
        }
 
        _optionInterfaceNames.Clear();
        _optionValueNames.Clear();
        var interfaceNamesByOptionsType = new Dictionary<string, string>(StringComparer.Ordinal);
        var valueNamesByOptionsField = new Dictionary<(string OptionsType, string FieldName), string>();
        foreach (var capability in context.Capabilities)
        {
            if (!_optionsTypeNames.TryGetValue(capability.CapabilityId, out var optionsType))
            {
                continue;
            }
 
            if (!interfaceNamesByOptionsType.TryGetValue(optionsType, out var optionInterface))
            {
                var candidate = optionsType.EndsWith("Options", StringComparison.Ordinal)
                    ? optionsType[..^1]
                    : optionsType + "Option";
                optionInterface = ReserveName(candidate, takenLowercase: false);
                interfaceNamesByOptionsType[optionsType] = optionInterface;
            }
            _optionInterfaceNames[capability.CapabilityId] = optionInterface;
 
            var targetParamName = capability.TargetParameterName ?? "builder";
            foreach (var parameter in capability.Parameters.Where(parameter =>
                !string.Equals(parameter.Name, targetParamName, StringComparison.Ordinal)
                && (IsCancellationToken(parameter) || parameter.IsOptional)))
            {
                var fieldName = ToPascalCase(parameter.Name);
                var sharedKey = (optionsType, fieldName);
                if (!valueNamesByOptionsField.TryGetValue(sharedKey, out var optionValue))
                {
                    var candidate = GetOptionValueNameCandidate(optionsType, fieldName);
                    if (candidate == optionsType || candidate == optionInterface)
                    {
                        candidate += "Value";
                    }
                    optionValue = ReserveName(candidate, takenLowercase: true);
                    valueNamesByOptionsField[sharedKey] = optionValue;
                }
                _optionValueNames[(capability.CapabilityId, parameter.Name)] = optionValue;
            }
        }
 
        _createBuilderOptionInterfaceName = null;
        _createBuilderOptionValueNames.Clear();
        if (context.DtoTypes.FirstOrDefault(dto => dto.Name == "CreateBuilderOptions") is { } createBuilderOptions)
        {
            _createBuilderOptionInterfaceName = ReserveName("CreateBuilderOption", takenLowercase: false);
            foreach (var property in createBuilderOptions.Properties)
            {
                var candidate = $"CreateBuilder{ToPascalCase(property.Name)}";
                _createBuilderOptionValueNames[property.Name] = ReserveName(candidate, takenLowercase: false);
            }
        }
 
        BuildAcceptingInterfaces(context);
 
        // Every doc render dereferences _docs, and the renderer resolves type names through the maps
        // built above. Building it here means any caller that builds the name maps also gets a usable
        // renderer, instead of failing with a NullReferenceException the first time a doc is rendered.
        BuildDocumentationMaps(context);
    }
 
    // Go has no return-type covariance, so the interface of a derived handle type never satisfies the
    // interface of its base type: each interface re-declares the fluent methods with its own return
    // type. A parameter typed as a base (WithNodePool takes KubernetesNodePoolResource) would therefore
    // reject the derived builder AddNodePool returns (AksNodePoolResource).
    //
    // Every concrete handle type that another concrete handle type derives from gets an accepting
    // interface, Any<Type>, used only where a caller supplies a value. It embeds handleReference and
    // declares unexported marker methods: the marker of the type itself and of each base type that has
    // an accepting interface too. The type and each of its derived types implement all of them. That
    // keeps the check at compile time (a builder outside the family lacks the marker) without changing
    // the fluent interface of any type. The base-type markers are what let an Any<Type> value be
    // passed where the Any<Base> of one of its base types is expected, because Go converts an
    // interface value to another interface only when its method set has every method of the target.
    //
    // A type that nothing derives from gets no accepting interface: its own interface already accepts
    // exactly the same values. The cost is that the set of accepting interfaces, and which parameters
    // use them, follows the handle types of the integrations an AppHost includes. The generated
    // header (see GenerateAcceptingInterfaces) tells users so.
    private void BuildAcceptingInterfaces(AtsContext context)
    {
        var ancestorIdsByType = new Dictionary<string, List<string>>(StringComparer.Ordinal);
        var baseTypeIds = new HashSet<string>(StringComparer.Ordinal);
        foreach (var handleType in context.HandleTypes)
        {
            if (!_implNames.ContainsKey(handleType.AtsTypeId))
            {
                continue;
            }
 
            // BaseTypeHierarchy lists every base class, including ones that are not emitted as handle
            // types (for example the abstract Resource class); those have no Go type to accept.
            var ancestorIds = handleType.BaseTypeHierarchy
                .Select(baseType => baseType.TypeId)
                .Where(_implNames.ContainsKey)
                .ToList();
            ancestorIdsByType[handleType.AtsTypeId] = ancestorIds;
            baseTypeIds.UnionWith(ancestorIds);
        }
 
        if (baseTypeIds.Count == 0)
        {
            return;
        }
 
        foreach (var typeId in baseTypeIds.OrderBy(id => _interfaceNames[id], StringComparer.Ordinal))
        {
            _acceptingNames[typeId] = ReserveName($"Any{_interfaceNames[typeId]}", takenLowercase: false);
        }
 
        // Every type with an implementation takes part, including a base type that capabilities
        // reference but HandleTypes does not list. It has no known ancestors, yet it still has an
        // accepting interface and must implement that interface's own marker.
        foreach (var typeId in _implNames.Keys)
        {
            var markers = (ancestorIdsByType.GetValueOrDefault(typeId) ?? [])
                .Append(typeId)
                .Where(_acceptingNames.ContainsKey)
                .Select(GetAcceptingMarkerName)
                .Order(StringComparer.Ordinal)
                .ToList();
            if (markers.Count > 0)
            {
                _acceptingMarkers[typeId] = markers;
            }
        }
    }
 
    private string GetAcceptingMarkerName(string typeId) => $"is{_interfaceNames[typeId]}";
 
    private static void CollectHandleTypeIds(HashSet<string> set, AtsTypeRef? typeRef)
    {
        if (typeRef is null)
        {
            return;
        }
        if (typeRef.TypeId == AtsConstants.ReferenceExpressionTypeId
            || IsCancellationTokenTypeId(typeRef.TypeId))
        {
            return;
        }
        if (typeRef.Category == AtsTypeCategory.Handle)
        {
            set.Add(typeRef.TypeId);
        }
        if (typeRef.ElementType is not null)
        {
            CollectHandleTypeIds(set, typeRef.ElementType);
        }
        if (typeRef.KeyType is not null)
        {
            CollectHandleTypeIds(set, typeRef.KeyType);
        }
        if (typeRef.ValueType is not null)
        {
            CollectHandleTypeIds(set, typeRef.ValueType);
        }
        if (typeRef.UnionTypes is not null)
        {
            foreach (var u in typeRef.UnionTypes)
            {
                CollectHandleTypeIds(set, u);
            }
        }
    }
 
    private static string StripInterfacePrefix(string name, bool isInterface)
    {
        if (!isInterface || name.Length < 2 || name[0] != 'I')
        {
            return name;
        }
        if (!char.IsUpper(name[1]))
        {
            return name;
        }
        return name[1..];
    }
 
    private readonly HashSet<string> _reservedNames = new(StringComparer.Ordinal);
 
    private bool IsNameTaken(string name) => _reservedNames.Contains(name);
 
    private string ReserveName(string candidate, bool takenLowercase)
    {
        var name = candidate;
        var counter = 2;
        while (_reservedNames.Contains(name)
            || (takenLowercase && _reservedNames.Contains(ToCamelCase(name))))
        {
            name = $"{candidate}{counter}";
            counter++;
        }
        _reservedNames.Add(name);
        if (takenLowercase)
        {
            _reservedNames.Add(ToCamelCase(name));
        }
        return name;
    }
 
    // ── Capability grouping ───────────────────────────────────────────────────
 
    /// <summary>
    /// Groups capabilities by their direct target type ID (no expansion).
    /// Used to emit interface-metadata declarations: methods listed here go on
    /// the marker interface for the target.
    /// </summary>
    private static Dictionary<string, List<AtsCapabilityInfo>> GroupCapabilitiesByDirectTarget(
        IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        var result = new Dictionary<string, List<AtsCapabilityInfo>>(StringComparer.Ordinal);
        foreach (var capability in capabilities)
        {
            if (string.IsNullOrEmpty(capability.TargetTypeId))
            {
                continue;
            }
            if (!result.TryGetValue(capability.TargetTypeId, out var list))
            {
                list = new List<AtsCapabilityInfo>();
                result[capability.TargetTypeId] = list;
            }
            list.Add(capability);
        }
        return result;
    }
 
    /// <summary>
    /// Groups capabilities by every concrete type they end up applying to.
    /// Uses ExpandedTargetTypes when the target is an interface; falls back to
    /// TargetType otherwise. Used to emit method bodies on each concrete impl.
    /// </summary>
    private Dictionary<string, List<AtsCapabilityInfo>> GroupCapabilitiesByConcreteImpl(
        IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        var result = new Dictionary<string, List<AtsCapabilityInfo>>(StringComparer.Ordinal);
 
        foreach (var capability in capabilities)
        {
            if (string.IsNullOrEmpty(capability.TargetTypeId))
            {
                continue;
            }
 
            var targetTypes = capability.ExpandedTargetTypes.Count > 0
                ? capability.ExpandedTargetTypes
                : capability.TargetType is not null
                    ? [capability.TargetType]
                    : (IReadOnlyList<AtsTypeRef>)Array.Empty<AtsTypeRef>();
 
            foreach (var targetType in targetTypes)
            {
                if (string.IsNullOrEmpty(targetType.TypeId))
                {
                    continue;
                }
                // Emit on any type that has an impl struct — concrete types
                // and self-referencing interfaces (e.g. IDistributedApplicationBuilder).
                if (!_implNames.ContainsKey(targetType.TypeId))
                {
                    continue;
                }
                if (!result.TryGetValue(targetType.TypeId, out var list))
                {
                    list = new List<AtsCapabilityInfo>();
                    result[targetType.TypeId] = list;
                }
                list.Add(capability);
            }
        }
        return result;
    }
 
    // ── Header / imports ─────────────────────────────────────────────────────
 
    private void WriteHeader()
    {
        WriteLine("""
            // aspire.go - Capability-based Aspire SDK
            // This SDK uses the ATS (Aspire Type System) capability API.
            // Capabilities are endpoints like 'Aspire.Hosting/createBuilder'.
            //
            // GENERATED CODE - DO NOT EDIT
 
            package aspire
 
            import (
                "context"
                "encoding/json"
                "fmt"
                "os"
                "strings"
                "time"
            )
 
            // Compile-time references to keep imports used in minimal SDKs.
            var _ = context.Background
            var _ = json.Number("")
            var _ = fmt.Errorf
            var _ = os.Getenv
            var _ = strings.EqualFold
            var _ = time.Second
            """);
        WriteLine();
    }
 
    // ── Documentation ────────────────────────────────────────────────────────
 
    private void BuildDocumentationMaps(AtsContext context)
    {
        _handleDocumentation.Clear();
        _docTypeNames.Clear();
 
        foreach (var handleType in context.HandleTypes)
        {
            if (handleType.Documentation is { } documentation)
            {
                _handleDocumentation[handleType.AtsTypeId] = documentation;
            }
        }
 
        foreach (var (typeId, goName) in _interfaceNames)
        {
            AddDocTypeName(ExtractTypeName(typeId), goName);
        }
 
        foreach (var dto in context.DtoTypes)
        {
            if (_dtoNames.TryGetValue(dto.TypeId, out var dtoName))
            {
                AddDocTypeName(dto.Name, dtoName);
            }
        }
 
        foreach (var enumType in context.EnumTypes)
        {
            if (_enumNames.TryGetValue(enumType.TypeId, out var enumName))
            {
                AddDocTypeName(enumType.Name, enumName);
            }
        }
 
        _docs = new GoDocComment(name => _docTypeNames.GetValueOrDefault(name), ToPascalCase);
    }
 
    private void AddDocTypeName(string shortName, string goName)
    {
        if (!_docTypeNames.TryAdd(shortName, goName) && _docTypeNames[shortName] != goName)
        {
            _docTypeNames[shortName] = null;
        }
    }
 
    private void WriteDocLines(IReadOnlyList<string> lines, string indent = "")
    {
        foreach (var line in lines)
        {
            WriteLine($"{indent}{line}");
        }
    }
 
    private GoDocContent CreateCapabilityDoc(AtsCapabilityInfo capability)
    {
        var targetParamName = capability.TargetParameterName ?? "builder";
        var (requiredParams, optionalParams) = SplitParameters(capability.Parameters
            .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
            .ToList());
 
        // A setter's only parameter carries a copy of the property documentation ("Gets or sets the port."),
        // which is already the summary and reads wrongly as a description of the argument.
        var isSetter = capability.CapabilityKind == AtsCapabilityKind.PropertySetter;
        var parameters = requiredParams
            .Select(p => new GoDocParameter(p.Name, GetLocalIdentifier(p.Name), isSetter ? null : p.Documentation?.Summary))
            .ToList();
 
        if (optionalParams.Count > 0)
        {
            parameters.Add(new GoDocParameter(
                "options",
                "options",
                $"Optional parameters; see {RenderTypeLink(GetOptionInterfaceName(capability))}."));
        }
 
        var returnsValue = capability.ReturnType is not null && capability.ReturnType.TypeId != AtsConstants.Void;
 
        return new GoDocContent
        {
            GoName = ToPascalCase(capability.MethodName),
            Subject = capability.CapabilityKind switch
            {
                AtsCapabilityKind.PropertyGetter => GoDocSubject.Getter,
                AtsCapabilityKind.PropertySetter => GoDocSubject.Setter,
                _ => GoDocSubject.Method,
            },
            Summary = capability.Documentation?.Summary,
            FallbackSummary = capability.Description,
            Remarks = capability.Documentation?.Remarks,
            Parameters = parameters,
            Returns = returnsValue ? capability.Documentation?.Returns : null,
            Notes = GetUnionNotes(capability),
            IsExperimental = AtsTypeSystemCompatibility.IsExperimental(capability),
            IsObsolete = capability.IsObsolete,
            ObsoleteMessage = capability.ObsoleteMessage,
        };
    }
 
    // A doc link only resolves to a declared package-level identifier; anything else (such as the
    // map[string]any fallback for an unknown DTO) is written as plain text.
    private static string RenderTypeLink(string goType)
    {
        return goType.All(c => char.IsLetterOrDigit(c) || c == '_') ? $"[{goType}]" : goType;
    }
 
    // ── Enums ────────────────────────────────────────────────────────────────
 
    private void GenerateEnumTypes(IReadOnlyList<AtsEnumTypeInfo> enumTypes)
    {
        if (enumTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Enums");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var enumType in enumTypes)
        {
            if (enumType.ClrType is null)
            {
                continue;
            }
 
            var enumName = _enumNames[enumType.TypeId];
            WriteDocLines(_docs.Render(new GoDocContent
            {
                GoName = enumName,
                Subject = GoDocSubject.Type,
                Summary = enumType.Documentation?.Summary,
                FallbackSummary = $"{enumName} represents {enumType.Name}.",
                Remarks = enumType.Documentation?.Remarks,
            }));
            WriteLine($"type {enumName} string");
            WriteLine();
            WriteLine("const (");
            var valueDocumentation = enumType.ValueInfos.ToDictionary(value => value.Name, value => value.Documentation, StringComparer.Ordinal);
            foreach (var member in enumType.Values)
            {
                var memberName = GetEnumMemberConstantName(enumName, member);
                if (valueDocumentation.TryGetValue(member, out var memberDocumentation))
                {
                    WriteDocLines(_docs.Render(new GoDocContent
                    {
                        GoName = memberName,
                        Subject = GoDocSubject.Field,
                        Summary = memberDocumentation?.Summary,
                        Remarks = memberDocumentation?.Remarks,
                    }), "\t");
                }
 
                WriteLine($"\t{memberName} {enumName} = {GoStringLiteral(member)}");
            }
            WriteLine(")");
            WriteLine();
        }
    }
 
    private static string GetEnumMemberConstantName(string enumName, string member) => $"{enumName}{ToPascalCase(member)}";
 
    // ── DTOs ─────────────────────────────────────────────────────────────────
 
    private void GenerateDtoTypes(IReadOnlyList<AtsDtoTypeInfo> dtoTypes)
    {
        if (dtoTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// DTOs");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var dto in dtoTypes)
        {
            if (dto.TypeId == AtsConstants.ReferenceExpressionTypeId)
            {
                continue;
            }
 
            var dtoName = _dtoNames[dto.TypeId];
            WriteDocLines(_docs.Render(new GoDocContent
            {
                GoName = dtoName,
                Subject = GoDocSubject.Type,
                Summary = dto.Documentation?.Summary ?? dto.Description,
                FallbackSummary = $"{dtoName} represents {dto.Name}.",
                Remarks = dto.Documentation?.Remarks,
            }));
            WriteLine($"type {dtoName} struct {{");
            if (dto.Properties.Count == 0)
            {
                WriteLine("}");
                WriteLine();
                EmitEmptyToMap(dtoName);
                continue;
            }
 
            foreach (var property in dto.Properties)
            {
                var propertyName = ToPascalCase(property.Name);
                // Callback-typed DTO properties carry the same metadata as method parameters, so
                // render the strongly-typed func signature (e.g. func(ctx InputsDialogValidationContext))
                // instead of the weak func(...any) any fallback.
                var propertyType = property.IsCallback
                    ? RenderCallbackType(DtoPropertyToParameterInfo(property))
                    : MapDtoPropertyTypeToGo(property.Type, property.IsOptional);
                var jsonTag = $"`json:{GoStringLiteral(JsonNamingPolicy.CamelCase.ConvertName(property.Name) + ",omitempty")}`";
                WriteDocLines(_docs.Render(new GoDocContent
                {
                    GoName = propertyName,
                    Subject = GoDocSubject.Field,
                    Summary = property.Documentation?.Summary ?? property.Description,
                    Remarks = property.Documentation?.Remarks,
                }), "\t");
                WriteLine($"\t{propertyName} {propertyType} {jsonTag}");
            }
            WriteLine("}");
            WriteLine();
 
            WriteLine($"// ToMap converts the DTO to a map for JSON serialization.");
            WriteLine($"func (d *{dtoName}) ToMap() map[string]any {{");
            WriteLine("\tm := map[string]any{}");
            foreach (var property in dto.Properties)
            {
                var propertyName = ToPascalCase(property.Name);
                if (property.IsCallback)
                {
                    EmitDtoCallbackToMap(property, propertyName);
                    continue;
                }
                var propertyType = MapDtoPropertyTypeToGo(property.Type, property.IsOptional);
                if (IsNilableGoType(propertyType))
                {
                    WriteLine($"\tif d.{propertyName} != nil {{ m[{GoStringLiteral(JsonNamingPolicy.CamelCase.ConvertName(property.Name))}] = serializeValue(d.{propertyName}) }}");
                }
                else
                {
                    WriteLine($"\tm[{GoStringLiteral(JsonNamingPolicy.CamelCase.ConvertName(property.Name))}] = serializeValue(d.{propertyName})");
                }
            }
            WriteLine("\treturn m");
            WriteLine("}");
            WriteLine();
        }
    }
 
    private void GenerateExportedValues(
        IReadOnlyList<AtsExportedValueInfo> exportedValues,
        IReadOnlyDictionary<string, AtsDtoTypeInfo> dtoTypesById)
    {
        if (exportedValues.Count == 0)
        {
            return;
        }
 
        var root = BuildExportedValueTree(exportedValues);
 
        WriteLine("// ============================================================================");
        WriteLine("// Exported Values");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var (name, node) in root.Children.OrderBy(pair => pair.Key, StringComparer.Ordinal))
        {
            WriteLine($"// {name} groups related exported values.");
            WriteLine($"var {name} = {RenderGoExportedValueType(node, indentLevel: 0, includeDocs: true)}{RenderGoExportedValueValue(node, dtoTypesById, indentLevel: 0)}");
            WriteLine();
        }
    }
 
    // The type literal is written twice for nested groups: once as the variable's type and once more in front
    // of each nested value. Only the variable's type carries documentation, because that is the declaration
    // go doc and gopls resolve a field to.
    private string RenderGoExportedValueType(GoExportedValueTreeNode node, int indentLevel, bool includeDocs)
    {
        var indent = new string('\t', indentLevel);
        var nestedIndent = new string('\t', indentLevel + 1);
        var sb = new StringBuilder();
        sb.AppendLine("struct {");
 
        foreach (var (name, child) in node.Children.OrderBy(pair => pair.Key, StringComparer.Ordinal))
        {
            if (child.Value is { } valueInfo)
            {
                if (includeDocs)
                {
                    foreach (var line in _docs.Render(new GoDocContent
                    {
                        GoName = name,
                        Subject = GoDocSubject.Field,
                        Summary = valueInfo.Documentation?.Summary ?? valueInfo.Description,
                        Remarks = valueInfo.Documentation?.Remarks,
                    }))
                    {
                        sb.Append(nestedIndent);
                        sb.AppendLine(line);
                    }
                }
 
                sb.Append(nestedIndent);
                sb.Append(name);
                sb.Append(' ');
                sb.AppendLine(MapTypeRefToGo(valueInfo.Type, isOptional: false));
            }
            else
            {
                sb.Append(nestedIndent);
                sb.Append(name);
                sb.Append(' ');
                sb.Append(RenderGoExportedValueType(child, indentLevel + 1, includeDocs));
                sb.AppendLine();
            }
        }
 
        sb.Append(indent);
        sb.Append('}');
        return sb.ToString();
    }
 
    private string RenderGoExportedValueValue(
        GoExportedValueTreeNode node,
        IReadOnlyDictionary<string, AtsDtoTypeInfo> dtoTypesById,
        int indentLevel)
    {
        var indent = new string('\t', indentLevel);
        var nestedIndent = new string('\t', indentLevel + 1);
        var sb = new StringBuilder();
        sb.AppendLine("{");
 
        foreach (var (name, child) in node.Children.OrderBy(pair => pair.Key, StringComparer.Ordinal))
        {
            sb.Append(nestedIndent);
            sb.Append(name);
            sb.Append(": ");
            if (child.Value is { } valueInfo)
            {
                sb.Append(RenderGoExportedValue(valueInfo.Value, valueInfo.Type, dtoTypesById));
            }
            else
            {
                sb.Append(RenderGoExportedValueType(child, indentLevel + 1, includeDocs: false));
                sb.Append(RenderGoExportedValueValue(child, dtoTypesById, indentLevel + 1));
            }
 
            sb.AppendLine(",");
        }
 
        sb.Append(indent);
        sb.Append('}');
        return sb.ToString();
    }
 
    private string RenderGoExportedValue(
        JsonNode? value,
        AtsTypeRef typeRef,
        IReadOnlyDictionary<string, AtsDtoTypeInfo> dtoTypesById)
    {
        if (value is null)
        {
            return "nil";
        }
 
        var renderedValue = typeRef.Category switch
        {
            AtsTypeCategory.Primitive => RenderGoJsonValue(value),
            AtsTypeCategory.Enum => $"{MapEnumType(typeRef.TypeId)}({RenderGoJsonValue(value)})",
            AtsTypeCategory.Dto when value is JsonObject obj && dtoTypesById.TryGetValue(typeRef.TypeId, out var dtoInfo)
                => RenderGoDtoValue(obj, dtoInfo, dtoTypesById),
            AtsTypeCategory.Array or AtsTypeCategory.List when value is JsonArray arr
                => $"[]{MapTypeRefToGo(typeRef.ElementType, isOptional: false)}{{{string.Join(", ", arr.Select(item => RenderGoExportedValue(item, typeRef.ElementType!, dtoTypesById)))}}}",
            AtsTypeCategory.Dict when value is JsonObject obj
                => $"map[{MapTypeRefToGo(typeRef.KeyType, isOptional: false)}]{MapTypeRefToGo(typeRef.ValueType, isOptional: false)}{{{string.Join(", ", obj.Select(pair => $"{GoStringLiteral(pair.Key)}: {RenderGoExportedValue(pair.Value, typeRef.ValueType!, dtoTypesById)}"))}}}",
            _ => RenderGoJsonValue(value)
        };
 
        if (!ShouldApplyNullableType(typeRef))
        {
            return renderedValue;
        }
 
        var valueType = typeRef.Category == AtsTypeCategory.Primitive
            ? MapPrimitiveType(typeRef)
            : MapEnumType(typeRef.TypeId);
        return $"func(value {valueType}) *{valueType} {{ return &value }}({renderedValue})";
    }
 
    // The relaxed JSON writer spells a character outside the Basic Multilingual Plane as a UTF-16 surrogate pair
    // ("\uD83D\uDE00"), and Go rejects surrogate halves in a string literal. Strings are therefore quoted by
    // GoStringLiteral; numbers and booleans are the same text in JSON and Go.
    private static string RenderGoJsonValue(JsonNode value) =>
        value is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var text)
            ? GoStringLiteral(text)
            : value.ToRelaxedJsonString();
 
    private string RenderGoDtoValue(
        JsonObject value,
        AtsDtoTypeInfo dtoInfo,
        IReadOnlyDictionary<string, AtsDtoTypeInfo> dtoTypesById)
    {
        var sb = new StringBuilder();
        sb.Append('&');
        sb.Append(_dtoNames[dtoInfo.TypeId]);
        sb.Append('{');
 
        var members = new List<string>();
        foreach (var property in dtoInfo.Properties)
        {
            if (!value.TryGetPropertyValue(property.Name, out var propertyValue))
            {
                continue;
            }
 
            members.Add($"{ToPascalCase(property.Name)}: {RenderGoExportedValue(propertyValue, property.Type, dtoTypesById)}");
        }
 
        sb.Append(string.Join(", ", members));
        sb.Append('}');
        return sb.ToString();
    }
 
    private static GoExportedValueTreeNode BuildExportedValueTree(IReadOnlyList<AtsExportedValueInfo> exportedValues)
    {
        var root = new GoExportedValueTreeNode();
        foreach (var exportedValue in exportedValues)
        {
            var current = root;
            foreach (var segment in exportedValue.PathSegments.Select(SanitizeIdentifier))
            {
                if (!current.Children.TryGetValue(segment, out var child))
                {
                    child = new GoExportedValueTreeNode();
                    current.Children[segment] = child;
                }
 
                current = child;
            }
 
            current.Value = exportedValue;
        }
 
        return root;
    }
 
    private void EmitEmptyToMap(string dtoName)
    {
        WriteLine($"// ToMap converts the DTO to a map for JSON serialization.");
        WriteLine($"func (d *{dtoName}) ToMap() map[string]any {{ return map[string]any{{}} }}");
        WriteLine();
    }
 
    // ── Marker interfaces (interface-metadata types) ─────────────────────────
    //
    // Interface-metadata types are emitted as empty marker interfaces. They
    // don't declare methods because Go has no return-type covariance: if we
    // declared `WithEnvironment() ResourceWithEnvironment` here, no concrete
    // impl could satisfy it while also providing a fluent return of its own
    // concrete type. Concrete impls list every method inline (see
    // GenerateConcreteHandleTypes), and capability returns of an interface
    // type fall back to the marker interface, which any concrete satisfies
    // trivially.
 
    // The interface-metadata types that get only a marker interface because no concrete impl is
    // generated for them, ordered by Go name.
    private List<KeyValuePair<string, string>> GetMarkerInterfaces()
    {
        return _interfaceNames
            .Where(kv => _isInterfaceType.TryGetValue(kv.Key, out var isI) && isI && !_implNames.ContainsKey(kv.Key))
            .OrderBy(kv => kv.Value, StringComparer.Ordinal)
            .ToList();
    }
 
    private void GenerateMarkerInterfaces(Dictionary<string, List<AtsCapabilityInfo>> capabilitiesByDirectTarget)
    {
        var interfaceTypeIds = GetMarkerInterfaces();
 
        if (interfaceTypeIds.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Marker interfaces (from interface-metadata types)");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var (typeId, name) in interfaceTypeIds)
        {
            var hasMethods = capabilitiesByDirectTarget.ContainsKey(typeId);
            var documentation = _handleDocumentation.GetValueOrDefault(typeId);
            WriteDocLines(_docs.Render(new GoDocContent
            {
                GoName = name,
                Subject = GoDocSubject.Type,
                Summary = documentation?.Summary,
                FallbackSummary = $"{name} marks types implementing {ExtractTypeName(typeId)}.",
                Remarks = documentation?.Remarks,
                Notes = hasMethods ? ["Methods are emitted on concrete impls; this interface is a marker for type assertions."] : [],
            }));
            WriteLine($"type {name} interface {{");
            WriteLine("\thandleReference");
            WriteLine("}");
            WriteLine();
        }
    }
 
    // ── Accepting interfaces (concrete handle types with derived types) ──────
    //
    // See BuildAcceptingInterfaces for why these exist. They are declared before the handle
    // wrappers; the marker methods that satisfy them are declared on each handle's interface and
    // implemented on its impl (see GenerateConcreteHandleTypes).
 
    private void GenerateAcceptingInterfaces()
    {
        if (_acceptingNames.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Accepting interfaces (handle types with derived types)");
        WriteLine("// ============================================================================");
        WriteLine("// Go has no return-type covariance, so the interface of a derived handle type does not");
        WriteLine("// satisfy the interface of its base type. Each Any<Type> below accepts <Type> or a handle");
        WriteLine("// of any type derived from it, and is used wherever a method, options struct, or DTO takes");
        WriteLine("// a <Type> as input. The unexported marker methods restrict it to that family of types.");
        WriteLine("//");
        WriteLine("// The set of Any<Type> interfaces depends on the integrations of the AppHost this SDK is");
        WriteLine("// generated for: an Any<Type> exists only while some handle type in the SDK derives from <Type>.");
        WriteLine("// Adding or removing an integration can add or remove an Any<Type> and change a method parameter");
        WriteLine("// from <Type> to Any<Type>, so code that names an Any<Type> or such a parameter's type depends");
        WriteLine("// on the integrations the AppHost includes.");
        WriteLine();
 
        foreach (var (typeId, name) in _acceptingNames.OrderBy(kv => kv.Value, StringComparer.Ordinal))
        {
            var interfaceName = _interfaceNames[typeId];
            WriteLine($"// {name} accepts {interfaceName} or a handle of any type derived from it.");
            WriteLine($"type {name} interface {{");
            WriteLine("\thandleReference");
 
            // The markers of the base types are declared too: Go converts an interface value to another
            // interface only when its method set has every method of the target, so without them an
            // Any<Type> value could not be passed where the Any<Base> of a base type is expected. That
            // admits nothing the base type rejects, since every handle of this family belongs to the
            // family of each base type. BaseTypeHierarchy lists the whole base chain, so each derived
            // type implements every marker declared here.
            foreach (var marker in _acceptingMarkers[typeId])
            {
                WriteLine($"\t{marker}()");
            }
 
            WriteLine("}");
            WriteLine();
        }
    }
 
    // ── Concrete handle types: interface + impl + methods ─────────────────────
 
    private void GenerateConcreteHandleTypes(Dictionary<string, List<AtsCapabilityInfo>> capabilitiesByConcreteImpl)
    {
        var concreteTypeIds = _implNames.Keys.OrderBy(id => _interfaceNames[id], StringComparer.Ordinal).ToList();
        if (concreteTypeIds.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Handle wrappers");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var typeId in concreteTypeIds)
        {
            var interfaceName = _interfaceNames[typeId];
            var implName = _implNames[typeId];
            var capabilities = capabilitiesByConcreteImpl.TryGetValue(typeId, out var caps)
                ? caps.Where(c => !IsSpecialCasedCapability(c.CapabilityId))
                      .OrderBy(c => c.MethodName, StringComparer.Ordinal)
                      .ToList()
                : new List<AtsCapabilityInfo>();
 
            var listDictGetters = CollectListDictGetters(capabilities);
 
            EmitConcreteInterface(typeId, interfaceName, capabilities, listDictGetters);
            EmitConcreteStruct(interfaceName, implName, listDictGetters);
            EmitConcreteConstructor(interfaceName, implName);
            EmitAcceptingMarkerMethods(typeId, implName);
 
            foreach (var capability in capabilities)
            {
                EmitCapabilityMethod(typeId, interfaceName, implName, capability, listDictGetters);
            }
 
            if (string.Equals(typeId, InteractionInputCollectionTypeId, StringComparison.Ordinal))
            {
                EmitInteractionInputCollectionAccessors(implName);
            }
        }
    }
 
    private static List<(string Field, string MethodName, AtsTypeRef ReturnType, string CapabilityId, string? Description)>
        CollectListDictGetters(IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        var result = new List<(string, string, AtsTypeRef, string, string?)>();
        foreach (var capability in capabilities)
        {
            var targetParamName = capability.TargetParameterName ?? "builder";
            var nonTargetParams = capability.Parameters
                .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
                .ToList();
            if (nonTargetParams.Count != 0)
            {
                continue;
            }
            if (capability.ReturnType is null)
            {
                continue;
            }
            if (capability.ReturnType.Category != AtsTypeCategory.List
                && capability.ReturnType.Category != AtsTypeCategory.Dict)
            {
                continue;
            }
            if (capability.ReturnType.IsReadOnly)
            {
                continue;
            }
            var methodName = ToPascalCase(capability.MethodName);
            var fieldName = ToCamelCase(methodName);
            result.Add((fieldName, methodName, capability.ReturnType, capability.CapabilityId, capability.Description));
        }
        return result;
    }
 
    private void EmitConcreteInterface(
        string typeId,
        string interfaceName,
        IReadOnlyList<AtsCapabilityInfo> capabilities,
        List<(string Field, string MethodName, AtsTypeRef ReturnType, string CapabilityId, string? Description)> listDictGetters)
    {
        var documentation = _handleDocumentation.GetValueOrDefault(typeId);
        WriteDocLines(_docs.Render(new GoDocContent
        {
            GoName = interfaceName,
            Subject = GoDocSubject.Type,
            Summary = documentation?.Summary,
            FallbackSummary = $"{interfaceName} is the public interface for handle type {interfaceName}.",
            Remarks = documentation?.Remarks,
        }));
        WriteLine($"type {interfaceName} interface {{");
        WriteLine("\thandleReference");
 
        // The interface itself must declare the markers: a value whose static type is this interface
        // is assignable to an accepting interface only if its method set includes the marker.
        if (_acceptingMarkers.TryGetValue(typeId, out var markers))
        {
            foreach (var marker in markers)
            {
                WriteLine($"\t{marker}()");
            }
        }
 
        // Capability methods (skip list/dict getters — they're rendered as
        // distinct accessor methods below).
        var listDictCapIds = listDictGetters.Select(g => g.CapabilityId).ToHashSet(StringComparer.Ordinal);
        foreach (var capability in capabilities)
        {
            if (listDictCapIds.Contains(capability.CapabilityId))
            {
                continue;
            }
            WriteInterfaceMemberDoc(_docs.Render(CreateCapabilityDoc(capability)));
            WriteLine($"\t{RenderMethodSignature(typeId, interfaceName, capability)}");
        }
 
        // List/Dict accessor methods.
        foreach (var (_, methodName, returnType, capabilityId, _) in listDictGetters)
        {
            var (wrapperType, typeArgs) = ListDictTypeArgs(returnType);
            WriteInterfaceMemberDoc(_docs.Render(CreateCapabilityDoc(capabilities.First(c => c.CapabilityId == capabilityId))));
            WriteLine($"\t{methodName}() *{wrapperType}{typeArgs}");
        }
 
        // The builder gets a Build() method synthesised by GenerateCreateBuilder;
        // declare it here so the public interface exposes it.
        if (string.Equals(typeId, AtsConstants.BuilderTypeId, StringComparison.Ordinal))
        {
            var appInterface = _interfaceNames.TryGetValue(AtsConstants.ApplicationTypeId, out var appName)
                ? appName
                : "DistributedApplication";
            WriteInterfaceMemberDoc(RenderGoDoc("Build", BuildSummary));
            WriteLine($"\tBuild() ({appInterface}, error)");
        }
 
        // By-name accessors are hand-authored convenience helpers layered over the generated
        // ToArray capability so polyglot callers get the same ergonomics as the .NET indexer.
        if (string.Equals(typeId, InteractionInputCollectionTypeId, StringComparison.Ordinal))
        {
            WriteInterfaceMemberDoc(RenderGoDoc("Get", InputGetSummary));
            WriteLine("\tGet(name string) (*InteractionInput, error)");
            WriteInterfaceMemberDoc(RenderGoDoc("Required", InputRequiredSummary));
            WriteLine("\tRequired(name string) (*InteractionInput, error)");
            WriteInterfaceMemberDoc(RenderGoDoc("Value", InputValueSummary));
            WriteLine("\tValue(name string) (string, error)");
            WriteInterfaceMemberDoc(RenderGoDoc("RequiredValue", InputRequiredValueSummary));
            WriteLine("\tRequiredValue(name string) (string, error)");
        }
 
        // Lifecycle escape hatches.
        var errSummary = string.Equals(typeId, AtsConstants.BuilderTypeId, StringComparison.Ordinal)
            ? BuilderErrSummary
            : ErrSummary;
        WriteInterfaceMemberDoc(RenderGoDoc("Err", errSummary));
        WriteLine("\tErr() error");
 
        WriteLine("}");
        WriteLine();
    }
 
    // A blank line before each documented member keeps a multi-line comment visibly attached to the member below it.
    private void WriteInterfaceMemberDoc(IReadOnlyList<string> lines)
    {
        if (lines.Count == 0)
        {
            return;
        }
 
        WriteLine();
        WriteDocLines(lines, "\t");
    }
 
    private IReadOnlyList<string> RenderGoDoc(string goName, string summary)
    {
        return _docs.Render(new GoDocContent { GoName = goName, Summary = summary });
    }
 
    private void EmitConcreteStruct(
        string interfaceName,
        string implName,
        List<(string Field, string MethodName, AtsTypeRef ReturnType, string CapabilityId, string? Description)> listDictGetters)
    {
        WriteLine($"// {implName} is the unexported impl of {interfaceName}.");
        WriteLine($"type {implName} struct {{");
        WriteLine("\t*resourceBuilderBase");
        foreach (var (field, _, returnType, _, _) in listDictGetters)
        {
            var (wrapperType, typeArgs) = ListDictTypeArgs(returnType);
            WriteLine($"\t{field} *{wrapperType}{typeArgs}");
        }
        WriteLine("}");
        WriteLine();
    }
 
    private void EmitConcreteConstructor(string interfaceName, string implName)
    {
        WriteLine($"// new{interfaceName}FromHandle wraps an existing handle as {interfaceName}.");
        WriteLine($"func new{interfaceName}FromHandle(h *handle, c *client) {interfaceName} {{");
        WriteLine($"\treturn &{implName}{{resourceBuilderBase: newResourceBuilderBase(h, c)}}");
        WriteLine("}");
        WriteLine();
    }
 
    private void EmitAcceptingMarkerMethods(string typeId, string implName)
    {
        if (!_acceptingMarkers.TryGetValue(typeId, out var markers))
        {
            return;
        }
 
        WriteLine($"// {implName} implements the marker of each accepting interface for its own type and its base types.");
        foreach (var marker in markers)
        {
            WriteLine($"func (*{implName}) {marker}() {{}}");
        }
        WriteLine();
    }
 
    // Type ID of InteractionInputCollection. The by-name accessors below are hand-authored on top of the
    // generated ToArray capability so Go matches the .NET indexer and the TypeScript get/value helpers.
    private const string InteractionInputCollectionTypeId = "Aspire.Hosting/Aspire.Hosting.InteractionInputCollection";
 
    // Hand-authored members are documented on both the public interface and the implementation.
    private const string BuildSummary = "Build invokes the build capability and returns the running application.";
    private const string BuilderErrSummary = "Err returns an error combining every failure recorded by this builder or by any handle created from it.";
    private const string ErrSummary = "Err returns the first error recorded on this wrapper, or nil if the wrapper resolved successfully and no chained operation has failed.";
    private const string InputGetSummary = "Get returns the input with the specified name, or nil if no input matches.";
    private const string InputRequiredSummary = "Required returns the input with the specified name, or an error if no input matches.";
    private const string InputValueSummary = "Value returns the value of the input with the specified name, or an empty string if no input matches or it has no value.";
    private const string InputRequiredValueSummary = "RequiredValue returns the value of the input with the specified name, or an error if no input matches.";
 
    private void EmitInteractionInputCollectionAccessors(string implName)
    {
        // These delegate to ToArray (a single RPC) and then match locally. Names are compared
        // case-insensitively via strings.EqualFold to mirror StringComparers.InteractionInputName in .NET.
        WriteDocLines(RenderGoDoc("Get", InputGetSummary));
        WriteLine($"func (s *{implName}) Get(name string) (*InteractionInput, error) {{");
        WriteLine("\tif err := s.Err(); err != nil { return nil, err }");
        WriteLine("\tinputs, err := s.ToArray()");
        WriteLine("\tif err != nil { return nil, err }");
        WriteLine("\tfor _, input := range inputs {");
        WriteLine("\t\tif strings.EqualFold(input.Name, name) { return input, nil }");
        WriteLine("\t}");
        WriteLine("\treturn nil, nil");
        WriteLine("}");
        WriteLine();
 
        WriteDocLines(RenderGoDoc("Required", InputRequiredSummary));
        WriteLine($"func (s *{implName}) Required(name string) (*InteractionInput, error) {{");
        WriteLine("\tinput, err := s.Get(name)");
        WriteLine("\tif err != nil { return nil, err }");
        WriteLine("\tif input == nil { return nil, fmt.Errorf(\"no input with name '%s' was found\", name) }");
        WriteLine("\treturn input, nil");
        WriteLine("}");
        WriteLine();
 
        WriteDocLines(RenderGoDoc("Value", InputValueSummary));
        WriteLine($"func (s *{implName}) Value(name string) (string, error) {{");
        WriteLine("\tinput, err := s.Get(name)");
        WriteLine("\tif err != nil { return \"\", err }");
        WriteLine("\tif input == nil || input.Value == nil { return \"\", nil }");
        WriteLine("\treturn *input.Value, nil");
        WriteLine("}");
        WriteLine();
 
        WriteDocLines(RenderGoDoc("RequiredValue", InputRequiredValueSummary));
        WriteLine($"func (s *{implName}) RequiredValue(name string) (string, error) {{");
        WriteLine("\tinput, err := s.Required(name)");
        WriteLine("\tif err != nil { return \"\", err }");
        WriteLine("\tif input.Value == nil { return \"\", nil }");
        WriteLine("\treturn *input.Value, nil");
        WriteLine("}");
        WriteLine();
    }
 
    // ── Capability method emission ────────────────────────────────────────────
 
    private string RenderMethodSignature(string currentTypeId, string interfaceName, AtsCapabilityInfo capability)
    {
        var methodName = ToPascalCase(capability.MethodName);
        var targetParamName = capability.TargetParameterName ?? "builder";
        var parameters = capability.Parameters
            .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
            .ToList();
 
        var (requiredParams, optionalParams) = SplitParameters(parameters);
        var paramSig = RenderParameterList(requiredParams, optionalParams, capability);
        var returnSig = RenderReturnSignature(currentTypeId, interfaceName, capability);
        return $"{methodName}({paramSig}){returnSig}";
    }
 
    private static (List<AtsParameterInfo> Required, List<AtsParameterInfo> Optional) SplitParameters(
        List<AtsParameterInfo> parameters)
    {
        var required = new List<AtsParameterInfo>();
        var optional = new List<AtsParameterInfo>();
        foreach (var p in parameters)
        {
            // CancellationToken is always treated as optional regardless of
            // metadata — we want app.Run() to be callable without an explicit
            // token. Optional callbacks also live in Options (matches TS).
            if (IsCancellationToken(p) || p.IsOptional)
            {
                optional.Add(p);
            }
            else
            {
                required.Add(p);
            }
        }
        return (required, optional);
    }
 
    private string RenderParameterList(
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        AtsCapabilityInfo capability)
    {
        var sb = new StringBuilder();
        foreach (var p in requiredParams)
        {
            if (sb.Length > 0)
            {
                sb.Append(", ");
            }
            sb.Append(GetLocalIdentifier(p.Name));
            sb.Append(' ');
            sb.Append(RenderParameterType(p));
        }
        if (optionalParams.Count > 0)
        {
            if (sb.Length > 0)
            {
                sb.Append(", ");
            }
            sb.Append("options ...");
            sb.Append(GetOptionInterfaceName(capability));
        }
        return sb.ToString();
    }
 
    private string RenderParameterType(AtsParameterInfo p)
    {
        if (p.IsCallback)
        {
            return RenderCallbackType(p);
        }
        if (IsCancellationToken(p))
        {
            return "*CancellationToken";
        }
        return MapInputTypeRefToGo(p.Type, p.IsOptional);
    }
 
    /// <summary>
    /// Renders a Go function type from the callback metadata. Falls back to
    /// `func(...any) any` only if the metadata is empty.
    /// </summary>
    private string RenderCallbackType(AtsParameterInfo p)
    {
        var sb = new StringBuilder("func(");
        if (p.CallbackParameters is { Count: > 0 } cps)
        {
            for (var i = 0; i < cps.Count; i++)
            {
                if (i > 0) { sb.Append(", "); }
                sb.Append(GetLocalIdentifier(cps[i].Name));
                sb.Append(' ');
                sb.Append(MapTypeRefToGo(cps[i].Type, false));
            }
        }
        else
        {
            sb.Append("...any");
        }
        sb.Append(')');
        var hasReturn = p.CallbackReturnType is not null
            && p.CallbackReturnType.TypeId != AtsConstants.Void;
        if (hasReturn)
        {
            sb.Append(' ');
            sb.Append(MapTypeRefToGo(p.CallbackReturnType, false));
        }
        else if (p.CallbackParameters is null)
        {
            // Legacy fallback signature used to return any; keep that shape
            // so dynamic-arg callbacks with no metadata still compile.
            sb.Append(" any");
        }
        return sb.ToString();
    }
 
    private string RenderReturnSignature(string currentTypeId, string interfaceName, AtsCapabilityInfo capability)
    {
        var hasReturn = capability.ReturnType is not null && capability.ReturnType.TypeId != AtsConstants.Void;
        if (!hasReturn)
        {
            // Void capability → returns error (sequential model). Failures are
            // not deferred; the caller checks the returned error.
            return " error";
        }
 
        // Handle returns yield a typed wrapper. Fluent (return == receiver)
        // returns the receiver interface for chaining; the err lives on the
        // receiver and is read via .Err(). Value returns are synchronous and
        // return (T, error).
        var returnType = capability.ReturnType!;
        if (returnType.Category != AtsTypeCategory.Handle)
        {
            return $" ({MapTypeRefToGo(returnType, false)}, error)";
        }
        if (IsSameType(returnType.TypeId, capability, currentTypeId))
        {
            return $" {interfaceName}";
        }
        return $" {MapTypeRefToGo(returnType, false)}";
    }
 
    private static bool IsSameType(string? typeId, AtsCapabilityInfo capability, string? currentTypeId = null)
    {
        if (typeId is null)
        {
            return false;
        }
        // Fluent: return type matches the constraint type (e.g. generic `T` resolves to the constraint).
        if (string.Equals(typeId, capability.TargetTypeId, StringComparison.Ordinal))
        {
            return true;
        }
        // Fluent: return type matches the specific concrete type being emitted right now.
        // Do NOT check all ExpandedTargetTypes — that incorrectly flags a method as fluent
        // when its return type happens to be one of the other expanded types (e.g.
        // GetAzureContainerRegistry() returns AzureContainerRegistryResource which is itself
        // an expanded target; testing all expanded types would mark every emit as fluent).
        if (currentTypeId is not null && string.Equals(typeId, currentTypeId, StringComparison.Ordinal))
        {
            return true;
        }
        return false;
    }
 
    private void EmitCapabilityMethod(
        string typeId,
        string interfaceName,
        string implName,
        AtsCapabilityInfo capability,
        List<(string Field, string MethodName, AtsTypeRef ReturnType, string CapabilityId, string? Description)> listDictGetters)
    {
        // List/Dict property getters get a distinct emission path.
        var listDictMatch = listDictGetters.FirstOrDefault(g => g.CapabilityId == capability.CapabilityId);
        if (listDictMatch.CapabilityId is not null)
        {
            EmitListDictGetter(implName, listDictMatch, capability);
            return;
        }
 
        // Union-param capabilities are emitted as plain methods with `any`
        // for the union parameters. Go's type-set generics can't express
        // unions containing interfaces-with-methods (which is the shape of
        // most of our handle types), so the design's generic-free-function
        // form would degenerate to `func Foo[T any](...)` — pure ceremony.
        // serializeValue handles dispatch by concrete type at runtime.
 
        var methodName = ToPascalCase(capability.MethodName);
        var targetParamName = capability.TargetParameterName ?? "builder";
        var parameters = capability.Parameters
            .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
            .ToList();
        var (requiredParams, optionalParams) = SplitParameters(parameters);
 
        var paramSig = RenderParameterList(requiredParams, optionalParams, capability);
        var returnSig = RenderReturnSignature(typeId, interfaceName, capability);
 
        // The full documentation lives on the public interface method; the unexported implementation only
        // keeps the summary so the source stays readable.
        WriteDocLines(_docs.RenderSummary(CreateCapabilityDoc(capability)));
        EmitUnionAllowedTypesDoc(capability);
 
        WriteLine($"func (s *{implName}) {methodName}({paramSig}){returnSig} {{");
        EmitCapabilityBody(capability, requiredParams, optionalParams, targetParamName, typeId);
 
        WriteLine("}");
        WriteLine();
    }
 
    /// <summary>
    /// Emits a body for a capability that returns a handle. Sequential model:
    /// fluent (return == receiver) → run RPC, store any error on s, return s.
    /// Otherwise → run RPC, return a fresh child wrapper (pre-errored if the
    /// parent already failed or the RPC failed).
    /// </summary>
    private void EmitHandleReturningBody(
        AtsCapabilityInfo capability,
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        string targetParamName,
        string currentTypeId)
    {
        var returnType = capability.ReturnType!;
        var isFluent = IsSameType(returnType.TypeId, capability, currentTypeId);
        var methodName = ToPascalCase(capability.MethodName);
        var receiverErr = string.Equals(currentTypeId, AtsConstants.BuilderTypeId, StringComparison.Ordinal)
            ? "s.resourceBuilderBase.Err()"
            : "s.Err()";
 
        if (isFluent)
        {
            WriteLine($"\tif {receiverErr} != nil {{ return s }}");
            EmitHandleParamErrorChecks("\t", capability, "s.setPropagatedErr(err); return s");
            EmitUnionTypeChecks("\t", capability, methodName, new[] { "s.setErr(err); return s" });
            EmitArgsConstruction("\t", capability, requiredParams, optionalParams, targetParamName, "s.handle");
                WriteLine($"\tif _, err := s.client.invokeCapability(ctx, {GoStringLiteral(capability.CapabilityId)}, reqArgs); err != nil {{ s.setErr(err) }}");
            WriteLine("\treturn s");
            return;
        }
 
        var childImplName = returnType.TypeId is not null && _implNames.TryGetValue(returnType.TypeId, out var implTarget)
            ? implTarget
            : null;
 
        if (childImplName is null)
        {
            // No registered impl for this typeId (e.g. *ReferenceExpression,
            // which is hand-written in base.go). Cast the wrapIfHandle result
            // directly to the declared return type. Errors land on s; we
            // return nil and the caller must consult s.Err().
            var returnGoType = MapTypeRefToGo(returnType, false);
            WriteLine($"\tif {receiverErr} != nil {{ return nil }}");
            EmitHandleParamErrorChecks("\t", capability, "s.setPropagatedErr(err); return nil");
            EmitUnionTypeChecks("\t", capability, methodName, new[] { "s.setErr(err); return nil" });
            EmitArgsConstruction("\t", capability, requiredParams, optionalParams, targetParamName, "s.handle");
                WriteLine($"\tresult, err := s.client.invokeCapability(ctx, {GoStringLiteral(capability.CapabilityId)}, reqArgs)");
            WriteLine("\tif err != nil { s.setErr(err); return nil }");
            WriteLine($"\ttyped, ok := result.({returnGoType})");
            WriteLine("\tif !ok {");
            WriteLine($"\t\ts.setErr(fmt.Errorf({GoUnexpectedTypeFormat(capability.CapabilityId)}, result))");
            WriteLine("\t\treturn nil");
            WriteLine("\t}");
            WriteLine("\treturn typed");
            return;
        }
 
        // Pre-errored child path: parent failure short-circuits this call.
        WriteLine($"\tif err := {receiverErr}; err != nil {{ return &{childImplName}{{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)}} }}");
        EmitHandleParamErrorChecks("\t", capability,
            $"return &{childImplName}{{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)}}");
        EmitUnionTypeChecks("\t", capability, methodName, new[]
        {
            $"return &{childImplName}{{resourceBuilderBase: newErroredResourceBuilder(err, s.client)}}",
        });
        EmitArgsConstruction("\t", capability, requiredParams, optionalParams, targetParamName, "s.handle");
        WriteLine($"\tresult, err := s.client.invokeCapability(ctx, {GoStringLiteral(capability.CapabilityId)}, reqArgs)");
        WriteLine("\tif err != nil {");
        WriteLine($"\t\treturn &{childImplName}{{resourceBuilderBase: newErroredResourceBuilder(err, s.client)}}");
        WriteLine("\t}");
        WriteLine("\thref, ok := result.(handleReference)");
        WriteLine("\tif !ok {");
        WriteLine($"\t\terr := fmt.Errorf({GoUnexpectedTypeFormat(capability.CapabilityId)}, result)");
        WriteLine($"\t\treturn &{childImplName}{{resourceBuilderBase: newErroredResourceBuilder(err, s.client)}}");
        WriteLine("\t}");
        WriteLine($"\treturn &{childImplName}{{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)}}");
    }
 
    /// <summary>
    /// Emits a body for a capability that returns a non-handle value. Sync RPC
    /// returning (T, error). A pending error on the receiver short-circuits.
    /// </summary>
    private void EmitValueReturningBody(
        AtsCapabilityInfo capability,
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        string targetParamName)
    {
        var returnType = capability.ReturnType!;
        var returnGoType = MapTypeRefToGo(returnType, false);
        var methodName = ToPascalCase(capability.MethodName);
 
        WriteLine($"\tif err := s.Err(); err != nil {{ var zero {returnGoType}; return zero, err }}");
        EmitHandleParamErrorChecks("\t", capability, $"var zero {returnGoType}; return zero, err");
        EmitUnionTypeChecks("\t", capability, methodName, new[] { $"var zero {returnGoType}", "return zero, err" });
        EmitArgsConstruction("\t", capability, requiredParams, optionalParams, targetParamName, "s.handle");
        WriteLine($"\tresult, err := s.client.invokeCapability(ctx, {GoStringLiteral(capability.CapabilityId)}, reqArgs)");
        WriteLine("\tif err != nil {");
        WriteLine($"\t\tvar zero {returnGoType}");
        WriteLine("\t\treturn zero, err");
        WriteLine("\t}");
        WriteLine($"\treturn decodeAs[{returnGoType}](result)");
    }
 
    /// <summary>
    /// Emits a body for a void capability: synchronous RPC returning error.
    /// </summary>
    private void EmitVoidBody(
        AtsCapabilityInfo capability,
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        string targetParamName)
    {
        var methodName = ToPascalCase(capability.MethodName);
        WriteLine("\tif err := s.Err(); err != nil { return err }");
        EmitHandleParamErrorChecks("\t", capability, "return err");
        EmitUnionTypeChecks("\t", capability, methodName, new[] { "return err" });
        EmitArgsConstruction("\t", capability, requiredParams, optionalParams, targetParamName, "s.handle");
        WriteLine($"\t_, err := s.client.invokeCapability(ctx, {GoStringLiteral(capability.CapabilityId)}, reqArgs)");
        WriteLine("\treturn err");
    }
 
    /// <summary>
    /// Emits short-circuit checks for handle-typed REQUIRED parameters: if
    /// any of them already carry an error, propagate it via the supplied
    /// errorAction (e.g. "s.setErr(err); return s" or "return err"). Optional
    /// handle parameters live inside the Options struct and are not in scope
    /// at this point.
    /// </summary>
    private void EmitHandleParamErrorChecks(string indent, AtsCapabilityInfo capability, string errorAction)
    {
        var targetParamName = capability.TargetParameterName ?? "builder";
        foreach (var p in capability.Parameters)
        {
            if (string.Equals(p.Name, targetParamName, StringComparison.Ordinal)) { continue; }
            if (p.IsOptional) { continue; } // optional → in Options struct, not a local
            if (p.IsCallback) { continue; }
            if (IsCancellationToken(p)) { continue; }
            if (p.Type?.Category != AtsTypeCategory.Handle) { continue; }
            var paramName = GetLocalIdentifier(p.Name);
            WriteLine($"{indent}if {paramName} != nil {{ if err := {paramName}.Err(); err != nil {{ {errorAction} }} }}");
        }
    }
 
    private void EmitArgsConstruction(
        string indent,
        AtsCapabilityInfo capability,
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        string targetParamName,
        string targetHandleVar)
    {
        // ctx defaults to Background. If the caller provided a CancellationToken
        // via Options, we override ctx inside the options block below so the
        // local Go RPC respects the user's cancellation.
        WriteLine($"{indent}ctx := context.Background()");
        WriteLine($"{indent}reqArgs := map[string]any{{");
        WriteLine($"{indent}\t{GoStringLiteral(targetParamName)}: {targetHandleVar}.ToJSON(),");
        WriteLine($"{indent}}}");
 
        foreach (var p in requiredParams)
        {
            var paramName = GetLocalIdentifier(p.Name);
            if (p.IsCallback)
            {
                EmitCallbackRegistration(indent, p, paramName);
                continue;
            }
            if (IsCancellationToken(p))
            {
                // Only register/send the cancellation token when the caller
                // explicitly provides one — matches TypeScript SDK behavior.
                // We deliberately do NOT substitute a default token here:
                // tying every server-side op to a wrapper-internal lifetime
                // would create unwanted cross-cancellation and leak token
                // state on the server for ops the user didn't ask to cancel.
                WriteLine($"{indent}if {paramName} != nil {{");
                WriteLine($"{indent}\tctx = {paramName}.Context()");
                WriteLine($"{indent}\tif id := s.client.registerCancellation({paramName}); id != \"\" {{");
                WriteLine($"{indent}\t\treqArgs[{GoStringLiteral(p.Name)}] = id");
                WriteLine($"{indent}\t}}");
                WriteLine($"{indent}}}");
                continue;
            }
 
            var type = MapTypeRefToGo(p.Type, isOptional: false);
            // A nil DTO or ReferenceExpression must be checked before it is boxed into any; otherwise
            // serializeValue sees a non-nil interface and dereferences the typed nil. Nullable primitive
            // and enum pointers intentionally bypass this guard because nil represents an explicit ATS null.
            if (IsNilableGoType(type) && (p.Type is null || !ShouldApplyNullableType(p.Type)))
            {
                var nilGuard = type == "any" ? $"!isNil({paramName})" : $"{paramName} != nil";
                WriteLine($"{indent}if {nilGuard} {{ reqArgs[{GoStringLiteral(p.Name)}] = serializeValue({paramName}) }}");
            }
            else
            {
                WriteLine($"{indent}reqArgs[{GoStringLiteral(p.Name)}] = serializeValue({paramName})");
            }
        }
 
        if (optionalParams.Count > 0)
        {
            var optionsType = GetOptionsTypeName(capability);
            WriteLine($"{indent}if len(options) > 0 {{");
            WriteLine($"{indent}\tmerged := &{optionsType}{{}}");
            WriteLine($"{indent}\tfor _, opt := range options {{");
            WriteLine($"{indent}\t\tif opt != nil {{ opt.apply(merged) }}");
            WriteLine($"{indent}\t}}");
            WriteLine($"{indent}\tfor k, v := range merged.ToMap() {{ reqArgs[k] = v }}");
            // Callbacks: wrap the typed user function in a positional `any`
            // shim that the transport's invokeCallback can call.
            foreach (var p in optionalParams.Where(o => o.IsCallback))
            {
                var fieldName = ToPascalCase(p.Name);
                EmitCallbackRegistration(indent + "\t", p, $"merged.{fieldName}");
            }
            // Cancellation tokens: register, inject the id, and use the
            // token's context for local Go-side cancellation.
            foreach (var p in optionalParams.Where(IsCancellationToken))
            {
                var fieldName = ToPascalCase(p.Name);
                WriteLine($"{indent}\tif merged.{fieldName} != nil {{");
                WriteLine($"{indent}\t\tctx = merged.{fieldName}.Context()");
                WriteLine($"{indent}\t\tif id := s.client.registerCancellation(merged.{fieldName}); id != \"\" {{");
                WriteLine($"{indent}\t\t\treqArgs[{GoStringLiteral(p.Name)}] = id");
                WriteLine($"{indent}\t\t}}");
                WriteLine($"{indent}\t}}");
            }
            WriteLine($"{indent}}}");
        }
    }
 
    /// <summary>
    /// Emits a callback-registration block for the supplied callback variable.
    /// Generates a typed→positional shim that decodes positional args back to
    /// the user-supplied callback's parameter types.
    /// </summary>
    private void EmitCallbackRegistration(string indent, AtsParameterInfo p, string callbackExpr)
    {
        WriteLine($"{indent}if {callbackExpr} != nil {{");
        WriteLine($"{indent}\tcb := {callbackExpr}");
        WriteLine($"{indent}\tshim := func(args ...any) any {{");
        EmitGoCallbackShimBody($"{indent}\t\t", p, "s.client");
        WriteLine($"{indent}\t}}");
        WriteLine($"{indent}\treqArgs[{GoStringLiteral(p.Name)}] = s.client.registerCallback(shim)");
        WriteLine($"{indent}}}");
    }
 
    // Emits a strongly-typed DTO callback property into the DTO's ToMap output. Method parameters
    // pre-register their callback (putting a string id into reqArgs), but DTO properties instead
    // embed the shim func directly in the map; the client's marshalTransportValue walks the
    // serialized args, finds the func(...any) any value, and registers it. This keeps the public
    // DTO field strongly typed (e.g. func(ctx InputsDialogValidationContext)) while reusing the
    // exact arg-decoding/return/writeback behavior of method-parameter callbacks.
    private void EmitDtoCallbackToMap(AtsDtoPropertyInfo property, string propertyName)
    {
        var p = DtoPropertyToParameterInfo(property);
        WriteLine($"\tif d.{propertyName} != nil {{");
        WriteLine($"\t\tcb := d.{propertyName}");
        WriteLine($"\t\tm[{GoStringLiteral(JsonNamingPolicy.CamelCase.ConvertName(property.Name))}] = func(args ...any) any {{");
        EmitGoCallbackShimBody("\t\t\t", p, clientExpression: null);
        WriteLine($"\t\t}}");
        WriteLine($"\t}}");
    }
 
    private static AtsParameterInfo DtoPropertyToParameterInfo(AtsDtoPropertyInfo property)
        => new()
        {
            Name = property.Name,
            Type = property.Type,
            IsOptional = property.IsOptional,
            IsCallback = property.IsCallback,
            CallbackParameters = property.CallbackParameters,
            CallbackReturnType = property.CallbackReturnType,
        };
 
    // Writes the body of a `func(args ...any) any` callback shim, assuming the user's typed
    // callback is in scope as `cb`. Shared by method-parameter and DTO-property callbacks.
    // clientExpression is the Go expression for the *client when one is in scope; DTO-property
    // shims run from ToMap, which has none, so they pass null.
    private void EmitGoCallbackShimBody(string indent, AtsParameterInfo p, string? clientExpression)
    {
        var hasReturn = p.CallbackReturnType is not null
            && p.CallbackReturnType.TypeId != AtsConstants.Void;
        var callExpr = new StringBuilder();
        callExpr.Append("cb(");
        if (p.CallbackParameters is { Count: > 0 } cps)
        {
            for (var i = 0; i < cps.Count; i++)
            {
                if (i > 0) { callExpr.Append(", "); }
                callExpr.Append(GetCallbackArgExpression(cps[i].Type, i, clientExpression));
            }
        }
        else
        {
            callExpr.Append("args...");
        }
        callExpr.Append(')');
 
        if (hasReturn)
        {
            WriteLine($"{indent}return {callExpr}");
        }
        else if (p.CallbackParameters is null)
        {
            // Legacy untyped callback returning any — preserve return value.
            WriteLine($"{indent}return {callExpr}");
        }
        else if (p.CallbackParameters is { Count: > 0 } callbackParameters && callbackParameters.Any(cp => cp.Type.Category == AtsTypeCategory.Dto))
        {
            var argNames = new List<string>(callbackParameters.Count);
            for (var i = 0; i < callbackParameters.Count; i++)
            {
                var argName = $"arg{i}";
                argNames.Add(argName);
                WriteLine($"{indent}{argName} := {GetCallbackArgExpression(callbackParameters[i].Type, i, clientExpression)}");
            }
 
            WriteLine($"{indent}cb({string.Join(", ", argNames)})");
            WriteLine($"{indent}return map[string]any{{");
            for (var i = 0; i < callbackParameters.Count; i++)
            {
                if (callbackParameters[i].Type.Category == AtsTypeCategory.Dto)
                {
                    WriteLine($"{indent}\t\"p{i}\": serializeValue({argNames[i]}),");
                }
            }
            WriteLine($"{indent}}}");
        }
        else
        {
            WriteLine($"{indent}{callExpr}");
            WriteLine($"{indent}return nil");
        }
    }
 
    // Returns the Go expression that reads callback argument `index` as the user's typed parameter.
    // The host tags a handle argument with the callback's declared CLR parameter type. For
    // IResourceBuilder<T> that is a closed generic type id that has no registered wrapper, so the
    // transport delivers a raw *handle. Building the declared wrapper from it, as the TypeScript
    // SDK does, keeps the user's callback from receiving nil. Marker interfaces have no generated
    // constructor, but registerWrappers registers a wrapper for their type id, so they keep
    // callbackArg, as do shims with no client in scope.
    private string GetCallbackArgExpression(AtsTypeRef type, int index, string? clientExpression)
    {
        if (clientExpression is not null && IsCancellationTokenTypeId(type.TypeId))
        {
            return $"callbackCancellationTokenArg({clientExpression}, args, {index})";
        }
 
        if (clientExpression is not null
            && type.Category == AtsTypeCategory.Handle
            && _implNames.ContainsKey(type.TypeId))
        {
            return $"callbackHandleArg({clientExpression}, args, {index}, new{_interfaceNames[type.TypeId]}FromHandle)";
        }
 
        return $"callbackArg[{MapTypeRefToGo(type, false)}](args, {index})";
    }
 
    // ── List / Dict accessor methods ─────────────────────────────────────────
 
    private void EmitListDictGetter(
        string implName,
        (string Field, string MethodName, AtsTypeRef ReturnType, string CapabilityId, string? Description) g,
        AtsCapabilityInfo capability)
    {
        var (wrapperType, typeArgs) = ListDictTypeArgs(g.ReturnType);
        var factory = wrapperType == "Dict" ? "newDictWithGetter" : "newListWithGetter";
 
        WriteDocLines(_docs.RenderSummary(CreateCapabilityDoc(capability)));
        WriteLine($"func (s *{implName}) {g.MethodName}() *{wrapperType}{typeArgs} {{");
        WriteLine("\ts.collectionMu.Lock()");
        WriteLine("\tdefer s.collectionMu.Unlock()");
        WriteLine($"\tif s.{g.Field} == nil {{");
        WriteLine($"\t\ts.{g.Field} = {factory}{typeArgs}(s.handleWrapperBase, {GoStringLiteral(g.CapabilityId)})");
        WriteLine("\t}");
        WriteLine($"\treturn s.{g.Field}");
        WriteLine("}");
        WriteLine();
    }
 
    private (string Wrapper, string TypeArgs) ListDictTypeArgs(AtsTypeRef returnType)
    {
        if (returnType.Category == AtsTypeCategory.Dict)
        {
            var keyType = MapTypeRefToGo(returnType.KeyType, false);
            var valueType = MapTypeRefToGo(returnType.ValueType, false);
            return ("Dict", $"[{keyType}, {valueType}]");
        }
        var elementType = MapTypeRefToGo(returnType.ElementType, false);
        return ("List", $"[{elementType}]");
    }
 
    // ── Options structs ──────────────────────────────────────────────────────
 
    private void GenerateOptionsStructs(IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        var emitted = new HashSet<string>(StringComparer.Ordinal);
        var seenForCapability = new Dictionary<string, string>(StringComparer.Ordinal);
        var anyEmitted = false;
 
        foreach (var capability in capabilities)
        {
            var targetParamName = capability.TargetParameterName ?? "builder";
            var optionalParams = capability.Parameters
                .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
                .Where(p => IsCancellationToken(p) || p.IsOptional)
                .ToList();
            if (optionalParams.Count == 0)
            {
                continue;
            }
 
            var name = GetOptionsTypeName(capability);
            seenForCapability[capability.CapabilityId] = name;
            if (!emitted.Add(name))
            {
                continue;
            }
 
            if (!anyEmitted)
            {
                WriteLine("// ============================================================================");
                WriteLine("// Options structs");
                WriteLine("// ============================================================================");
                WriteLine();
                anyEmitted = true;
            }
 
            WriteLine($"// {name} carries optional parameters for {ToPascalCase(capability.MethodName)}.");
            WriteLine($"type {name} struct {{");
            foreach (var p in optionalParams)
            {
                var fieldName = ToPascalCase(p.Name);
                var fieldType = RenderOptionsFieldType(p);
                // Callback fields and cancellation tokens are not JSON-marshaled
                // — they're handled via registerCallback / registerCancellation
                // in the body. Marking with json:"-" keeps them out of any
                // accidental Marshal calls.
                var jsonTag = (p.IsCallback || IsCancellationToken(p))
                    ? "`json:\"-\"`"
                    : $"`json:{GoStringLiteral(p.Name + ",omitempty")}`";
                WriteDocLines(_docs.Render(new GoDocContent
                {
                    GoName = fieldName,
                    Subject = GoDocSubject.Field,
                    Summary = capability.CapabilityKind == AtsCapabilityKind.PropertySetter ? null : p.Documentation?.Summary,
                }), "\t");
                WriteLine($"\t{fieldName} {fieldType} {jsonTag}");
            }
            WriteLine("}");
            WriteLine();
 
            var optionInterfaceName = GetOptionInterfaceName(capability);
            WriteLine($"// {optionInterfaceName} configures {ToPascalCase(capability.MethodName)}.");
            WriteLine($"type {optionInterfaceName} interface {{");
            WriteLine($"\tapply(*{name})");
            WriteLine("}");
            WriteLine();
            WriteLine($"func (option *{name}) apply(options *{name}) {{");
            WriteLine("\tif option != nil { *options = *deepUpdate(options, option) }");
            WriteLine("}");
            WriteLine();
 
            foreach (var p in optionalParams)
            {
                var fieldName = ToPascalCase(p.Name);
                var optionName = GetOptionFunctionName(capability, p);
                var parameterType = RenderOptionFunctionParameterType(p);
                WriteLine($"// {optionName} sets {fieldName} for {ToPascalCase(capability.MethodName)}.");
                if (CanUseDirectOptionType(p))
                {
                    WriteLine($"type {optionName} {parameterType}");
                    WriteLine();
                    WriteLine($"func (value {optionName}) apply(options *{name}) {{");
                    WriteLine($"\toptions.{fieldName} = {RenderDirectOptionAssignment(p)}");
                    WriteLine("}");
                }
                else
                {
                    var implementationName = ToCamelCase(optionName);
                    WriteLine($"type {implementationName} struct {{ value {parameterType} }}");
                    WriteLine();
                    WriteLine($"func (option {implementationName}) apply(options *{name}) {{");
                    WriteLine($"\toptions.{fieldName} = {RenderWrappedOptionAssignment(p)}");
                    WriteLine("}");
                    WriteLine();
                    WriteLine($"func {optionName}(value {parameterType}) {optionInterfaceName} {{");
                    WriteLine($"\treturn {implementationName}{{value: value}}");
                    WriteLine("}");
                }
                WriteLine();
            }
 
            WriteLine($"func (o *{name}) ToMap() map[string]any {{");
            WriteLine("\tm := map[string]any{}");
            WriteLine("\tif o == nil { return m }");
            foreach (var p in optionalParams)
            {
                // Callbacks and CTs are registered separately by the calling
                // body — they cannot be plain-serialized.
                if (p.IsCallback || IsCancellationToken(p))
                {
                    continue;
                }
                var fieldName = ToPascalCase(p.Name);
                var fieldType = MapTypeRefToGo(p.Type, isOptional: true);
                if (IsNilableGoType(fieldType))
                {
                    var value = p.Type?.Category == AtsTypeCategory.Handle
                        && !IsNilableGoType(MapInputTypeRefToGo(p.Type, isOptional: false))
                            ? $"*o.{fieldName}"
                            : $"o.{fieldName}";
                    WriteLine($"\tif o.{fieldName} != nil {{ m[{GoStringLiteral(p.Name)}] = serializeValue({value}) }}");
                }
                else
                {
                    WriteLine($"\tm[{GoStringLiteral(p.Name)}] = serializeValue(o.{fieldName})");
                }
            }
            WriteLine("\treturn m");
            WriteLine("}");
            WriteLine();
        }
    }
 
    private void GenerateCreateBuilderOptions(AtsContext context)
    {
        if (_createBuilderOptionInterfaceName is null
            || context.DtoTypes.FirstOrDefault(dto => dto.Name == "CreateBuilderOptions") is not { } dto
            || !_dtoNames.TryGetValue(dto.TypeId, out var optionsType))
        {
            return;
        }
 
        WriteLine($"// {_createBuilderOptionInterfaceName} configures CreateBuilder.");
        WriteLine($"type {_createBuilderOptionInterfaceName} interface {{");
        WriteLine($"\tapply(*{optionsType})");
        WriteLine("}");
        WriteLine();
        WriteLine($"func (option *{optionsType}) apply(options *{optionsType}) {{");
        WriteLine("\tif option != nil { *options = *deepUpdate(options, option) }");
        WriteLine("}");
        WriteLine();
 
        foreach (var property in dto.Properties)
        {
            var fieldName = ToPascalCase(property.Name);
            var optionName = _createBuilderOptionValueNames[property.Name];
            var valueType = RenderCreateBuilderOptionValueType(property);
            var fieldType = MapDtoPropertyTypeToGo(property.Type, property.IsOptional);
 
            WriteLine($"// {optionName} sets {fieldName} for CreateBuilder.");
            if (CanUseDirectOptionType(property.Type))
            {
                WriteLine($"type {optionName} {valueType}");
                WriteLine();
                WriteLine($"func (value {optionName}) apply(options *{optionsType}) {{");
                var assignment = fieldType == $"*{valueType}"
                    ? $"(*{valueType})(&value)"
                    : $"({fieldType})(value)";
                WriteLine($"\toptions.{fieldName} = {assignment}");
                WriteLine("}");
            }
            else
            {
                var implementationName = ToCamelCase(optionName);
                WriteLine($"type {implementationName} struct {{ value {valueType} }}");
                WriteLine();
                WriteLine($"func (option {implementationName}) apply(options *{optionsType}) {{");
                WriteLine($"\toptions.{fieldName} = option.value");
                WriteLine("}");
                WriteLine();
                WriteLine($"func {optionName}(value {valueType}) {_createBuilderOptionInterfaceName} {{");
                WriteLine($"\treturn {implementationName}{{value: value}}");
                WriteLine("}");
            }
            WriteLine();
        }
    }
 
    private string RenderCreateBuilderOptionValueType(AtsDtoPropertyInfo property)
    {
        var type = MapDtoPropertyTypeToGo(property.Type, isOptional: false);
        return property.Type.Category is AtsTypeCategory.Primitive or AtsTypeCategory.Enum
            && type.StartsWith("*", StringComparison.Ordinal)
                ? type[1..]
                : type;
    }
 
    private static bool CanUseDirectOptionType(AtsTypeRef type)
    {
        return type.Category switch
        {
            AtsTypeCategory.Primitive => type.TypeId is not (AtsConstants.Any or AtsConstants.ReferenceExpressionTypeId),
            AtsTypeCategory.Enum or AtsTypeCategory.Callback or AtsTypeCategory.Array => true,
            AtsTypeCategory.List or AtsTypeCategory.Dict => type.IsReadOnly,
            _ => false
        };
    }
 
    /// <summary>
    /// Field type for an Options struct member. Callbacks render as their
    /// typed Go function signature; CT renders as *CancellationToken; other
    /// values use the input mapping with optional/nilable wrapping.
    /// </summary>
    private string RenderOptionsFieldType(AtsParameterInfo p)
    {
        if (p.IsCallback)
        {
            return RenderCallbackType(p);
        }
        if (IsCancellationToken(p))
        {
            return "*CancellationToken";
        }
        return MapInputTypeRefToGo(p.Type, isOptional: true);
    }
 
    private string GetOptionsTypeName(AtsCapabilityInfo capability)
    {
        return _optionsTypeNames.TryGetValue(capability.CapabilityId, out var name)
            ? name
            : $"{ToPascalCase(capability.MethodName)}Options";
    }
 
    private string GetOptionInterfaceName(AtsCapabilityInfo capability)
    {
        return _optionInterfaceNames.TryGetValue(capability.CapabilityId, out var name)
            ? name
            : $"{ToPascalCase(capability.MethodName)}Option";
    }
 
    private string GetOptionFunctionName(AtsCapabilityInfo capability, AtsParameterInfo parameter)
    {
        return _optionValueNames.TryGetValue((capability.CapabilityId, parameter.Name), out var name)
            ? name
            : GetOptionValueNameCandidate(GetOptionsTypeName(capability), ToPascalCase(parameter.Name));
    }
 
    private static string GetOptionValueNameCandidate(string optionsTypeName, string fieldName)
    {
        var prefix = optionsTypeName;
        if (prefix.EndsWith("Options", StringComparison.Ordinal))
        {
            prefix = prefix[..^"Options".Length];
        }
        if (prefix.StartsWith("With", StringComparison.Ordinal)
            && prefix.Length > "With".Length
            && char.IsUpper(prefix["With".Length]))
        {
            prefix = prefix["With".Length..];
        }
 
        return prefix + fieldName;
    }
 
    private string RenderOptionFunctionParameterType(AtsParameterInfo parameter)
    {
        if (parameter.IsCallback)
        {
            return RenderCallbackType(parameter);
        }
        if (IsCancellationToken(parameter))
        {
            return "*CancellationToken";
        }
        var type = MapInputTypeRefToGo(parameter.Type, isOptional: false);
        return parameter.Type is { Category: AtsTypeCategory.Primitive or AtsTypeCategory.Enum }
            && type.StartsWith("*", StringComparison.Ordinal)
                ? type[1..]
                : type;
    }
 
    private static bool CanUseDirectOptionType(AtsParameterInfo parameter)
    {
        if (IsCancellationToken(parameter) || parameter.Type is not { } type)
        {
            return false;
        }
 
        return CanUseDirectOptionType(type);
    }
 
    private string RenderDirectOptionAssignment(AtsParameterInfo parameter)
    {
        var fieldType = RenderOptionsFieldType(parameter);
        var parameterType = RenderOptionFunctionParameterType(parameter);
        if (fieldType == $"*{parameterType}")
        {
            return $"(*{parameterType})(&value)";
        }
 
        return $"({fieldType})(value)";
    }
 
    private string RenderWrappedOptionAssignment(AtsParameterInfo parameter)
    {
        return RenderOptionsFieldType(parameter) == RenderOptionFunctionParameterType(parameter)
            ? "option.value"
            : "&option.value";
    }
 
    private string GetOptionsQualifiedName(AtsCapabilityInfo capability, string simpleName)
    {
        var targetInterfaceName = capability.TargetTypeId is not null
            && _interfaceNames.TryGetValue(capability.TargetTypeId, out var ifName)
            ? ifName
            : SanitizeIdentifier(capability.TargetTypeId ?? "Unknown");
        return $"{targetInterfaceName}{simpleName}";
    }
 
    /// <summary>
    /// Returns true when two capabilities have the same optional-param field
    /// signatures and can share a single Options struct. Compares params by
    /// name and rendered Go type so callbacks with different argument types
    /// (e.g. AzureStorageEmulatorResource vs AzureEventHubsEmulatorResource)
    /// are correctly detected as incompatible.
    /// </summary>
    private bool AreOptionsCompatible(AtsCapabilityInfo a, AtsCapabilityInfo b)
    {
        var aTarget = a.TargetParameterName ?? "builder";
        var bTarget = b.TargetParameterName ?? "builder";
 
        var aOpts = a.Parameters
            .Where(p => !string.Equals(p.Name, aTarget, StringComparison.Ordinal)
                        && (IsCancellationToken(p) || p.IsOptional))
            .OrderBy(p => p.Name, StringComparer.Ordinal)
            .ToList();
        var bOpts = b.Parameters
            .Where(p => !string.Equals(p.Name, bTarget, StringComparison.Ordinal)
                        && (IsCancellationToken(p) || p.IsOptional))
            .OrderBy(p => p.Name, StringComparer.Ordinal)
            .ToList();
 
        if (aOpts.Count != bOpts.Count)
        {
            return false;
        }
 
        for (var i = 0; i < aOpts.Count; i++)
        {
            if (!string.Equals(aOpts[i].Name, bOpts[i].Name, StringComparison.Ordinal))
            {
                return false;
            }
            if (!string.Equals(RenderOptionsFieldType(aOpts[i]), RenderOptionsFieldType(bOpts[i]), StringComparison.Ordinal))
            {
                return false;
            }
        }
        return true;
    }
 
    // ── Union parameter handling ─────────────────────────────────────────────
    //
    // Go's type-set generics cannot express unions containing
    // interfaces-with-methods, which is the shape of nearly every metadata
    // union (handle types are always interfaces-with-methods). The
    // design-spec generic-free-function form would degenerate to
    // `func Foo[T any](...)` — pure ceremony with no compile-time benefit.
    //
    // Instead union parameters are emitted as plain `any` and the generated
    // body type-checks them at runtime against the documented allowed-types
    // list. Wrong types fail fast with a clear error rather than silently
    // serialising garbage. The allowed types are also enumerated in the
    // method's doc comment so editors / godoc surface them.
 
    private static List<AtsParameterInfo> GetUnionParameters(AtsCapabilityInfo capability)
    {
        // Required positional union parameters only — optional unions live in
        // the Options struct as `any` fields and aren't bound to a local var.
        var targetParamName = capability.TargetParameterName ?? "builder";
        var result = new List<AtsParameterInfo>();
        foreach (var p in capability.Parameters)
        {
            if (string.Equals(p.Name, targetParamName, StringComparison.Ordinal)) { continue; }
            if (p.IsOptional) { continue; }
            if (p.IsCallback) { continue; }
            if (IsCancellationToken(p)) { continue; }
            if (p.Type?.Category == AtsTypeCategory.Union)
            {
                result.Add(p);
            }
        }
        return result;
    }
 
    /// <summary>
    /// Emits a runtime type guard for each union parameter. On mismatch the
    /// emitted code creates a descriptive error and runs the supplied
    /// errorAction lines (e.g. propagating to a child wrapper) before
    /// returning. Caller is responsible for choosing the right indentation
    /// and the right error action for the surrounding shape (sync vs goroutine).
    /// </summary>
    private void EmitUnionTypeChecks(
        string indent,
        AtsCapabilityInfo capability,
        string methodName,
        IReadOnlyList<string> errorActionLines)
    {
        foreach (var p in GetUnionParameters(capability))
        {
            var paramName = GetLocalIdentifier(p.Name);
            var allowed = GetUnionAllowedTypes(p.Type!);
            var acceptsNumbers = AllowsGoNumericTypes(GetUnionMemberTypes(p.Type!));
            WriteLine(acceptsNumbers
                ? $"{indent}switch value := {paramName}.(type) {{"
                : $"{indent}switch {paramName}.(type) {{");
            if (acceptsNumbers)
            {
                WriteLine($"{indent}case json.Number:");
                WriteLine($"{indent}\tif !isValidJSONNumber(value) {{");
                WriteLine($"{indent}\t\terr := fmt.Errorf(\"aspire: {methodName}: parameter %q must be one of [{allowed}], got invalid json.Number %q\", {GoStringLiteral(p.Name)}, value)");
                foreach (var line in errorActionLines)
                {
                    WriteLine($"{indent}\t\t{line}");
                }
                WriteLine($"{indent}\t}}");
            }
            WriteLine($"{indent}case {GetUnionTypeSwitchCases(p.Type!)}:");
            WriteLine($"{indent}default:");
            WriteLine($"{indent}\terr := fmt.Errorf(\"aspire: {methodName}: parameter %q must be one of [{allowed}], got %T\", {GoStringLiteral(p.Name)}, {paramName})");
            foreach (var line in errorActionLines)
            {
                WriteLine($"{indent}\t{line}");
            }
            WriteLine($"{indent}}}");
        }
    }
 
    private void EmitUnionAllowedTypesDoc(AtsCapabilityInfo capability)
    {
        foreach (var note in GetUnionNotes(capability))
        {
            WriteLine($"// {note}");
        }
    }
 
    // The notes feed both the unexported implementation method's comment (EmitUnionAllowedTypesDoc)
    // and the exported method's godoc (CreateCapabilityDoc), so the two always say the same thing.
    private List<string> GetUnionNotes(AtsCapabilityInfo capability)
    {
        return GetUnionParameters(capability)
            .Select(p =>
            {
                var allowed = GetUnionAllowedTypes(p.Type!);
                var numericNote = AllowsGoNumericTypes(GetUnionMemberTypes(p.Type!))
                    ? " (float64 accepts any built-in Go numeric type and json.Number)"
                    : "";
                return $"Allowed types for parameter {GetLocalIdentifier(p.Name)}: {allowed}{numericNote}.";
            })
            .ToList();
    }
 
    private string GetUnionAllowedTypes(AtsTypeRef typeRef) => string.Join(", ", GetUnionMemberTypes(typeRef));
 
    // float64 is how an ATS number maps to Go. A union that contains it also accepts the other
    // Go numeric types (see GetUnionTypeSwitchCases), and the generated godoc says so.
    private static bool AllowsGoNumericTypes(string[] memberTypes) => memberTypes.Contains("float64", StringComparer.Ordinal);
 
    // Go boxes an untyped constant such as 3 as int, so a union that allows an ATS number
    // (float64) has to accept the other Go numeric types too. They all serialize as JSON numbers.
    private string GetUnionTypeSwitchCases(AtsTypeRef typeRef)
    {
        var memberTypes = GetUnionMemberTypes(typeRef);
        if (!AllowsGoNumericTypes(memberTypes))
        {
            return string.Join(", ", memberTypes);
        }
 
        return string.Join(", ", memberTypes.Concat(s_goNumericTypes).Distinct(StringComparer.Ordinal));
    }
 
    private string[] GetUnionMemberTypes(AtsTypeRef typeRef)
    {
        if (typeRef.UnionTypes is not { Count: > 0 } types)
        {
            return ["any"];
        }
 
        var allowedTypes = types
            .Select(type => MapInputTypeRefToGo(type, isOptional: false))
            .Distinct(StringComparer.Ordinal)
            .ToArray();
 
        return allowedTypes.Length > 0 ? allowedTypes : ["any"];
    }
 
    /// <summary>
    /// Routes to the appropriate body emission based on return shape.
    /// </summary>
    private void EmitCapabilityBody(
        AtsCapabilityInfo capability,
        List<AtsParameterInfo> requiredParams,
        List<AtsParameterInfo> optionalParams,
        string targetParamName,
        string currentTypeId)
    {
        var hasReturn = capability.ReturnType is not null && capability.ReturnType.TypeId != AtsConstants.Void;
        if (hasReturn && capability.ReturnType!.Category == AtsTypeCategory.Handle)
        {
            EmitHandleReturningBody(capability, requiredParams, optionalParams, targetParamName, currentTypeId);
        }
        else if (hasReturn)
        {
            EmitValueReturningBody(capability, requiredParams, optionalParams, targetParamName);
        }
        else
        {
            EmitVoidBody(capability, requiredParams, optionalParams, targetParamName);
        }
    }
 
    // ── Per-client wrapper registration ──────────────────────────────────────
 
    private void GenerateRegisterWrappers()
    {
        WriteLine("// ============================================================================");
        WriteLine("// Per-client handle wrapper registration");
        WriteLine("// ============================================================================");
        WriteLine();
        WriteLine("func registerWrappers(c *client) {");
        WriteLine("\tc.registerHandleWrapper(\"" + AtsConstants.ReferenceExpressionTypeId + "\", func(h *handle, c *client) any {");
        WriteLine("\t\treturn newHandleBackedReferenceExpression(h, c)");
        WriteLine("\t})");
        foreach (var (typeId, implName) in _implNames.OrderBy(kv => kv.Value, StringComparer.Ordinal))
        {
            var interfaceName = _interfaceNames[typeId];
            WriteLine($"\tc.registerHandleWrapper({GoStringLiteral(typeId)}, func(h *handle, c *client) any {{");
            WriteLine($"\t\treturn new{interfaceName}FromHandle(h, c)");
            WriteLine("\t})");
        }
 
        // The AppHost tags a handle with the type its capability declares, so a result such as
        // IResource[] arrives with the IResource type id. A marker interface has no generated
        // constructor, and without a registered wrapper wrapIfHandle leaves a raw *handle that no
        // marker interface accepts. The bare wrapper satisfies every marker, as the TypeScript
        // SDK's registration of its interface wrappers does.
        foreach (var (typeId, _) in GetMarkerInterfaces())
        {
            WriteLine($"\tc.registerHandleWrapper(\"{typeId}\", func(h *handle, c *client) any {{");
            WriteLine("\t\treturn newHandleWrapperBase(h, c)");
            WriteLine("\t})");
        }
        WriteLine("}");
        WriteLine();
    }
 
    // ── CreateBuilder / Build ────────────────────────────────────────────────
 
    private void GenerateCreateBuilder(AtsContext context)
    {
        var builderTypeId = AtsConstants.BuilderTypeId;
        var hasBuilderImpl = _implNames.TryGetValue(builderTypeId, out var existingBuilderImpl);
        var builderInterface = _interfaceNames.TryGetValue(builderTypeId, out var existingBuilderIface)
            ? existingBuilderIface
            : "DistributedApplicationBuilder";
        var builderImpl = hasBuilderImpl ? existingBuilderImpl! : ToCamelCase(builderInterface);
 
        var appTypeId = AtsConstants.ApplicationTypeId;
        var hasAppInterface = _interfaceNames.ContainsKey(appTypeId);
        var appInterface = hasAppInterface ? _interfaceNames[appTypeId] : "DistributedApplication";
        var hasAppImpl = _implNames.ContainsKey(appTypeId);
 
        WriteLine("// ============================================================================");
        WriteLine("// Builder construction & Build()");
        WriteLine("// ============================================================================");
        WriteLine();
 
        if (!hasBuilderImpl)
        {
            // Synthesize a private impl struct. The interface (and any
            // capability methods on it) was emitted by GenerateConcreteHandleTypes
            // already if hasBuilderImpl was true; otherwise we synthesize a
            // minimal interface here.
            if (!_interfaceNames.ContainsKey(builderTypeId))
            {
                WriteLine($"// {builderInterface} is the entry point to the Aspire SDK.");
                WriteLine($"type {builderInterface} interface {{");
                WriteLine("\thandleReference");
                WriteLine($"\tBuild() ({appInterface}, error)");
                WriteLine("}");
                WriteLine();
            }
            WriteLine($"type {builderImpl} struct {{ *resourceBuilderBase }}");
            WriteLine();
        }
 
        if (!hasAppInterface)
        {
            WriteLine($"// {appInterface} is returned by Build(); it represents the running application.");
            WriteLine($"type {appInterface} interface {{ handleReference }}");
            WriteLine();
        }
        if (!hasAppImpl)
        {
            // Synthesize an impl so wrapIfHandle can return a value satisfying
            // the interface even when the metadata doesn't expose the application
            // as a concrete handle type.
            WriteLine($"type {ToCamelCase(appInterface)} struct {{ *resourceBuilderBase }}");
            WriteLine();
        }
 
        // Err overrides the promoted handle Err(): a failed AddX records its error on
        // the child it returns, which a caller may never check, so the builder
        // reports every error owned by a handle of its client.
        WriteDocLines(RenderGoDoc("Err", BuilderErrSummary));
        WriteLine($"func (b *{builderImpl}) Err() error {{");
        WriteLine("\tif err := b.client.combinedError(); err != nil { return err }");
        WriteLine("\treturn b.resourceBuilderBase.Err()");
        WriteLine("}");
        WriteLine();
 
        // Build() method on the concrete impl. Sequential model: any prior
        // chain error, including one recorded only on a child handle,
        // short-circuits; otherwise invoke the build capability and return the
        // wrapped result.
        WriteDocLines(RenderGoDoc("Build", BuildSummary));
        WriteLine($"func (b *{builderImpl}) Build() ({appInterface}, error) {{");
        WriteLine("\tif err := b.Err(); err != nil { return nil, err }");
        WriteLine($"\tresult, err := b.client.invokeCapability(context.Background(), \"{AtsConstants.BuildCapability}\", map[string]any{{");
        WriteLine("\t\t\"context\": b.handle.ToJSON(),");
        WriteLine("\t})");
        WriteLine("\tif err != nil { return nil, err }");
        WriteLine($"\tapp, ok := result.({appInterface})");
        WriteLine($"\tif !ok {{ return nil, fmt.Errorf(\"aspire: build returned unexpected type %T\", result) }}");
        WriteLine("\treturn app, nil");
        WriteLine("}");
        WriteLine();
 
        // CreateBuilder factory.
        var createOptionsType = GetCreateBuilderOptionsType(context);
        WriteLine($"// CreateBuilder establishes a connection to the AppHost and returns a new builder.");
        if (createOptionsType is not null && _createBuilderOptionInterfaceName is not null)
        {
            WriteLine($"func CreateBuilder(options ...{_createBuilderOptionInterfaceName}) ({builderInterface}, error) {{");
        }
        else
        {
            WriteLine($"func CreateBuilder() ({builderInterface}, error) {{");
        }
        WriteLine("\tsocketPath := os.Getenv(\"REMOTE_APP_HOST_SOCKET_PATH\")");
        WriteLine("\tif socketPath == \"\" {");
        WriteLine("\t\treturn nil, fmt.Errorf(\"REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`\")");
        WriteLine("\t}");
        WriteLine("\tc := newClient(socketPath)");
        WriteLine("\tif err := c.connect(context.Background(), 5*time.Second); err != nil { return nil, err }");
        WriteLine("\tc.onDisconnect(func() { os.Exit(1) })");
        WriteLine("\tregisterWrappers(c)");
        WriteLine();
        WriteLine("\tresolved := map[string]any{}");
        if (createOptionsType is not null && _createBuilderOptionInterfaceName is not null)
        {
            WriteLine("\tif len(options) > 0 {");
            WriteLine($"\t\tmerged := &{createOptionsType}{{}}");
            WriteLine("\t\tfor _, opt := range options {");
            WriteLine("\t\t\tif opt != nil { opt.apply(merged) }");
            WriteLine("\t\t}");
            WriteLine("\t\tfor k, v := range merged.ToMap() { resolved[k] = v }");
            WriteLine("\t}");
        }
        WriteLine("\tif _, ok := resolved[\"args\"]; !ok { resolved[\"args\"] = os.Args[1:] }");
        // ASPIRE_PROJECT_DIRECTORY is set by the CLI so the host reports the correct project
        // directory (not the cwd) when matching --apphost <directory> requests.
        WriteLine("\tif projectDirectory, ok := resolved[\"projectDirectory\"].(string); !ok || projectDirectory == \"\" {");
        WriteLine("\t\tif projectDirectory := os.Getenv(\"ASPIRE_PROJECT_DIRECTORY\"); projectDirectory != \"\" {");
        WriteLine("\t\t\tresolved[\"projectDirectory\"] = projectDirectory");
        WriteLine("\t\t} else if pwd, err := os.Getwd(); err == nil {");
        WriteLine("\t\t\tresolved[\"projectDirectory\"] = pwd");
        WriteLine("\t\t}");
        WriteLine("\t}");
        WriteLine("\tif appHostFilePath, ok := resolved[\"appHostFilePath\"].(string); !ok || appHostFilePath == \"\" {");
        WriteLine("\t\tif appHostFilePath := os.Getenv(\"ASPIRE_APPHOST_FILEPATH\"); appHostFilePath != \"\" { resolved[\"appHostFilePath\"] = appHostFilePath }");
        WriteLine("\t}");
        WriteLine("\tif dashboardApplicationName, ok := resolved[\"dashboardApplicationName\"].(string); ok && dashboardApplicationName == \"\" {");
        WriteLine("\t\tdelete(resolved, \"dashboardApplicationName\")");
        WriteLine("\t}");
        WriteLine();
        WriteLine($"\tresult, err := c.invokeCapability(context.Background(), \"{AtsConstants.CreateBuilderCapability}\", map[string]any{{\"argsOrOptions\": resolved}})");
        WriteLine("\tif err != nil { return nil, err }");
        WriteLine("\thref, ok := result.(handleReference)");
        WriteLine("\tif !ok { return nil, fmt.Errorf(\"aspire: createBuilder returned unexpected type %T\", result) }");
        WriteLine($"\treturn &{builderImpl}{{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), c)}}, nil");
        WriteLine("}");
        WriteLine();
    }
 
    private string? GetCreateBuilderOptionsType(AtsContext context)
    {
        // The TypeScript generator uses a DTO named "CreateBuilderOptions" for
        // the createBuilder parameter; mirror that.
        foreach (var dto in context.DtoTypes)
        {
            if (dto.Name.Equals("CreateBuilderOptions", StringComparison.Ordinal)
                && _dtoNames.TryGetValue(dto.TypeId, out var goName))
            {
                return goName;
            }
        }
        return null;
    }
 
    // ── Type mapping ─────────────────────────────────────────────────────────
 
    private string MapTypeRefToGo(AtsTypeRef? typeRef, bool isOptional) =>
        MapTypeRefToGo(typeRef, isOptional, asInput: false);
 
    /// <summary>
    /// Maps a type in a position where the caller supplies the value (method parameter, options
    /// field, DTO field, union member). A handle type with derived types maps to its accepting
    /// interface there, so a derived builder can be passed; return types, callback signatures and
    /// everything else use <see cref="MapTypeRefToGo(AtsTypeRef?, bool)"/>.
    /// </summary>
    private string MapInputTypeRefToGo(AtsTypeRef? typeRef, bool isOptional) =>
        MapTypeRefToGo(typeRef, isOptional, asInput: true);
 
    private string MapTypeRefToGo(AtsTypeRef? typeRef, bool isOptional, bool asInput)
    {
        if (typeRef is null)
        {
            return "any";
        }
 
        if (typeRef.TypeId == AtsConstants.ReferenceExpressionTypeId)
        {
            return "*ReferenceExpression";
        }
 
        // Mutable lists and dictionaries wrap a server-side collection and are generic over their
        // element type, which Go treats as invariant: a *List[AnyX] is not a *List[X]. Their elements
        // therefore stay concrete even for inputs.
        var baseType = typeRef.Category switch
        {
            AtsTypeCategory.Primitive => MapPrimitiveType(typeRef),
            AtsTypeCategory.Enum => MapEnumType(typeRef.TypeId),
            AtsTypeCategory.Handle => MapHandleType(typeRef.TypeId, asInput),
            AtsTypeCategory.Dto => "*" + MapDtoType(typeRef.TypeId),
            AtsTypeCategory.Callback => "func(...any) any",
            AtsTypeCategory.Array => $"[]{MapTypeRefToGo(typeRef.ElementType, false, asInput)}",
            AtsTypeCategory.List => typeRef.IsReadOnly
                ? $"[]{MapTypeRefToGo(typeRef.ElementType, false, asInput)}"
                : $"*List[{MapTypeRefToGo(typeRef.ElementType, false)}]",
            AtsTypeCategory.Dict => typeRef.IsReadOnly
                ? $"map[{MapTypeRefToGo(typeRef.KeyType, false, asInput)}]{MapTypeRefToGo(typeRef.ValueType, false, asInput)}"
                : $"*Dict[{MapTypeRefToGo(typeRef.KeyType, false)}, {MapTypeRefToGo(typeRef.ValueType, false)}]",
            AtsTypeCategory.Union => "any",
            AtsTypeCategory.Unknown => "any",
            _ => "any"
        };
 
        if ((isOptional || ShouldApplyNullableType(typeRef)) && !IsNilableGoType(baseType))
        {
            return $"*{baseType}";
        }
        return baseType;
    }
 
    private string MapDtoPropertyTypeToGo(AtsTypeRef? typeRef, bool isOptional)
    {
        if (typeRef is null)
        {
            return "any";
        }
 
        if (typeRef.TypeId == AtsConstants.ReferenceExpressionTypeId)
        {
            return "*ReferenceExpression";
        }
 
        var baseType = typeRef.Category switch
        {
            AtsTypeCategory.Array or AtsTypeCategory.List => $"[]{MapDtoPropertyTypeToGo(typeRef.ElementType, false)}",
            AtsTypeCategory.Dict => $"map[{MapDtoPropertyTypeToGo(typeRef.KeyType, false)}]{MapDtoPropertyTypeToGo(typeRef.ValueType, false)}",
            AtsTypeCategory.Union => "any",
            // DTO fields map like inputs, so a handle-typed field would become Any<Type>. That is only right for
            // DTOs the caller builds: DTOs are also decoded from server results and passed to callbacks, where
            // Any<Type> has no fluent methods and decodeStructFields cannot unmarshal into an interface-typed
            // field. Revisit this mapping before a DTO with a handle-typed field can appear in a result.
            _ => MapInputTypeRefToGo(typeRef, isOptional: false)
        };
 
        if (isOptional && !IsNilableGoType(baseType))
        {
            return $"*{baseType}";
        }
        return baseType;
    }
 
    private static bool IsNilableGoType(string typeName) =>
        typeName.StartsWith("*", StringComparison.Ordinal) ||
        typeName.StartsWith("[]", StringComparison.Ordinal) ||
        typeName.StartsWith("map[", StringComparison.Ordinal) ||
        typeName == "any" ||
        typeName.StartsWith("func(", StringComparison.Ordinal);
 
    private static bool ShouldApplyNullableType(AtsTypeRef typeRef) =>
        typeRef.IsNullable == true
        && typeRef.Category is AtsTypeCategory.Primitive or AtsTypeCategory.Enum
        && typeRef.TypeId is not (AtsConstants.Void or AtsConstants.Any or AtsConstants.CancellationToken);
 
    /// <summary>
    /// Handle types map to their Go interface name, or to their accepting interface when the value
    /// is an input and the type has derived types. Interfaces in Go are already reference types —
    /// no leading * required.
    /// </summary>
    private string MapHandleType(string typeId, bool asInput)
    {
        if (asInput && _acceptingNames.TryGetValue(typeId, out var acceptingName))
        {
            return acceptingName;
        }
        if (_interfaceNames.TryGetValue(typeId, out var name))
        {
            return name;
        }
        return "handleReference";
    }
 
    private string MapDtoType(string typeId) =>
        _dtoNames.TryGetValue(typeId, out var name) ? name : "map[string]any";
 
    private string MapEnumType(string typeId) =>
        _enumNames.TryGetValue(typeId, out var name) ? name : "string";
 
    private static string MapPrimitiveType(AtsTypeRef typeRef) => typeRef.TypeId switch
    {
        AtsConstants.String or AtsConstants.Char => "string",
        AtsConstants.Number => MapNumericType(typeRef.ClrType),
        AtsConstants.Boolean => "bool",
        AtsConstants.Void => "",
        AtsConstants.Any => "any",
        AtsConstants.DateTime or AtsConstants.DateTimeOffset or
        AtsConstants.DateOnly or AtsConstants.TimeOnly => "string",
        AtsConstants.TimeSpan => "float64",
        AtsConstants.Guid or AtsConstants.Uri => "string",
        AtsConstants.CancellationToken => "*CancellationToken",
        _ => "any"
    };
 
    private static string MapNumericType(Type? clrType)
    {
        clrType = Nullable.GetUnderlyingType(clrType ?? typeof(double)) ?? clrType ?? typeof(double);
 
        return clrType == typeof(int) ? "int"
            : clrType == typeof(long) ? "int64"
            : clrType == typeof(float) ? "float32"
            : clrType == typeof(short) ? "int16"
            : clrType == typeof(byte) ? "uint8"
            : clrType == typeof(ushort) ? "uint16"
            : clrType == typeof(uint) ? "uint"
            : clrType == typeof(ulong) ? "uint64"
            : clrType == typeof(sbyte) ? "int8"
            : "float64";
    }
 
    /// <summary>
    /// Capabilities the generator emits with hand-written bodies. They must be
    /// skipped when iterating metadata-driven capability emission so the body
    /// is not duplicated.
    /// </summary>
    private static bool IsSpecialCasedCapability(string capabilityId) =>
        string.Equals(capabilityId, AtsConstants.BuildCapability, StringComparison.Ordinal)
        || string.Equals(capabilityId, AtsConstants.CreateBuilderCapability, StringComparison.Ordinal);
 
    private static bool IsCancellationToken(AtsParameterInfo parameter) =>
        IsCancellationTokenTypeId(parameter.Type?.TypeId);
 
    private static bool IsCancellationTokenTypeId(string? typeId) =>
        string.Equals(typeId, AtsConstants.CancellationToken, StringComparison.Ordinal)
        || (typeId?.EndsWith("/System.Threading.CancellationToken", StringComparison.Ordinal) ?? false);
 
    // ── Identifier helpers ────────────────────────────────────────────────────
 
    private static string ExtractTypeName(string typeId)
    {
        var slashIndex = typeId.IndexOf('/', StringComparison.Ordinal);
        var typeName = slashIndex >= 0 ? typeId[(slashIndex + 1)..] : typeId;
        var lastDot = typeName.LastIndexOf('.');
        var plusIndex = typeName.LastIndexOf('+');
        var delimiterIndex = Math.Max(lastDot, plusIndex);
        return delimiterIndex >= 0 ? typeName[(delimiterIndex + 1)..] : typeName;
    }
 
    private static string SanitizeIdentifier(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return "_";
        }
 
        var builder = new StringBuilder(name.Length);
        foreach (var ch in name)
        {
            builder.Append(char.IsLetterOrDigit(ch) || ch == '_' ? ch : '_');
        }
 
        if (!char.IsLetter(builder[0]) && builder[0] != '_')
        {
            builder.Insert(0, '_');
        }
 
        var sanitized = builder.ToString();
        return s_goKeywords.Contains(sanitized) ? sanitized + "_" : sanitized;
    }
 
    private static string GetLocalIdentifier(string name) => SanitizeIdentifier(ToCamelCase(name));
 
    // Quotes ATS metadata (capability ids, parameter and property names, exported string values) as a Go
    // interpreted string literal so a quote, backslash or line break in the metadata cannot end the literal or
    // the line. Control characters, U+2028/U+2029 and the byte-order mark are written as \uXXXX escapes because
    // Go rejects some of them in source files and editors treat the others as line breaks. The backtick is
    // escaped as well so the same literal can sit inside a raw-string struct tag. Other characters, including
    // surrogate pairs, are copied as they are: the file is UTF-8 and Go rejects a \uXXXX escape for a surrogate.
    private static string GoStringLiteral(string value)
    {
        var builder = new StringBuilder(value.Length + 2).Append('"');
        foreach (var ch in value)
        {
            var escaped = ch switch
            {
                '"' => "\\\"",
                '\\' => "\\\\",
                '\n' => "\\n",
                '\r' => "\\r",
                '\t' => "\\t",
                _ => null,
            };
 
            if (escaped is not null)
            {
                builder.Append(escaped);
            }
            else if (char.IsControl(ch) || ch is '`' or '\u2028' or '\u2029' or '\uFEFF')
            {
                builder.Append("\\u").Append(((int)ch).ToString("x4", CultureInfo.InvariantCulture));
            }
            else
            {
                builder.Append(ch);
            }
        }
 
        return builder.Append('"').ToString();
    }
 
    // The capability id is part of the fmt.Errorf format string. Doubling '%' keeps an id such as
    // "Tests/with%dPercent" from being read as a verb; ids without '%' pass through unchanged.
    private static string GoUnexpectedTypeFormat(string capabilityId) =>
        GoStringLiteral($"aspire: {capabilityId.Replace("%", "%%", StringComparison.Ordinal)} returned unexpected type %T");
 
    private static string ToPascalCase(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            return name;
        }
 
        // Every caller emits the result as a Go identifier, so it is sanitized here for all of them.
        return SanitizeIdentifier(char.IsUpper(name[0]) ? name : char.ToUpperInvariant(name[0]) + name[1..]);
    }
 
    private static string ToCamelCase(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            return name;
        }
        if (char.IsLower(name[0]))
        {
            return name;
        }
        return char.ToLowerInvariant(name[0]) + name[1..];
    }
 
    private void WriteLine(string value = "")
    {
        _writer.WriteLine(value);
    }
}