feat: integrate performance upgrade and agent inspection tooling
Validate ShrinkSDK Workspace / catalog (push) Successful in 5s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m20s

This commit is contained in:
2026-09-29 10:16:02 +08:00
parent f92105c97d
commit 08246ddb01
114 changed files with 7135 additions and 87 deletions
@@ -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)
});
}
}