feat: integrate performance upgrade and agent inspection tooling
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using System.Text.Json;
|
||||
using ShrinkSDK.Inspect;
|
||||
using Index = ShrinkSDK.Inspect.Index;
|
||||
|
||||
return await InspectCommand.Run(args);
|
||||
|
||||
public static class InspectCommand
|
||||
{
|
||||
public static async Task<int> Run(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length < 2 || args[0] != "inspect")
|
||||
{
|
||||
Console.WriteLine("shrink inspect modules|capabilities|symbol|flow|impact|check [query] [--root dir] [--module name] [--limit n] [--depth n] [--json] [--compilation file|--project csproj] [--snapshot file]");
|
||||
return args.Length == 0 ? 0 : 2;
|
||||
}
|
||||
var options = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var words = new List<string>();
|
||||
for (var i = 2; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--json") options["json"] = "true";
|
||||
else if (args[i].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var key = args[i][2..];
|
||||
if (key is not ("root" or "module" or "limit" or "offset" or "depth" or "compilation" or "project" or "snapshot")) throw new ArgumentException("Unknown option --" + key);
|
||||
if (++i >= args.Length) throw new ArgumentException("Missing value for --" + key);
|
||||
options[key] = args[i];
|
||||
}
|
||||
else words.Add(args[i]);
|
||||
}
|
||||
string? Option(string key) => options.GetValueOrDefault(key);
|
||||
var command = args[1];
|
||||
if (command is not ("modules" or "capabilities" or "symbol" or "flow" or "impact" or "check")) throw new ArgumentException("Unknown query " + command);
|
||||
var query = string.Join(" ", words);
|
||||
if (command is "symbol" or "flow" or "impact" && query.Length == 0) throw new ArgumentException("An exact symbol, route, key or file query is required.");
|
||||
var limit = int.Parse(Option("limit") ?? (command == "capabilities" ? "3" : "20"));
|
||||
var offset = int.Parse(Option("offset") ?? "0");
|
||||
var depth = int.Parse(Option("depth") ?? "2");
|
||||
if (limit is < 1 or > 1000 || depth is < 0 or > 10 || offset < 0) throw new ArgumentException("limit must be 1..1000; depth must be 0..10; offset must be nonnegative.");
|
||||
var index = new Index { Root = Path.GetFullPath(Option("root") ?? Environment.CurrentDirectory) };
|
||||
if (!Directory.Exists(index.Root)) throw new DirectoryNotFoundException(index.Root);
|
||||
if (Option("project") != null && Option("compilation") != null) throw new ArgumentException("Choose --project or --compilation, not both.");
|
||||
ModuleCatalog.Load(index);
|
||||
if (command is not ("modules" or "capabilities"))
|
||||
{
|
||||
await SemanticIndex.Build(index, Option("compilation"), Option("project"), checkRules: command == "check");
|
||||
index.Relations.RemoveAll(r => string.IsNullOrEmpty(r.From) || string.IsNullOrEmpty(r.To));
|
||||
var unique = index.Relations.Distinct().ToArray();
|
||||
index.Relations.Clear(); index.Relations.AddRange(unique);
|
||||
SemanticIndex.CheckRelations(index);
|
||||
}
|
||||
if (Option("snapshot") is { } snapshot) AttachSnapshot(index, snapshot);
|
||||
var moduleFilter = Option("module");
|
||||
var stateQuery = query.StartsWith("key:", StringComparison.Ordinal) ? query[4..] : query;
|
||||
bool MatchModule(string module) => moduleFilter == null || module.Contains(moduleFilter, StringComparison.OrdinalIgnoreCase);
|
||||
object[] all = command switch
|
||||
{
|
||||
"modules" => index.Modules.Where(m => MatchModule(m.Name)).Cast<object>().ToArray(),
|
||||
"capabilities" => ModuleCatalog.Search(index, query, moduleFilter).Cast<object>().ToArray(),
|
||||
"symbol" => index.Symbols.Values.Where(s => MatchModule(s.Module) && (s.Id.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Name.Contains(query, StringComparison.OrdinalIgnoreCase))).OrderBy(s => s.Id).Cast<object>().Concat(Walk(index, query, 1, moduleFilter)).ToArray(),
|
||||
"flow" or "impact" => Walk(index, query, depth, moduleFilter).Cast<object>().Concat(index.RuntimeStates.Where(s => s.Scope.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Value.ToString().Contains(stateQuery, StringComparison.OrdinalIgnoreCase))).ToArray(),
|
||||
"check" => index.Findings.Distinct().Cast<object>().ToArray(),
|
||||
_ => []
|
||||
};
|
||||
var missingDescriptions = index.Modules.Where(m => MatchModule(m.Name) && !m.SemanticDescriptionComplete).Select(m => m.Name).ToArray();
|
||||
var result = new {
|
||||
success = true, formatVersion = 1, toolVersion = typeof(InspectCommand).Assembly.GetName().Version?.ToString(), command, query, buildId = index.BuildId,
|
||||
offset, limit, nextOffset = all.Length > offset + limit ? (int?)(offset + limit) : null,
|
||||
coverage = index.Coverage.Distinct().Take(12), coverageTruncated = index.Coverage.Distinct().Count() > 12,
|
||||
semanticCoverage = new { incompleteCount = missingDescriptions.Length, examples = missingDescriptions.Take(5) },
|
||||
validation = command == "impact" ? index.Modules.Where(m => all.OfType<Relation>().Any(r => index.ModuleFor(r.Location.File) == m.Name)).SelectMany(m => m.Validation).Distinct().Take(8).ToArray() : Array.Empty<string>(),
|
||||
total = all.Length, truncated = all.Length > offset + limit, items = all.Skip(offset).Take(limit)
|
||||
};
|
||||
if (options.ContainsKey("json")) Console.WriteLine(JsonSerializer.Serialize(result, SemanticIndex.Json));
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"# {command}: {query}\n\nShowing {Math.Min(limit, Math.Max(0, all.Length - offset))}/{all.Length}." + (all.Length > limit ? " Truncated; narrow the query or continue with --offset." : ""));
|
||||
if (missingDescriptions.Length > 0) Console.WriteLine($"\nSemantic descriptions incomplete: {missingDescriptions.Length} modules. No match is not proof of absence.");
|
||||
foreach (var warning in index.Coverage.Distinct().Take(12)) Console.WriteLine("\n> " + warning);
|
||||
foreach (var item in all.Skip(offset).Take(limit)) Render(item);
|
||||
}
|
||||
return command == "check" && index.Findings.Any(f => f.Severity == "error") ? 1 : 0;
|
||||
}
|
||||
catch (Exception e) when (e is ArgumentException or IOException or JsonException or InvalidOperationException or FormatException or OverflowException)
|
||||
{
|
||||
if (args.Contains("--json")) Console.WriteLine(JsonSerializer.Serialize(new { success = false, code = "SHRINK-INSPECT-INPUT", error = e.Message }, SemanticIndex.Json));
|
||||
else Console.Error.WriteLine("SHRINK-INSPECT-INPUT: " + e.Message);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<Relation> Walk(Index index, string query, int depth, string? module)
|
||||
{
|
||||
var ids = index.Symbols.Values.Where(s =>
|
||||
(s.Id.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Name.Contains(query, StringComparison.OrdinalIgnoreCase) || s.Location.File.Contains(query, StringComparison.OrdinalIgnoreCase))).Select(s => s.Id).ToHashSet();
|
||||
foreach (var edge in index.Relations)
|
||||
foreach (var endpoint in new[] { edge.From, edge.To })
|
||||
if (endpoint.Contains(query, StringComparison.OrdinalIgnoreCase)) ids.Add(endpoint);
|
||||
var seen = new HashSet<Relation>();
|
||||
for (var step = 0; step < depth; step++)
|
||||
{
|
||||
var next = new HashSet<string>();
|
||||
foreach (var edge in index.Relations.Where(r => ids.Contains(r.From) || ids.Contains(r.To)))
|
||||
if (seen.Add(edge)) { // External utility methods do not connect every unrelated caller into one flow.
|
||||
foreach (var endpoint in new[] { edge.From, edge.To })
|
||||
if (index.Symbols.ContainsKey(endpoint) || !endpoint.StartsWith("M:", StringComparison.Ordinal)) next.Add(endpoint);
|
||||
if (module == null || index.ModuleFor(edge.Location.File).Contains(module, StringComparison.OrdinalIgnoreCase) ||
|
||||
new[] { edge.From, edge.To }.Any(id => index.Symbols.TryGetValue(id, out var symbol) && symbol.Module.Contains(module, StringComparison.OrdinalIgnoreCase))) yield return edge; }
|
||||
ids.UnionWith(next);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AttachSnapshot(Index index, string path)
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var root = document.RootElement;
|
||||
var identity = root.TryGetProperty("BuildId", out var b) ? b.GetString() : null;
|
||||
if (string.IsNullOrEmpty(index.BuildId) || identity != index.BuildId)
|
||||
{ index.Coverage.Add("Runtime snapshot build mismatch or unverified source identity. Runtime relations were NOT merged."); return; }
|
||||
if (root.TryGetProperty("Coverage", out var coverage))
|
||||
foreach (var item in coverage.EnumerateArray()) index.Coverage.Add("Snapshot: " + item.GetString());
|
||||
if (root.TryGetProperty("State", out var state))
|
||||
foreach (var scope in state.EnumerateObject())
|
||||
foreach (var field in scope.Value.EnumerateObject())
|
||||
if (field.Value.ValueKind == JsonValueKind.Array)
|
||||
foreach (var entry in field.Value.EnumerateArray()) index.RuntimeStates.Add(new(scope.Name, field.Name, entry.Clone(), new(Path.GetFullPath(path), 1)));
|
||||
else index.RuntimeStates.Add(new(scope.Name, field.Name, field.Value.Clone(), new(Path.GetFullPath(path), 1)));
|
||||
if (!root.TryGetProperty("Relations", out var relations)) return;
|
||||
foreach (var relation in relations.EnumerateArray())
|
||||
{
|
||||
var from = relation.GetProperty("From").GetString()!;
|
||||
var to = relation.GetProperty("To").GetString()!;
|
||||
var kind = relation.GetProperty("Kind").GetString()!;
|
||||
var scope = relation.TryGetProperty("Scope", out var scopeElement) ? scopeElement.GetString() : "";
|
||||
index.Relations.Add(new(from, to, kind + " [" + scope + "]", "runtime-bound", new(Path.GetFullPath(path), 1)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void Render(object item)
|
||||
{
|
||||
string Link(SourceLocation loc) => $"[{Path.GetFileName(loc.File)}:{loc.Line}]({loc.File.Replace('\\', '/')}:{loc.Line})";
|
||||
Console.WriteLine(item switch {
|
||||
Capability c => $"\n- **{c.Module}** — {c.Summary}\n Match: {c.MatchReason}\n Limits: {c.Limits}\n Extension: {c.Extension}\n Evidence: {c.Evidence}; {Link(c.Location)}",
|
||||
ModuleInfo m => $"\n- **{m.Name} {m.Version}** — {m.Root}\n Dependencies: {string.Join(", ", m.Dependencies)}\n Validation: {string.Join(", ", m.Validation)}",
|
||||
SymbolInfo s => $"\n- `{s.Id}` ({s.Module}) — {Link(s.Location)}",
|
||||
Relation r => $"\n- `{r.From}` → `{r.To}` [{r.Kind}; {r.Evidence}] — {Link(r.Location)}",
|
||||
RuntimeState s => $"\n- Runtime `{s.Scope}/{s.Name}`: {s.Value} — {Link(s.Location)}",
|
||||
Finding f => $"\n- {f.Severity} **{f.Code}** {f.Message} — {Link(f.Location)}",
|
||||
_ => "\n" + JsonSerializer.Serialize(item)
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user