< Summary

Information
Class: NexusLabs.Needlr.Generators.DiagnosticsGenerator
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/DiagnosticsGenerator.cs
Line coverage
97%
Covered lines: 719
Uncovered lines: 15
Coverable lines: 734
Total lines: 1264
Line coverage: 97.9%
Branch coverage
93%
Covered branches: 317
Total branches: 338
Branch coverage: 93.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/DiagnosticsGenerator.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Linq;
 7using System.Text;
 8
 9using NexusLabs.Needlr.Generators.Models;
 10
 11namespace NexusLabs.Needlr.Generators;
 12
 13/// <summary>
 14/// Generates diagnostic markdown output files (DependencyGraph.md, LifetimeSummary.md, RegistrationIndex.md, AnalyzerSt
 15/// </summary>
 16internal static class DiagnosticsGenerator
 17{
 18    /// <summary>
 19    /// Emitted in place of a wall-clock timestamp so generated source stays byte-identical
 20    /// across builds. The <c>NeedlrExtractDiagnostics</c> MSBuild task substitutes the real
 21    /// UTC time when it writes the markdown reports to disk.
 22    /// </summary>
 23    internal const string GeneratedAtPlaceholder = "__NEEDLR_GENERATED_AT__";
 24
 25    internal static string GenerateDiagnosticsSource(DiscoveryResult discoveryResult, string assemblyName, string? proje
 26    {
 10927        var builder = new StringBuilder();
 10928        var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName);
 10929        var timestamp = GeneratedAtPlaceholder;
 30
 10931        builder.AppendLine("// <auto-generated/>");
 10932        builder.AppendLine("// Needlr Diagnostic Output Metadata");
 10933        builder.AppendLine("// This file contains diagnostic markdown content for extraction by MSBuild.");
 10934        builder.AppendLine();
 10935        builder.AppendLine("#nullable enable");
 10936        builder.AppendLine();
 10937        builder.AppendLine($"namespace {safeAssemblyName}.Generated;");
 10938        builder.AppendLine();
 10939        builder.AppendLine("/// <summary>");
 10940        builder.AppendLine("/// Contains diagnostic output markdown content generated at compile time.");
 10941        builder.AppendLine("/// Use NeedlrDiagnostics MSBuild property to enable file generation.");
 10942        builder.AppendLine("/// </summary>");
 10943        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 10944        builder.AppendLine("internal static class NeedlrDiagnostics");
 10945        builder.AppendLine("{");
 46
 47        // Output path for MSBuild target
 10948        builder.AppendLine($"    public const string OutputPath = @\"{GeneratorHelpers.EscapeStringLiteral(options.Outpu
 10949        builder.AppendLine();
 50
 51        // Generate all three diagnostic outputs when enabled
 10952        var dependencyGraphContent = GenerateDependencyGraphMarkdown(discoveryResult, assemblyName, timestamp, options.T
 10953        builder.AppendLine("    /// <summary>DependencyGraph.md content</summary>");
 10954        builder.AppendLine($"    public const string DependencyGraph = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(
 10955        builder.AppendLine();
 56
 10957        var lifetimeSummaryContent = GenerateLifetimeSummaryMarkdown(discoveryResult, assemblyName, timestamp, options.T
 10958        builder.AppendLine("    /// <summary>LifetimeSummary.md content</summary>");
 10959        builder.AppendLine($"    public const string LifetimeSummary = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(
 10960        builder.AppendLine();
 61
 10962        var registrationIndexContent = GenerateRegistrationIndexMarkdown(discoveryResult, assemblyName, projectDirectory
 10963        builder.AppendLine("    /// <summary>RegistrationIndex.md content</summary>");
 10964        builder.AppendLine($"    public const string RegistrationIndex = @\"{GeneratorHelpers.EscapeVerbatimStringLitera
 10965        builder.AppendLine();
 66
 10967        var analyzerStatusContent = GenerateAnalyzerStatusMarkdown(timestamp);
 10968        builder.AppendLine("    /// <summary>AnalyzerStatus.md content</summary>");
 10969        builder.AppendLine($"    public const string AnalyzerStatus = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(a
 10970        builder.AppendLine();
 71
 10972        var optionsSummaryContent = GenerateOptionsSummaryMarkdown(discoveryResult, assemblyName, timestamp, options.Typ
 10973        builder.AppendLine("    /// <summary>OptionsSummary.md content</summary>");
 10974        builder.AppendLine($"    public const string OptionsSummary = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(o
 75
 10976        builder.AppendLine("}");
 77
 10978        return builder.ToString();
 79    }
 80
 81    internal static string GenerateDependencyGraphMarkdown(DiscoveryResult discovery, string assemblyName, string timest
 82    {
 12883        var sb = new StringBuilder();
 12884        var types = FilterTypes(discovery.InjectableTypes, typeFilter);
 12885        referencedAssemblyTypes = FilterReferencedAssemblyTypes(referencedAssemblyTypes, typeFilter);
 86
 12887        sb.AppendLine("# Needlr Dependency Graph");
 12888        sb.AppendLine();
 12889        sb.AppendLine($"Generated: {timestamp} UTC");
 12890        sb.AppendLine($"Assembly: {assemblyName}");
 12891        sb.AppendLine();
 92
 93        // Show referenced TypeRegistry assemblies with their types
 12894        if (referencedAssemblyTypes.Count > 0)
 95        {
 1596            sb.AppendLine("## Referenced Plugin Assemblies");
 1597            sb.AppendLine();
 1598            sb.AppendLine("Types from referenced assemblies with `[GenerateTypeRegistry]`:");
 1599            sb.AppendLine();
 100
 78101            foreach (var kvp in referencedAssemblyTypes.OrderBy(kv => kv.Key, StringComparer.Ordinal))
 102            {
 16103                var refAsm = kvp.Key;
 16104                var refTypes = kvp.Value;
 105
 16106                sb.AppendLine($"### {refAsm}");
 16107                sb.AppendLine();
 16108                sb.AppendLine("```mermaid");
 16109                sb.AppendLine("graph TD");
 110
 111                // Group by lifetime
 37112                var refSingletons = refTypes.Where(t => t.Lifetime == GeneratorLifetime.Singleton).ToList();
 37113                var refScopeds = refTypes.Where(t => t.Lifetime == GeneratorLifetime.Scoped).ToList();
 37114                var refTransients = refTypes.Where(t => t.Lifetime == GeneratorLifetime.Transient).ToList();
 115
 16116                if (refSingletons.Any())
 117                {
 16118                    sb.AppendLine($"    subgraph Singleton[\"{refAsm} - Singleton\"]");
 68119                    foreach (var type in refSingletons)
 120                    {
 18121                        var shape = type.IsDecorator ? "[[" : (type.HasFactory ? "{{" : "[");
 18122                        var endShape = type.IsDecorator ? "]]" : (type.HasFactory ? "}}" : "]");
 18123                        sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.FullName)}{shape}\"{type.ShortNa
 124                    }
 16125                    sb.AppendLine("    end");
 126                }
 16127                if (refScopeds.Any())
 128                {
 2129                    sb.AppendLine($"    subgraph Scoped[\"{refAsm} - Scoped\"]");
 8130                    foreach (var type in refScopeds)
 131                    {
 2132                        var shape = type.IsDecorator ? "[[" : (type.HasFactory ? "{{" : "[");
 2133                        var endShape = type.IsDecorator ? "]]" : (type.HasFactory ? "}}" : "]");
 2134                        sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.FullName)}{shape}\"{type.ShortNa
 135                    }
 2136                    sb.AppendLine("    end");
 137                }
 16138                if (refTransients.Any())
 139                {
 1140                    sb.AppendLine($"    subgraph Transient[\"{refAsm} - Transient\"]");
 4141                    foreach (var type in refTransients)
 142                    {
 1143                        var shape = type.IsDecorator ? "[[" : (type.HasFactory ? "{{" : "[");
 1144                        var endShape = type.IsDecorator ? "]]" : (type.HasFactory ? "}}" : "]");
 1145                        sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.FullName)}{shape}\"{type.ShortNa
 146                    }
 1147                    sb.AppendLine("    end");
 148                }
 149
 150                // Show dependency edges within this assembly
 37151                var refTypeNames = new HashSet<string>(refTypes.Select(t => t.ShortName), StringComparer.Ordinal);
 74152                foreach (var type in refTypes)
 153                {
 48154                    foreach (var dep in type.Dependencies)
 155                    {
 3156                        var depShort = GeneratorHelpers.GetShortTypeName(dep);
 3157                        var matchingType = refTypes.FirstOrDefault(t =>
 7158                            t.ShortName == depShort ||
 11159                            t.Interfaces.Any(i => GeneratorHelpers.GetShortTypeName(i) == depShort));
 3160                        if (matchingType.FullName != null)
 161                        {
 3162                            sb.AppendLine($"    {GeneratorHelpers.GetMermaidNodeId(type.FullName)} --> {GeneratorHelpers
 163                        }
 164                    }
 165                }
 166
 167                // Show factory→product edges (dotted arrows)
 37168                var factorySources = refTypes.Where(t => t.HasFactory).ToList();
 40169                foreach (var source in factorySources)
 170                {
 4171                    var factoryName = source.ShortName + "Factory";
 8172                    var matchingFactory = refTypes.FirstOrDefault(t => t.ShortName == factoryName);
 4173                    if (matchingFactory.FullName != null)
 174                    {
 0175                        sb.AppendLine($"    {GeneratorHelpers.GetMermaidNodeId(matchingFactory.FullName)} -.->|produces|
 176                    }
 177                }
 178
 16179                sb.AppendLine("```");
 16180                sb.AppendLine();
 181
 182                // Show summary table for this assembly
 16183                sb.AppendLine($"| Service | Lifetime | Interfaces |");
 16184                sb.AppendLine($"|---------|----------|------------|");
 95185                foreach (var type in refTypes.OrderBy(t => t.ShortName, StringComparer.Ordinal))
 186                {
 21187                    var interfaces = type.Interfaces.Any() ? string.Join(", ", type.Interfaces.Select(GeneratorHelpers.G
 21188                    sb.AppendLine($"| {type.ShortName} | {type.Lifetime} | {interfaces} |");
 189                }
 16190                sb.AppendLine();
 191            }
 192        }
 193
 54096194        var singletons = types.Where(t => t.Lifetime == GeneratorLifetime.Singleton).ToList();
 54096195        var scopeds = types.Where(t => t.Lifetime == GeneratorLifetime.Scoped).ToList();
 54096196        var transients = types.Where(t => t.Lifetime == GeneratorLifetime.Transient).ToList();
 197
 128198        sb.AppendLine("## Service Dependencies");
 128199        sb.AppendLine();
 128200        sb.AppendLine("```mermaid");
 128201        sb.AppendLine("graph TD");
 202
 203        // Emit subgraphs by lifetime
 128204        if (singletons.Any())
 205        {
 122206            sb.AppendLine("    subgraph Singleton");
 108178207            foreach (var type in singletons)
 53967208                sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.TypeName)}[\"{GeneratorHelpers.GetShortT
 122209            sb.AppendLine("    end");
 210        }
 128211        if (scopeds.Any())
 212        {
 1213            sb.AppendLine("    subgraph Scoped");
 4214            foreach (var type in scopeds)
 1215                sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.TypeName)}[\"{GeneratorHelpers.GetShortT
 1216            sb.AppendLine("    end");
 217        }
 128218        if (transients.Any())
 219        {
 0220            sb.AppendLine("    subgraph Transient");
 0221            foreach (var type in transients)
 0222                sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.TypeName)}[\"{GeneratorHelpers.GetShortT
 0223            sb.AppendLine("    end");
 224        }
 225
 226        // Emit edges for dependencies
 54096227        var typeNames = new HashSet<string>(types.Select(t => GeneratorHelpers.GetShortTypeName(t.TypeName)), StringComp
 108192228        foreach (var type in types)
 229        {
 152190230            foreach (var dep in type.ConstructorParameterTypes)
 231            {
 22127232                var depShort = GeneratorHelpers.GetShortTypeName(dep);
 233                // Find if we have a type implementing this interface
 22127234                var matchingType = types.FirstOrDefault(t =>
 11229329235                    GeneratorHelpers.GetShortTypeName(t.TypeName) == depShort ||
 11252186236                    t.InterfaceNames.Any(i => GeneratorHelpers.GetShortTypeName(i) == depShort));
 237
 22127238                if (matchingType.TypeName != null)
 7263239                    sb.AppendLine($"    {GeneratorHelpers.GetMermaidNodeId(type.TypeName)} --> {GeneratorHelpers.GetMerm
 240            }
 241        }
 242
 128243        sb.AppendLine("```");
 128244        sb.AppendLine();
 245
 246        // Decorator chains section (aggregates host and plugin decorators)
 128247        var pluginDecorators = referencedAssemblyTypes
 40248            .SelectMany(kv => kv.Value.Where(t => t.IsDecorator).Select(t => (Assembly: kv.Key, Type: t)))
 128249            .ToList();
 250
 128251        var decorators = FilterDecorators(discovery.Decorators, typeFilter);
 252
 128253        if (decorators.Any() || pluginDecorators.Any())
 254        {
 22255            sb.AppendLine("## Decorator Chains");
 22256            sb.AppendLine();
 22257            sb.AppendLine("```mermaid");
 22258            sb.AppendLine("graph LR");
 259
 260            // Host decorators - group by service type
 22261            var decoratorsByService = decorators
 33262                .GroupBy(d => d.ServiceTypeName)
 42263                .OrderBy(g => g.Key, StringComparer.Ordinal);
 264
 84265            foreach (var serviceGroup in decoratorsByService)
 266            {
 20267                var serviceShortName = GeneratorHelpers.GetShortTypeName(serviceGroup.Key);
 46268                var orderedDecorators = serviceGroup.OrderByDescending(d => d.Order).ToList();
 269
 270                // Find the underlying implementation for this service
 20271                var implementation = types.FirstOrDefault(t =>
 38272                    t.InterfaceNames.Any(i => GeneratorHelpers.GetShortTypeName(i) == serviceShortName) &&
 71273                    !decorators.Any(d => GeneratorHelpers.GetShortTypeName(d.DecoratorTypeName) == GeneratorHelpers.GetS
 274
 275                // Build the chain: highest order decorator -> ... -> lowest order decorator -> implementation
 106276                for (int i = 0; i < orderedDecorators.Count; i++)
 277                {
 33278                    var decorator = orderedDecorators[i];
 33279                    var decoratorId = GeneratorHelpers.GetMermaidNodeId(decorator.DecoratorTypeName);
 33280                    var decoratorName = GeneratorHelpers.GetShortTypeName(decorator.DecoratorTypeName);
 281
 282                    // Add node definition
 33283                    sb.AppendLine($"    {decoratorId}[[\"{decoratorName}\"]]");
 284
 285                    // Add edge to next decorator or implementation
 33286                    if (i < orderedDecorators.Count - 1)
 287                    {
 13288                        var nextDecorator = orderedDecorators[i + 1];
 13289                        sb.AppendLine($"    {decoratorId} --> {GeneratorHelpers.GetMermaidNodeId(nextDecorator.Decorator
 290                    }
 20291                    else if (implementation.TypeName != null)
 292                    {
 19293                        var implId = GeneratorHelpers.GetMermaidNodeId(implementation.TypeName);
 19294                        var implName = GeneratorHelpers.GetShortTypeName(implementation.TypeName);
 19295                        sb.AppendLine($"    {implId}[\"{implName}\"]");
 19296                        sb.AppendLine($"    {decoratorId} --> {implId}");
 297                    }
 298                }
 299            }
 300
 301            // Plugin decorators (we don't have chain order info, just show the decorator types)
 53302            foreach (var (assembly, type) in pluginDecorators.OrderBy(x => x.Type.ShortName, StringComparer.Ordinal))
 303            {
 3304                var decoratorId = GeneratorHelpers.GetMermaidNodeId(type.FullName);
 3305                var decoratorName = type.ShortName;
 3306                sb.AppendLine($"    {decoratorId}[[\"{decoratorName}\"]]");
 307            }
 308
 22309            sb.AppendLine("```");
 22310            sb.AppendLine();
 311        }
 312
 313        // Intercepted services section (aggregates host and plugin interceptors)
 128314        var interceptedServices = FilterInterceptedServices(discovery.InterceptedServices, typeFilter);
 128315        var pluginInterceptors = referencedAssemblyTypes
 40316            .SelectMany(kv => kv.Value.Where(t => t.HasInterceptorProxy).Select(t => (Assembly: kv.Key, Type: t)))
 128317            .ToList();
 318
 128319        if (interceptedServices.Any() || pluginInterceptors.Any())
 320        {
 4321            sb.AppendLine("## Intercepted Services");
 4322            sb.AppendLine();
 4323            sb.AppendLine("```mermaid");
 4324            sb.AppendLine("graph LR");
 325
 326            // Host intercepted services
 14327            foreach (var service in interceptedServices.OrderBy(s => s.TypeName, StringComparer.Ordinal))
 328            {
 2329                var targetId = GeneratorHelpers.GetMermaidNodeId(service.TypeName);
 2330                var targetName = GeneratorHelpers.GetShortTypeName(service.TypeName);
 2331                var proxyId = targetId + "_Proxy";
 2332                var proxyName = targetName + "_InterceptorProxy";
 333
 334                // Target service
 2335                sb.AppendLine($"    {targetId}[\"{targetName}\"]");
 336                // Proxy with stadium shape
 2337                sb.AppendLine($"    {proxyId}[[\"{proxyName}\"]]");
 338                // Edge showing proxy wraps target
 2339                sb.AppendLine($"    {proxyId} -.->|wraps| {targetId}");
 340
 341                // Show interceptors applied
 8342                foreach (var interceptorType in service.AllInterceptorTypeNames)
 343                {
 2344                    var interceptorId = GeneratorHelpers.GetMermaidNodeId(interceptorType);
 2345                    var interceptorName = GeneratorHelpers.GetShortTypeName(interceptorType);
 2346                    sb.AppendLine($"    {interceptorId}([[\"{interceptorName}\"]])");
 2347                    sb.AppendLine($"    {proxyId} --> {interceptorId}");
 348                }
 349            }
 350
 351            // Plugin intercepted services
 17352            foreach (var (assembly, type) in pluginInterceptors.OrderBy(x => x.Type.ShortName, StringComparer.Ordinal))
 353            {
 3354                var targetId = GeneratorHelpers.GetMermaidNodeId(type.FullName);
 3355                var targetName = type.ShortName;
 3356                var proxyId = targetId + "_Proxy";
 3357                var proxyName = targetName + "_InterceptorProxy";
 358
 3359                sb.AppendLine($"    {targetId}[\"{targetName}\"]");
 3360                sb.AppendLine($"    {proxyId}[[\"{proxyName}\"]]");
 3361                sb.AppendLine($"    {proxyId} -.->|wraps| {targetId}");
 362            }
 363
 4364            sb.AppendLine("```");
 4365            sb.AppendLine();
 366        }
 367
 368        // Keyed services section
 54096369        var keyedTypes = types.Where(t => t.IsKeyed).ToList();
 128370        if (keyedTypes.Any())
 371        {
 4372            sb.AppendLine("## Keyed Services");
 4373            sb.AppendLine();
 4374            sb.AppendLine("```mermaid");
 4375            sb.AppendLine("graph TD");
 376
 377            // Group by service key
 4378            var typesByKey = keyedTypes
 14379                .SelectMany(t => t.ServiceKeys.Select(k => (Key: k, Type: t)))
 7380                .GroupBy(x => x.Key)
 10381                .OrderBy(g => g.Key, StringComparer.Ordinal);
 382
 20383            foreach (var keyGroup in typesByKey)
 384            {
 6385                var safeKey = GeneratorHelpers.SanitizeMermaidId(keyGroup.Key);
 6386                sb.AppendLine($"    subgraph key_{safeKey}[\"{GeneratorHelpers.EscapeMermaidLabel(keyGroup.Key)}\"]");
 33387                foreach (var item in keyGroup.OrderBy(x => x.Type.TypeName, StringComparer.Ordinal))
 388                {
 7389                    var nodeId = GeneratorHelpers.GetMermaidNodeId(item.Type.TypeName);
 7390                    var nodeName = GeneratorHelpers.GetShortTypeName(item.Type.TypeName);
 7391                    sb.AppendLine($"        {nodeId}[\"{nodeName}\"]");
 392                }
 6393                sb.AppendLine("    end");
 394            }
 395
 4396            sb.AppendLine("```");
 4397            sb.AppendLine();
 398        }
 399
 400        // Plugin assemblies section
 128401        var plugins = FilterPluginTypes(discovery.PluginTypes, typeFilter);
 128402        if (plugins.Any())
 403        {
 110404            sb.AppendLine("## Plugin Assemblies");
 110405            sb.AppendLine();
 110406            sb.AppendLine("```mermaid");
 110407            sb.AppendLine("graph TD");
 408
 409            // Group plugins by assembly
 110410            var pluginsByAssembly = plugins
 396411                .GroupBy(p => p.AssemblyName)
 294412                .OrderBy(g => g.Key, StringComparer.Ordinal);
 413
 588414            foreach (var asmGroup in pluginsByAssembly)
 415            {
 184416                var safeAsm = GeneratorHelpers.SanitizeMermaidId(asmGroup.Key);
 184417                var shortAsm = GeneratorHelpers.GetShortTypeName(asmGroup.Key);
 184418                sb.AppendLine($"    subgraph asm_{safeAsm}[\"{GeneratorHelpers.EscapeMermaidLabel(shortAsm)}\"]");
 1556419                foreach (var plugin in asmGroup.OrderBy(p => p.TypeName, StringComparer.Ordinal))
 420                {
 396421                    var nodeId = GeneratorHelpers.GetMermaidNodeId(plugin.TypeName);
 396422                    var nodeName = GeneratorHelpers.GetShortTypeName(plugin.TypeName);
 423                    // Use stadium shape for plugins
 396424                    sb.AppendLine($"        {nodeId}([\"{nodeName}\"])");
 425                }
 184426                sb.AppendLine("    end");
 427            }
 428
 110429            sb.AppendLine("```");
 110430            sb.AppendLine();
 431        }
 432
 433        // Factory services section (aggregates host and plugin factories)
 128434        var factories = FilterFactories(discovery.Factories, typeFilter);
 128435        var pluginFactories = referencedAssemblyTypes
 41436            .SelectMany(kv => kv.Value.Where(t => t.HasFactory).Select(t => (Assembly: kv.Key, Type: t)))
 128437            .ToList();
 438
 128439        if (factories.Any() || pluginFactories.Any())
 440        {
 10441            sb.AppendLine("## Factory Services");
 10442            sb.AppendLine();
 10443            sb.AppendLine("```mermaid");
 10444            sb.AppendLine("graph LR");
 445
 446            // Host factories
 41447            foreach (var factory in factories.OrderBy(f => f.TypeName, StringComparer.Ordinal))
 448            {
 7449                var sourceNodeId = GeneratorHelpers.GetMermaidNodeId(factory.TypeName);
 7450                var sourceName = GeneratorHelpers.GetShortTypeName(factory.TypeName);
 7451                var factoryNodeId = sourceNodeId + "Factory";
 7452                var factoryName = sourceName + "Factory";
 453
 454                // Generated factory with regular shape
 7455                sb.AppendLine($"    {factoryNodeId}[\"{factoryName}\"]");
 456                // Source type (product) with hexagon shape
 7457                sb.AppendLine($"    {sourceNodeId}{{{{\"{sourceName}\"}}}}");
 458                // Edge showing factory produces the type (dotted arrow)
 7459                sb.AppendLine($"    {factoryNodeId} -.->|produces| {sourceNodeId}");
 460            }
 461
 462            // Plugin factories
 32463            foreach (var (assembly, type) in pluginFactories.OrderBy(f => f.Type.ShortName, StringComparer.Ordinal))
 464            {
 4465                var sourceNodeId = GeneratorHelpers.GetMermaidNodeId(type.FullName);
 4466                var sourceName = type.ShortName;
 4467                var factoryNodeId = sourceNodeId + "Factory";
 4468                var factoryName = sourceName + "Factory";
 469
 470                // Generated factory with regular shape
 4471                sb.AppendLine($"    {factoryNodeId}[\"{factoryName}\"]");
 472                // Source type (product) with hexagon shape
 4473                sb.AppendLine($"    {sourceNodeId}{{{{\"{sourceName}\"}}}}");
 474                // Edge showing factory produces the type (dotted arrow)
 4475                sb.AppendLine($"    {factoryNodeId} -.->|produces| {sourceNodeId}");
 476            }
 477
 10478            sb.AppendLine("```");
 10479            sb.AppendLine();
 480        }
 481
 482        // Interface mapping section
 54096483        var typesWithInterfaces = types.Where(t => t.InterfaceNames.Length > 0).ToList();
 128484        if (typesWithInterfaces.Any())
 485        {
 99486            sb.AppendLine("## Interface Mapping");
 99487            sb.AppendLine();
 99488            sb.AppendLine("```mermaid");
 99489            sb.AppendLine("graph LR");
 490
 627491            foreach (var type in typesWithInterfaces.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 492            {
 143493                var implId = GeneratorHelpers.GetMermaidNodeId(type.TypeName);
 143494                var implName = GeneratorHelpers.GetShortTypeName(type.TypeName);
 143495                sb.AppendLine($"    {implId}[\"{implName}\"]");
 496
 572497                foreach (var iface in type.InterfaceNames)
 498                {
 143499                    var ifaceId = GeneratorHelpers.GetMermaidNodeId(iface);
 143500                    var ifaceName = GeneratorHelpers.GetShortTypeName(iface);
 501                    // Interface uses rounded box, dotted edge points from interface to impl
 143502                    sb.AppendLine($"    {ifaceId}((\"{ifaceName}\"))");
 143503                    sb.AppendLine($"    {ifaceId} -.-> {implId}");
 504                }
 505            }
 506
 99507            sb.AppendLine("```");
 99508            sb.AppendLine();
 509        }
 510
 511        // Complexity metrics section
 128512        sb.AppendLine("## Complexity Metrics");
 128513        sb.AppendLine();
 128514        sb.AppendLine("| Metric | Value |");
 128515        sb.AppendLine("|--------|-------|");
 516
 128517        sb.AppendLine($"| Total Services | {GeneratorHelpers.Literal(types.Count)} |");
 518
 519        // Calculate max dependency depth using BFS
 128520        var maxDepth = CalculateMaxDependencyDepth(types);
 128521        sb.AppendLine($"| Max Dependency Depth | {maxDepth} |");
 522
 523        // Calculate hub services (services that appear as dependencies in 3+ other services)
 128524        var hubServices = CalculateHubServices(types, 3);
 128525        sb.AppendLine($"| Hub Services (≥3 dependents) | {GeneratorHelpers.Literal(hubServices.Count)} |");
 526
 128527        if (hubServices.Any())
 528        {
 90529            sb.AppendLine();
 982530            sb.AppendLine("**Hub Services:** " + string.Join(", ", hubServices.Select(h => $"{GeneratorHelpers.GetShortT
 531        }
 532
 128533        sb.AppendLine();
 534
 535        // Dependency details table
 128536        sb.AppendLine("## Dependency Details");
 128537        sb.AppendLine();
 128538        sb.AppendLine("| Service | Lifetime | Dependencies |");
 128539        sb.AppendLine("|---------|----------|--------------|");
 540
 162160541        foreach (var type in types.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 542        {
 53968543            var deps = type.ConstructorParameterTypes.Any()
 53968544                ? string.Join(", ", type.ConstructorParameterTypes.Select(GeneratorHelpers.GetShortTypeName))
 53968545                : "-";
 53968546            sb.AppendLine($"| {GeneratorHelpers.GetShortTypeName(type.TypeName)} | {type.Lifetime} | {deps} |");
 547        }
 548
 128549        return sb.ToString();
 550    }
 551
 552    internal static string GenerateLifetimeSummaryMarkdown(DiscoveryResult discovery, string assemblyName, string timest
 553    {
 116554        var sb = new StringBuilder();
 116555        var types = FilterTypes(discovery.InjectableTypes, typeFilter);
 116556        referencedAssemblyTypes = FilterReferencedAssemblyTypes(referencedAssemblyTypes, typeFilter);
 557
 54055558        var singletons = types.Where(t => t.Lifetime == GeneratorLifetime.Singleton).ToList();
 54055559        var scopeds = types.Where(t => t.Lifetime == GeneratorLifetime.Scoped).ToList();
 54055560        var transients = types.Where(t => t.Lifetime == GeneratorLifetime.Transient).ToList();
 116561        var total = types.Count;
 562
 116563        sb.AppendLine("# Needlr Lifetime Summary");
 116564        sb.AppendLine();
 116565        sb.AppendLine($"Generated: {timestamp} UTC");
 116566        sb.AppendLine($"Assembly: {assemblyName}");
 116567        sb.AppendLine();
 568
 569        // Referenced plugin assemblies lifetime breakdown
 116570        if (referencedAssemblyTypes.Count > 0)
 571        {
 14572            sb.AppendLine("## Referenced Plugin Assemblies");
 14573            sb.AppendLine();
 574
 73575            foreach (var kvp in referencedAssemblyTypes.OrderBy(kv => kv.Key, StringComparer.Ordinal))
 576            {
 15577                var refAsm = kvp.Key;
 15578                var refTypes = kvp.Value;
 35579                var refSingletons = refTypes.Count(t => t.Lifetime == GeneratorLifetime.Singleton);
 35580                var refScopeds = refTypes.Count(t => t.Lifetime == GeneratorLifetime.Scoped);
 35581                var refTransients = refTypes.Count(t => t.Lifetime == GeneratorLifetime.Transient);
 15582                var refTotal = refTypes.Count;
 583
 15584                sb.AppendLine($"### {refAsm}");
 15585                sb.AppendLine();
 15586                sb.AppendLine("| Lifetime | Count | % |");
 15587                sb.AppendLine("|----------|-------|---|");
 15588                sb.AppendLine($"| Singleton | {refSingletons} | {GeneratorHelpers.Percentage(refSingletons, refTotal)}% 
 15589                sb.AppendLine($"| Scoped | {refScopeds} | {GeneratorHelpers.Percentage(refScopeds, refTotal)}% |");
 15590                sb.AppendLine($"| Transient | {refTransients} | {GeneratorHelpers.Percentage(refTransients, refTotal)}% 
 15591                sb.AppendLine($"| **Total** | **{refTotal}** | 100% |");
 15592                sb.AppendLine();
 593            }
 594        }
 595
 116596        sb.AppendLine("## Registration Counts");
 116597        sb.AppendLine();
 116598        sb.AppendLine("| Lifetime | Count | % |");
 116599        sb.AppendLine("|----------|-------|---|");
 600
 116601        if (total > 0)
 602        {
 112603            sb.AppendLine($"| Singleton | {GeneratorHelpers.Literal(singletons.Count)} | {GeneratorHelpers.Percentage(si
 112604            sb.AppendLine($"| Scoped | {GeneratorHelpers.Literal(scopeds.Count)} | {GeneratorHelpers.Percentage(scopeds.
 112605            sb.AppendLine($"| Transient | {GeneratorHelpers.Literal(transients.Count)} | {GeneratorHelpers.Percentage(tr
 112606            sb.AppendLine($"| **Total** | **{total}** | 100% |");
 607        }
 608        else
 609        {
 4610            sb.AppendLine("| (none) | 0 | - |");
 611        }
 612
 116613        sb.AppendLine();
 614
 615        // List by category
 116616        if (singletons.Any())
 617        {
 112618            sb.AppendLine($"## Singleton ({GeneratorHelpers.Literal(singletons.Count)})");
 112619            sb.AppendLine();
 162041620            foreach (var type in singletons.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 53939621                sb.AppendLine($"- {GeneratorHelpers.GetShortTypeName(type.TypeName)}");
 112622            sb.AppendLine();
 623        }
 624
 116625        if (scopeds.Any())
 626        {
 0627            sb.AppendLine($"## Scoped ({GeneratorHelpers.Literal(scopeds.Count)})");
 0628            sb.AppendLine();
 0629            foreach (var type in scopeds.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 0630                sb.AppendLine($"- {GeneratorHelpers.GetShortTypeName(type.TypeName)}");
 0631            sb.AppendLine();
 632        }
 633
 116634        if (transients.Any())
 635        {
 0636            sb.AppendLine($"## Transient ({GeneratorHelpers.Literal(transients.Count)})");
 0637            sb.AppendLine();
 0638            foreach (var type in transients.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 0639                sb.AppendLine($"- {GeneratorHelpers.GetShortTypeName(type.TypeName)}");
 0640            sb.AppendLine();
 641        }
 642
 116643        return sb.ToString();
 644    }
 645
 646    internal static string GenerateRegistrationIndexMarkdown(DiscoveryResult discovery, string assemblyName, string? pro
 647    {
 144648        var sb = new StringBuilder();
 144649        var types = FilterTypes(discovery.InjectableTypes, typeFilter);
 144650        var plugins = FilterPluginTypes(discovery.PluginTypes, typeFilter);
 144651        var decorators = FilterDecorators(discovery.Decorators, typeFilter);
 144652        referencedAssemblyTypes = FilterReferencedAssemblyTypes(referencedAssemblyTypes, typeFilter);
 653
 144654        sb.AppendLine("# Needlr Registration Index");
 144655        sb.AppendLine();
 144656        sb.AppendLine($"Generated: {timestamp} UTC");
 144657        sb.AppendLine($"Assembly: {assemblyName}");
 144658        sb.AppendLine();
 659
 660        // Referenced plugin assemblies services
 144661        if (referencedAssemblyTypes.Count > 0)
 662        {
 16663            sb.AppendLine("## Referenced Plugin Assemblies");
 16664            sb.AppendLine();
 665
 83666            foreach (var kvp in referencedAssemblyTypes.OrderBy(kv => kv.Key, StringComparer.Ordinal))
 667            {
 17668                var refAsm = kvp.Key;
 17669                var refTypes = kvp.Value;
 670
 17671                sb.AppendLine($"### {refAsm} ({GeneratorHelpers.Literal(refTypes.Count)} services)");
 17672                sb.AppendLine();
 17673                sb.AppendLine("| # | Interface | Implementation | Lifetime |");
 17674                sb.AppendLine("|---|-----------|----------------|----------|");
 675
 17676                var index = 1;
 97677                foreach (var type in refTypes.OrderBy(t => t.ShortName, StringComparer.Ordinal))
 678                {
 21679                    var iface = type.Interfaces.FirstOrDefault() ?? "-";
 21680                    sb.AppendLine($"| {index} | {GeneratorHelpers.GetShortTypeName(iface)} | {type.ShortName} | {type.Li
 21681                    index++;
 682                }
 17683                sb.AppendLine();
 684            }
 685        }
 686
 687        // Services table
 144688        sb.AppendLine($"## Services ({GeneratorHelpers.Literal(types.Count)})");
 144689        sb.AppendLine();
 690
 144691        if (types.Any())
 692        {
 121693            sb.AppendLine("| # | Interface | Implementation | Lifetime | Source |");
 121694            sb.AppendLine("|---|-----------|----------------|----------|--------|");
 695
 121696            var index = 1;
 162089697            foreach (var type in types.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 698            {
 53949699                var iface = type.InterfaceNames.FirstOrDefault() ?? "-";
 53949700                var source = BreadcrumbWriter.GetRelativeSourcePath(type.SourceFilePath, projectDirectory);
 53949701                sb.AppendLine($"| {index} | {GeneratorHelpers.GetShortTypeName(iface)} | {GeneratorHelpers.GetShortTypeN
 53949702                index++;
 703            }
 121704            sb.AppendLine();
 705        }
 706        else
 707        {
 23708            sb.AppendLine("No injectable services discovered.");
 23709            sb.AppendLine();
 710        }
 711
 712        // Decorators section (aggregates host and plugin decorators)
 144713        var pluginDecorators = referencedAssemblyTypes
 43714            .SelectMany(kv => kv.Value.Where(t => t.IsDecorator).Select(t => (Assembly: kv.Key, Type: t)))
 144715            .ToList();
 716
 144717        if (decorators.Any() || pluginDecorators.Any())
 718        {
 28719            var totalCount = decorators.Count + pluginDecorators.Count;
 28720            sb.AppendLine($"## Decorators ({totalCount})");
 28721            sb.AppendLine();
 28722            sb.AppendLine("| Service | Decorator Chain | Assembly |");
 28723            sb.AppendLine("|---------|-----------------|----------|");
 724
 725            // Host decorators
 28726            var decoratorsByTarget = decorators
 37727                .GroupBy(d => d.ServiceTypeName)
 51728                .OrderBy(g => g.Key, StringComparer.Ordinal);
 729
 102730            foreach (var group in decoratorsByTarget)
 731            {
 23732                var chain = string.Join(" → ",
 37733                    group.OrderBy(d => d.Order)
 60734                         .Select(d => GeneratorHelpers.GetShortTypeName(d.DecoratorTypeName)));
 23735                sb.AppendLine($"| {GeneratorHelpers.GetShortTypeName(group.Key)} | {chain} | (host) |");
 736            }
 737
 738            // Plugin decorators
 71739            foreach (var (assembly, type) in pluginDecorators.OrderBy(x => x.Type.ShortName, StringComparer.Ordinal))
 740            {
 741                // For plugin decorators, we show them individually since we don't have chain info
 5742                var serviceName = type.Interfaces.FirstOrDefault() ?? "-";
 5743                sb.AppendLine($"| {GeneratorHelpers.GetShortTypeName(serviceName)} | {type.ShortName} | {assembly} |");
 744            }
 28745            sb.AppendLine();
 746        }
 747
 748        // Interceptors section (aggregates host and plugin interceptors)
 144749        var interceptedServices = FilterInterceptedServices(discovery.InterceptedServices, typeFilter);
 144750        var pluginIntercepted = referencedAssemblyTypes
 43751            .SelectMany(kv => kv.Value.Where(t => t.HasInterceptorProxy).Select(t => (Assembly: kv.Key, Type: t)))
 144752            .ToList();
 753
 144754        if (interceptedServices.Any() || pluginIntercepted.Any())
 755        {
 12756            var totalCount = interceptedServices.Count + pluginIntercepted.Count;
 12757            sb.AppendLine($"## Intercepted Services ({totalCount})");
 12758            sb.AppendLine();
 12759            sb.AppendLine("| Service | Interceptors | Proxy | Assembly |");
 12760            sb.AppendLine("|---------|--------------|-------|----------|");
 761
 762            // Host intercepted services
 45763            foreach (var service in interceptedServices.OrderBy(s => s.TypeName, StringComparer.Ordinal))
 764            {
 7765                var serviceName = GeneratorHelpers.GetShortTypeName(service.TypeName);
 7766                var interceptors = string.Join(", ", service.AllInterceptorTypeNames.Select(GeneratorHelpers.GetShortTyp
 7767                var proxyName = serviceName + "_InterceptorProxy";
 7768                sb.AppendLine($"| {serviceName} | {interceptors} | {proxyName} | (host) |");
 769            }
 770
 771            // Plugin intercepted services
 39772            foreach (var (assembly, type) in pluginIntercepted.OrderBy(x => x.Type.ShortName, StringComparer.Ordinal))
 773            {
 5774                var proxyName = type.ShortName + "_InterceptorProxy";
 5775                sb.AppendLine($"| {type.ShortName} | (see plugin) | {proxyName} | {assembly} |");
 776            }
 12777            sb.AppendLine();
 778        }
 779
 780        // Factories section (aggregates host and plugin factories)
 144781        var factories = FilterFactories(discovery.Factories, typeFilter);
 144782        var pluginFactories = referencedAssemblyTypes
 44783            .SelectMany(kv => kv.Value.Where(t => t.HasFactory).Select(t => (Assembly: kv.Key, Type: t)))
 144784            .ToList();
 785
 144786        if (factories.Any() || pluginFactories.Any())
 787        {
 17788            var totalCount = factories.Count + pluginFactories.Count;
 17789            sb.AppendLine($"## Factories ({totalCount})");
 17790            sb.AppendLine();
 17791            sb.AppendLine("| Source Type | Factory Interface | Generated Factory | Assembly |");
 17792            sb.AppendLine("|-------------|-------------------|-------------------|----------|");
 793
 794            // Host factories
 67795            foreach (var factory in factories.OrderBy(f => f.TypeName, StringComparer.Ordinal))
 796            {
 11797                var sourceName = factory.SimpleTypeName;
 11798                var factoryInterface = "I" + sourceName + "Factory";
 11799                var factoryImpl = sourceName + "Factory";
 11800                sb.AppendLine($"| {sourceName} | {factoryInterface} | {factoryImpl} | (host) |");
 801            }
 802
 803            // Plugin factories
 52804            foreach (var (assembly, type) in pluginFactories.OrderBy(x => x.Type.ShortName, StringComparer.Ordinal))
 805            {
 6806                var factoryInterface = "I" + type.ShortName + "Factory";
 6807                var factoryImpl = type.ShortName + "Factory";
 6808                sb.AppendLine($"| {type.ShortName} | {factoryInterface} | {factoryImpl} | {assembly} |");
 809            }
 17810            sb.AppendLine();
 811        }
 812
 813        // Plugins section
 144814        if (plugins.Any())
 815        {
 905816            var orderedPlugins = plugins.OrderBy(p => p.Order).ThenBy(p => p.TypeName, StringComparer.Ordinal);
 817
 111818            sb.AppendLine($"## Plugins ({GeneratorHelpers.Literal(plugins.Count)})");
 111819            sb.AppendLine();
 111820            sb.AppendLine("| Order | Plugin | Interfaces |");
 111821            sb.AppendLine("|-------|--------|------------|");
 822
 1016823            foreach (var plugin in orderedPlugins)
 824            {
 397825                var interfaces = string.Join(", ", plugin.InterfaceNames.Select(GeneratorHelpers.GetShortTypeName));
 397826                sb.AppendLine($"| {GeneratorHelpers.Literal(plugin.Order)} | {GeneratorHelpers.GetShortTypeName(plugin.T
 827            }
 111828            sb.AppendLine();
 829        }
 830
 831        // Keyed Services section
 54093832        var keyedTypes = types.Where(t => t.ServiceKeys.Length > 0).ToList();
 144833        if (keyedTypes.Any())
 834        {
 11835            sb.AppendLine($"## Keyed Services ({keyedTypes.Sum(t => t.ServiceKeys.Length)})");
 4836            sb.AppendLine();
 4837            sb.AppendLine("| Key | Interface | Implementation | Lifetime |");
 4838            sb.AppendLine("|-----|-----------|----------------|----------|");
 839
 29840            foreach (var type in keyedTypes.OrderBy(t => t.TypeName, StringComparer.Ordinal))
 841            {
 30842                foreach (var key in type.ServiceKeys)
 843                {
 8844                    var iface = type.InterfaceNames.FirstOrDefault() ?? "-";
 8845                    sb.AppendLine($"| `\"{GeneratorHelpers.EscapeMarkdownTableCell(key)}\"` | {GeneratorHelpers.GetShort
 846                }
 847            }
 4848            sb.AppendLine();
 849        }
 850
 144851        return sb.ToString();
 852    }
 853
 854    internal static string GenerateAnalyzerStatusMarkdown(string timestamp)
 855    {
 109856        var sb = new StringBuilder();
 857
 109858        sb.AppendLine("# Needlr Analyzer Status");
 109859        sb.AppendLine();
 109860        sb.AppendLine($"Generated: {timestamp} UTC");
 109861        sb.AppendLine();
 109862        sb.AppendLine("## Active Analyzers");
 109863        sb.AppendLine();
 109864        sb.AppendLine("| ID | Name | Status | Default Severity | Description |");
 109865        sb.AppendLine("|:---|:-----|:-------|:-----------------|:------------|");
 109866        sb.AppendLine("| NDLRCOR001 | Reflection in AOT | ⚪ Conditional | Error | Detects reflection APIs in AOT project
 109867        sb.AppendLine("| NDLRCOR002 | Plugin Constructor | ✅ Active | Warning | Plugins should have parameterless constr
 109868        sb.AppendLine("| NDLRCOR003 | DeferToContainer in Generated | ✅ Active | Error | [DeferToContainer] must be on u
 109869        sb.AppendLine("| NDLRCOR004 | Global Namespace Type | ✅ Active | Warning | Types in global namespace may not be 
 109870        sb.AppendLine("| NDLRCOR005 | Lifetime Mismatch | ✅ Active | Warning | Detects captive dependencies |");
 109871        sb.AppendLine("| NDLRCOR006 | Circular Dependency | ✅ Active | Error | Detects circular service dependencies |")
 109872        sb.AppendLine("| NDLRCOR007 | Intercept Type | ✅ Active | Error | Intercept type must implement IMethodIntercept
 109873        sb.AppendLine("| NDLRCOR008 | Intercept Without Interface | ✅ Active | Warning | [Intercept] requires interface-
 109874        sb.AppendLine("| NDLRCOR009 | Lazy Resolution | ✅ Active | Info | Lazy<T> references undiscovered type |");
 109875        sb.AppendLine("| NDLRCOR010 | Collection Resolution | ✅ Active | Info | IEnumerable<T> has no implementations |"
 109876        sb.AppendLine("| NDLRCOR011 | Keyed Service Usage | ✅ Active | Info | Tracks [FromKeyedServices] parameter usage
 109877        sb.AppendLine();
 109878        sb.AppendLine("## Mode");
 109879        sb.AppendLine();
 109880        sb.AppendLine("**Source Generation**: Enabled (GenerateTypeRegistry detected)");
 109881        sb.AppendLine();
 109882        sb.AppendLine("## Configuration");
 109883        sb.AppendLine();
 109884        sb.AppendLine("Analyzer severity can be configured via `.editorconfig`:");
 109885        sb.AppendLine();
 109886        sb.AppendLine("```ini");
 109887        sb.AppendLine("# Example: Suppress Lazy resolution warnings");
 109888        sb.AppendLine("dotnet_diagnostic.NDLRCOR009.severity = none");
 109889        sb.AppendLine();
 109890        sb.AppendLine("# Example: Promote to warning");
 109891        sb.AppendLine("dotnet_diagnostic.NDLRCOR009.severity = warning");
 109892        sb.AppendLine("```");
 109893        sb.AppendLine();
 894
 109895        return sb.ToString();
 896    }
 897
 898    internal static IReadOnlyList<DiscoveredType> FilterTypes(IReadOnlyList<DiscoveredType> types, HashSet<string> filte
 899    {
 388900        if (IsUnfiltered(filter))
 325901            return types;
 902
 63903        return types.Where(t =>
 18213904            MatchesTypeName(filter, t.TypeName) ||
 18213905            MatchesAnyTypeName(filter, t.InterfaceNames))
 63906                    .ToList();
 907    }
 908
 909    internal static IReadOnlyList<DiscoveredPlugin> FilterPluginTypes(IReadOnlyList<DiscoveredPlugin> plugins, HashSet<s
 910    {
 272911        if (IsUnfiltered(filter))
 221912            return plugins;
 913
 51914        return plugins.Where(p =>
 112915            MatchesTypeName(filter, p.TypeName) ||
 112916            MatchesAnyTypeName(filter, p.InterfaceNames))
 51917                      .ToList();
 918    }
 919
 920    internal static IReadOnlyList<DiscoveredDecorator> FilterDecorators(IReadOnlyList<DiscoveredDecorator> decorators, H
 921    {
 272922        if (IsUnfiltered(filter))
 221923            return decorators;
 924
 51925        return decorators.Where(d =>
 11926            MatchesTypeName(filter, d.DecoratorTypeName) ||
 11927            MatchesTypeName(filter, d.ServiceTypeName))
 51928                         .ToList();
 929    }
 930
 931    internal static IReadOnlyList<DiscoveredFactory> FilterFactories(IReadOnlyList<DiscoveredFactory> factories, HashSet
 932    {
 272933        if (IsUnfiltered(filter))
 221934            return factories;
 935
 51936        return factories.Where(f =>
 12937            MatchesTypeName(filter, f.TypeName) ||
 12938            MatchesTypeName(filter, f.ReturnTypeName) ||
 12939            MatchesAnyTypeName(filter, f.InterfaceNames))
 51940                        .ToList();
 941    }
 942
 943    internal static IReadOnlyList<DiscoveredInterceptedService> FilterInterceptedServices(IReadOnlyList<DiscoveredInterc
 944    {
 272945        if (IsUnfiltered(filter))
 221946            return interceptedServices.ToList();
 947
 51948        return interceptedServices.Where(s =>
 10949            MatchesTypeName(filter, s.TypeName) ||
 10950            MatchesAnyTypeName(filter, s.InterfaceNames) ||
 10951            MatchesAnyTypeName(filter, s.AllInterceptorTypeNames))
 51952                                  .ToList();
 953    }
 954
 955    /// <summary>
 956    /// Applies the diagnostic type filter to types contributed by referenced plugin assemblies.
 957    /// Assemblies without any matching type are dropped so empty sections are not rendered.
 958    /// </summary>
 959    internal static Dictionary<string, List<DiagnosticTypeInfo>> FilterReferencedAssemblyTypes(Dictionary<string, List<D
 960    {
 388961        var filtered = new Dictionary<string, List<DiagnosticTypeInfo>>(StringComparer.Ordinal);
 884962        foreach (var kvp in referencedAssemblyTypes)
 963        {
 54964            var matching = IsUnfiltered(filter)
 54965                ? kvp.Value
 54966                : kvp.Value.Where(t =>
 9967                    MatchesTypeName(filter, t.FullName) ||
 9968                    filter.Contains(t.ShortName) ||
 9969                    MatchesAnyTypeName(filter, t.Interfaces))
 54970                    .ToList();
 971
 54972            if (matching.Count > 0)
 48973                filtered[kvp.Key] = matching;
 974        }
 975
 388976        return filtered;
 977    }
 978
 979    internal static int CalculateMaxDependencyDepth(IReadOnlyList<DiscoveredType> types)
 980    {
 134981        if (types.Count == 0) return 0;
 982
 983        // Build a lookup from interface/type name to the type that provides it
 122984        var providerLookup = new Dictionary<string, DiscoveredType>(StringComparer.Ordinal);
 108180985        foreach (var type in types)
 986        {
 53968987            providerLookup[GeneratorHelpers.GetShortTypeName(type.TypeName)] = type;
 108222988            foreach (var iface in type.InterfaceNames)
 143989                providerLookup[GeneratorHelpers.GetShortTypeName(iface)] = type;
 990        }
 991
 992        // Calculate depth for each type using memoization
 122993        var depthCache = new Dictionary<string, int>(StringComparer.Ordinal);
 122994        int maxDepth = 0;
 995
 108180996        foreach (var type in types)
 997        {
 53968998            var depth = GetDepth(type, providerLookup, depthCache, new HashSet<string>());
 54269999            if (depth > maxDepth) maxDepth = depth;
 1000        }
 1001
 1221002        return maxDepth;
 1003    }
 1004
 1005    internal static int GetDepth(DiscoveredType type, Dictionary<string, DiscoveredType> providerLookup, Dictionary<stri
 1006    {
 612311007        var key = GeneratorHelpers.GetShortTypeName(type.TypeName);
 1008
 612311009        if (cache.TryGetValue(key, out var cached))
 70841010            return cached;
 1011
 1012        // Cycle detection
 541471013        if (!visiting.Add(key))
 1791014            return 0;
 1015
 539681016        int maxChildDepth = 0;
 1521901017        foreach (var dep in type.ConstructorParameterTypes)
 1018        {
 221271019            var depShort = GeneratorHelpers.GetShortTypeName(dep);
 221271020            if (providerLookup.TryGetValue(depShort, out var depType))
 1021            {
 72631022                var childDepth = GetDepth(depType, providerLookup, cache, visiting);
 72631023                if (childDepth > maxChildDepth)
 62831024                    maxChildDepth = childDepth;
 1025            }
 1026        }
 1027
 539681028        visiting.Remove(key);
 539681029        var result = maxChildDepth + 1;
 539681030        cache[key] = result;
 539681031        return result;
 1032    }
 1033
 1034    internal static List<(string TypeName, int DependentCount)> CalculateHubServices(IReadOnlyList<DiscoveredType> types
 1035    {
 1036        // Count how many times each type/interface appears as a dependency
 1281037        var dependentCounts = new Dictionary<string, int>(StringComparer.Ordinal);
 1038
 1081921039        foreach (var type in types)
 1040        {
 1521901041            foreach (var dep in type.ConstructorParameterTypes)
 1042            {
 221271043                var depShort = GeneratorHelpers.GetShortTypeName(dep);
 221271044                if (!dependentCounts.ContainsKey(depShort))
 84011045                    dependentCounts[depShort] = 0;
 221271046                dependentCounts[depShort]++;
 1047            }
 1048        }
 1049
 1050        // Find types that are depended upon by minDependents or more services
 1281051        var hubs = new List<(string TypeName, int DependentCount)>();
 1081921052        foreach (var type in types)
 1053        {
 539681054            var shortName = GeneratorHelpers.GetShortTypeName(type.TypeName);
 539681055            var count = 0;
 1056
 1057            // Check if this type's name or any of its interfaces is depended upon
 539681058            if (dependentCounts.TryGetValue(shortName, out var c1))
 25921059                count += c1;
 1060
 1082221061            foreach (var iface in type.InterfaceNames)
 1062            {
 1431063                if (dependentCounts.TryGetValue(GeneratorHelpers.GetShortTypeName(iface), out var c2))
 231064                    count += c2;
 1065            }
 1066
 539681067            if (count >= minDependents)
 8921068                hubs.Add((type.TypeName, count));
 1069        }
 1070
 10191071        return hubs.OrderByDescending(h => h.DependentCount).ToList();
 1072    }
 1073
 1074    internal static string GenerateOptionsSummaryMarkdown(DiscoveryResult discovery, string assemblyName, string timesta
 1075    {
 1251076        var sb = new StringBuilder();
 1251077        var options = FilterOptions(discovery.Options, typeFilter);
 1078
 1251079        sb.AppendLine("# Needlr Options Summary");
 1251080        sb.AppendLine();
 1251081        sb.AppendLine($"Generated: {timestamp} UTC");
 1251082        sb.AppendLine($"Assembly: {assemblyName}");
 1251083        sb.AppendLine();
 1084
 1251085        if (options.Count == 0)
 1086        {
 1011087            sb.AppendLine("*No options classes discovered. Add `[Options]` attribute to configuration classes.*");
 1011088            sb.AppendLine();
 1011089            return sb.ToString();
 1090        }
 1091
 241092        sb.AppendLine($"## Overview");
 241093        sb.AppendLine();
 241094        sb.AppendLine($"| Metric | Count |");
 241095        sb.AppendLine($"|:-------|------:|");
 241096        sb.AppendLine($"| Total Options Classes | {GeneratorHelpers.Literal(options.Count)} |");
 531097        sb.AppendLine($"| Named Options | {options.Count(o => o.IsNamed)} |");
 531098        sb.AppendLine($"| With Validation | {options.Count(o => o.ValidateOnStart)} |");
 531099        sb.AppendLine($"| With External Validator | {options.Count(o => o.HasExternalValidator)} |");
 241100        sb.AppendLine();
 1101
 1102        // Options table
 241103        sb.AppendLine("## Options Classes");
 241104        sb.AppendLine();
 241105        sb.AppendLine("| Class | Section | Name | ValidateOnStart | Validator |");
 241106        sb.AppendLine("|:------|:--------|:-----|:---------------:|:----------|");
 1107
 1641108        foreach (var opt in options.OrderBy(o => o.SectionName, StringComparer.Ordinal).ThenBy(o => o.TypeName, StringCo
 1109        {
 291110            var shortName = GeneratorHelpers.GetShortTypeName(opt.TypeName);
 291111            var namedLabel = opt.IsNamed ? opt.Name : "-";
 291112            var validateIcon = opt.ValidateOnStart ? "✅" : "❌";
 291113            var validatorInfo = GetValidatorDescription(opt);
 1114
 291115            sb.AppendLine($"| `{shortName}` | `{opt.SectionName}` | {namedLabel} | {validateIcon} | {validatorInfo} |");
 1116        }
 1117
 241118        sb.AppendLine();
 1119
 1120        // Detailed breakdown by section
 821121        var sectionGroups = options.GroupBy(o => o.SectionName).OrderBy(g => g.Key, StringComparer.Ordinal);
 1122
 241123        sb.AppendLine("## Configuration Sections");
 241124        sb.AppendLine();
 1125
 1061126        foreach (var group in sectionGroups)
 1127        {
 291128            sb.AppendLine($"### `{group.Key}`");
 291129            sb.AppendLine();
 1130
 1451131            foreach (var opt in group.OrderBy(o => o.TypeName, StringComparer.Ordinal))
 1132            {
 291133                var shortName = GeneratorHelpers.GetShortTypeName(opt.TypeName);
 291134                sb.AppendLine($"**{shortName}**");
 291135                sb.AppendLine();
 291136                sb.AppendLine($"- Configuration Path: `{group.Key}`");
 1137
 291138                if (opt.IsNamed)
 91139                    sb.AppendLine($"- Named Options: `{opt.Name}`");
 1140
 291141                sb.AppendLine($"- Validate On Start: {(opt.ValidateOnStart ? "Yes" : "No")}");
 1142
 291143                if (opt.HasExternalValidator)
 1144                {
 31145                    sb.AppendLine($"- External Validator: `{GeneratorHelpers.GetShortTypeName(opt.ValidatorTypeName!)}`"
 1146                }
 1147
 291148                if (opt.HasValidatorMethod)
 1149                {
 41150                    var methodType = opt.ValidatorMethod!.Value.IsStatic ? "static" : "instance";
 41151                    sb.AppendLine($"- Validation Method: `{opt.ValidatorMethod.Value.MethodName}()` ({methodType})");
 1152                }
 251153                else if (opt.ValidateMethodOverride != null)
 1154                {
 11155                    sb.AppendLine($"- Validation Method: `{opt.ValidateMethodOverride}()` (specified but not found)");
 1156                }
 1157
 291158                if (opt.SourceFilePath != null)
 1159                {
 1160                    // Use just the filename for brevity in markdown
 131161                    var fileName = System.IO.Path.GetFileName(opt.SourceFilePath);
 131162                    sb.AppendLine($"- Source: `{fileName}`");
 1163                }
 1164
 291165                sb.AppendLine();
 1166            }
 1167        }
 1168
 1169        // Validation warnings
 531170        var missingValidation = options.Where(o => !o.ValidateOnStart && (o.HasValidatorMethod || o.HasExternalValidator
 241171        if (missingValidation.Count > 0)
 1172        {
 21173            sb.AppendLine("## ⚠️ Potential Issues");
 21174            sb.AppendLine();
 21175            sb.AppendLine("The following options have validators configured but `ValidateOnStart` is disabled:");
 21176            sb.AppendLine();
 1177
 81178            foreach (var opt in missingValidation)
 1179            {
 21180                var shortName = GeneratorHelpers.GetShortTypeName(opt.TypeName);
 21181                sb.AppendLine($"- `{shortName}`: Has validator but won't run at startup");
 1182            }
 1183
 21184            sb.AppendLine();
 21185            sb.AppendLine("*Set `ValidateOnStart = true` in the `[Options]` attribute to enable startup validation.*");
 21186            sb.AppendLine();
 1187        }
 1188
 1189        // Usage example
 241190        sb.AppendLine("## Usage");
 241191        sb.AppendLine();
 241192        sb.AppendLine("Options are automatically registered via source generation. Access them via:");
 241193        sb.AppendLine();
 241194        sb.AppendLine("```csharp");
 241195        sb.AppendLine("// Constructor injection");
 241196        sb.AppendLine("public MyService(IOptions<DatabaseOptions> options) { }");
 241197        sb.AppendLine();
 241198        sb.AppendLine("// Named options");
 241199        sb.AppendLine("public MyService(IOptionsSnapshot<ConnectionOptions> options)");
 241200        sb.AppendLine("{");
 241201        sb.AppendLine("    var primary = options.Get(\"Primary\");");
 241202        sb.AppendLine("}");
 241203        sb.AppendLine("```");
 241204        sb.AppendLine();
 1205
 241206        return sb.ToString();
 1207    }
 1208
 1209    private static string GetValidatorDescription(DiscoveredOptions opt)
 1210    {
 291211        if (opt.HasExternalValidator)
 1212        {
 31213            var validatorShort = GeneratorHelpers.GetShortTypeName(opt.ValidatorTypeName!);
 31214            return $"`{validatorShort}`";
 1215        }
 1216
 261217        if (opt.HasValidatorMethod)
 1218        {
 41219            var methodType = opt.ValidatorMethod!.Value.IsStatic ? "static" : "self";
 41220            return $"`{opt.ValidatorMethod.Value.MethodName}()` ({methodType})";
 1221        }
 1222
 221223        if (opt.ValidateMethodOverride != null)
 1224        {
 11225            return $"`{opt.ValidateMethodOverride}()` (not found)";
 1226        }
 1227
 211228        return "-";
 1229    }
 1230
 1231    internal static IReadOnlyList<DiscoveredOptions> FilterOptions(IReadOnlyList<DiscoveredOptions> options, HashSet<str
 1232    {
 1251233        if (IsUnfiltered(filter))
 1101234            return options;
 1235
 151236        return options.Where(o =>
 91237            MatchesTypeName(filter, o.TypeName) ||
 91238            filter.Contains(o.SectionName) ||
 91239            (o.IsNamed && filter.Contains(o.Name!)))
 151240                      .ToList();
 1241    }
 1242
 1243    private static bool IsUnfiltered(HashSet<string> filter)
 1244    {
 16551245        return filter == null || filter.Count == 0;
 1246    }
 1247
 1248    /// <summary>
 1249    /// Determines whether a fully qualified type name matches the filter by any of its
 1250    /// supported identities: the fully qualified name, the namespace-qualified name, or the short name.
 1251    /// </summary>
 1252    private static bool MatchesTypeName(HashSet<string> filter, string typeName)
 1253    {
 185331254        return filter.Contains(typeName) ||
 185331255               filter.Contains(GeneratorHelpers.GetShortTypeName(typeName)) ||
 185331256               filter.Contains(GeneratorHelpers.StripGlobalPrefix(typeName));
 1257    }
 1258
 1259    private static bool MatchesAnyTypeName(HashSet<string> filter, IEnumerable<string> typeNames)
 1260    {
 184181261        return typeNames.Any(typeName => MatchesTypeName(filter, typeName));
 1262    }
 1263}
 1264

Methods/Properties

GenerateDiagnosticsSource(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,NexusLabs.Needlr.Generators.DiagnosticOptions,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateDependencyGraphMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateLifetimeSummaryMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateRegistrationIndexMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateAnalyzerStatusMarkdown(System.String)
FilterTypes(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>,System.Collections.Generic.HashSet`1<System.String>)
FilterPluginTypes(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredPlugin>,System.Collections.Generic.HashSet`1<System.String>)
FilterDecorators(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredDecorator>,System.Collections.Generic.HashSet`1<System.String>)
FilterFactories(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredFactory>,System.Collections.Generic.HashSet`1<System.String>)
FilterInterceptedServices(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredInterceptedService>,System.Collections.Generic.HashSet`1<System.String>)
FilterReferencedAssemblyTypes(System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>,System.Collections.Generic.HashSet`1<System.String>)
CalculateMaxDependencyDepth(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>)
GetDepth(NexusLabs.Needlr.Generators.Models.DiscoveredType,System.Collections.Generic.Dictionary`2<System.String,NexusLabs.Needlr.Generators.Models.DiscoveredType>,System.Collections.Generic.Dictionary`2<System.String,System.Int32>,System.Collections.Generic.HashSet`1<System.String>)
CalculateHubServices(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>,System.Int32)
GenerateOptionsSummaryMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>)
GetValidatorDescription(NexusLabs.Needlr.Generators.Models.DiscoveredOptions)
FilterOptions(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredOptions>,System.Collections.Generic.HashSet`1<System.String>)
IsUnfiltered(System.Collections.Generic.HashSet`1<System.String>)
MatchesTypeName(System.Collections.Generic.HashSet`1<System.String>,System.String)
MatchesAnyTypeName(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IEnumerable`1<System.String>)