feat(webapi): add solution overview summary endpoint

Adds GET /api/roslyn/solution/overview/summary with json|text|yaml formats.
Parses getsolutionoverview payload and renders compact summary for agents.
This commit is contained in:
2025-10-28 18:18:44 -04:00
parent cade9dcc47
commit cd0f96ca1e

View File

@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using RoslynBridge.WebApi.Models;
using System.Text;
using System.Text.Json;
using System.Linq;
using RoslynBridge.WebApi.Services;
namespace RoslynBridge.WebApi.Controllers;
@@ -91,6 +94,130 @@ public class RoslynController : ControllerBase
return Ok(result);
}
/// <summary>
/// Get a compact summary of the solution overview
/// </summary>
/// <param name="topNProjects">How many projects to list (by file count)</param>
/// <param name="includeNamespaces">Include top-level namespaces in summary</param>
/// <param name="format">One of: json | text | yaml</param>
/// <param name="instancePort">Optional: specific VS instance port to target</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Compact summary as JSON or plain text</returns>
[HttpGet("solution/overview/summary")]
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
public async Task<IActionResult> GetSolutionOverviewSummary(
[FromQuery] int topNProjects = 5,
[FromQuery] bool includeNamespaces = true,
[FromQuery] string? format = "json",
[FromQuery] int? instancePort = null,
CancellationToken cancellationToken = default)
{
var request = new RoslynQueryRequest { QueryType = "getsolutionoverview" };
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
if (!result.Success)
{
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
return Ok(result);
}
var err = result.Error ?? "Unknown error";
return Content($"error: {err}", "text/plain", Encoding.UTF8);
}
// Expect Data as JsonElement; shape per Models.SolutionOverview
int projectCount = 0;
int documentCount = 0;
var projectsSummary = new List<(string Name, int FileCount)>();
var topLevelNamespaces = new List<string>();
if (result.Data is JsonElement dataEl && dataEl.ValueKind == JsonValueKind.Object)
{
if (dataEl.TryGetProperty("projectCount", out var pc)) projectCount = pc.GetInt32();
if (dataEl.TryGetProperty("documentCount", out var dc)) documentCount = dc.GetInt32();
if (includeNamespaces && dataEl.TryGetProperty("topLevelNamespaces", out var nsEl) && nsEl.ValueKind == JsonValueKind.Array)
{
foreach (var ns in nsEl.EnumerateArray())
{
if (ns.ValueKind == JsonValueKind.String)
{
topLevelNamespaces.Add(ns.GetString() ?? string.Empty);
}
}
}
if (dataEl.TryGetProperty("projects", out var prEl) && prEl.ValueKind == JsonValueKind.Array)
{
foreach (var p in prEl.EnumerateArray())
{
string name = p.TryGetProperty("name", out var nEl) && nEl.ValueKind == JsonValueKind.String ? (nEl.GetString() ?? string.Empty) : string.Empty;
int files = p.TryGetProperty("fileCount", out var fEl) && fEl.ValueKind == JsonValueKind.Number ? fEl.GetInt32() : 0;
projectsSummary.Add((name, files));
}
}
}
var topProjects = projectsSummary
.OrderByDescending(p => p.FileCount)
.ThenBy(p => p.Name)
.Take(Math.Max(0, topNProjects))
.ToList();
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
var compact = new
{
projectCount,
documentCount,
projects = topProjects.Select(p => new { name = p.Name, files = p.FileCount }).ToList(),
topLevelNamespaces = includeNamespaces ? topLevelNamespaces : null
};
return Ok(new RoslynQueryResponse
{
Success = true,
Message = "Compact solution overview",
Data = compact,
Error = null
});
}
// text or yaml: render a compact string
var sb = new StringBuilder();
if (string.Equals(format, "yaml", StringComparison.OrdinalIgnoreCase))
{
sb.AppendLine($"projectCount: {projectCount}");
sb.AppendLine($"documentCount: {documentCount}");
if (includeNamespaces)
{
sb.Append("topLevelNamespaces: [");
sb.Append(string.Join(", ", topLevelNamespaces));
sb.AppendLine("]");
}
sb.AppendLine("projects:");
foreach (var p in topProjects)
{
sb.AppendLine($" - name: {p.Name}");
sb.AppendLine($" files: {p.FileCount}");
}
}
else
{
// text
sb.AppendLine($"Projects: {projectCount} | Documents: {documentCount}");
if (includeNamespaces && topLevelNamespaces.Count > 0)
{
sb.AppendLine($"TopNamespaces: {string.Join(", ", topLevelNamespaces)}");
}
sb.AppendLine("Top Projects:");
foreach (var p in topProjects)
{
sb.AppendLine($" - {p.Name} ({p.FileCount})");
}
}
return Content(sb.ToString(), "text/plain", Encoding.UTF8);
}
/// <summary>
/// Get diagnostics (errors and warnings)
/// </summary>