Files
Workspace/Tools/AgentSupport/Shrink.Inspect/ModuleCatalog.cs
T
cneicy 08246ddb01
Validate ShrinkSDK Workspace / catalog (push) Successful in 5s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m20s
feat: integrate performance upgrade and agent inspection tooling
2026-09-29 10:16:02 +08:00

106 lines
6.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using System.Text.RegularExpressions;
namespace ShrinkSDK.Inspect;
public static class ModuleCatalog
{
private static readonly HashSet<string> Excluded = new(StringComparer.OrdinalIgnoreCase)
{ ".git", "Library", "Temp", "obj", "bin", "Logs", "Builds", "Artifacts", "node_modules", "Development~", "Samples~" };
public static IEnumerable<string> Files(string root, string pattern)
{
foreach (var file in Directory.EnumerateFiles(root, pattern)) yield return file;
foreach (var dir in Directory.EnumerateDirectories(root))
{
if (Excluded.Contains(Path.GetFileName(dir)) || (File.GetAttributes(dir) & FileAttributes.ReparsePoint) != 0) continue;
foreach (var file in Files(dir, pattern)) yield return file;
}
}
public static void Load(Index index)
{
var installed = Path.Combine(index.Root, "Library", "PackageCache");
var scanRoots = new List<string> { index.Root };
if (Directory.Exists(installed)) scanRoots.AddRange(Directory.GetDirectories(installed, "com.cneicy.shrink-*@*"));
var packages = scanRoots.SelectMany(r => Files(r, "package.json")).Select(path => (path, doc: Read(path)))
.Where(x => x.doc.TryGetProperty("name", out _)).ToArray();
var asmdefs = scanRoots.SelectMany(r => Files(r, "*.asmdef")).ToArray();
var roots = packages.Select(p => Path.GetDirectoryName(p.path)!).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var path in asmdefs)
{
var dir = Path.GetDirectoryName(path)!;
if (!roots.Any(root => dir.Equals(root, StringComparison.OrdinalIgnoreCase) || dir.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))) roots.Add(dir);
}
// Business directories can share an asmdef while retaining separate ownership.
foreach (var readme in scanRoots.SelectMany(r => Files(r, "README.md")))
if (File.ReadAllText(readme).Contains("<!-- shrink:capabilities -->", StringComparison.Ordinal)) roots.Add(Path.GetDirectoryName(readme)!);
foreach (var root in roots.Order(StringComparer.Ordinal))
{
var package = packages.FirstOrDefault(p => Path.GetDirectoryName(p.path) == root);
var name = package.doc.ValueKind == JsonValueKind.Object ? String(package.doc, "name") : Path.GetFileName(root);
var version = package.doc.ValueKind == JsonValueKind.Object ? String(package.doc, "version") : "unversioned";
var dependencies = package.doc.ValueKind == JsonValueKind.Object && package.doc.TryGetProperty("dependencies", out var deps)
? deps.EnumerateObject().Select(p => p.Name + "@" + p.Value.GetString()).ToArray() : [];
var assemblies = asmdefs.Where(p => p.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
.Select(p => String(Read(p), "name")).ToArray();
var readme = Path.Combine(root, "README.md");
var complete = File.Exists(readme) && ReadCapabilities(name, readme, index.Capabilities);
var validation = Files(root, "*.cs").Where(p => p.Contains("Tests", StringComparison.OrdinalIgnoreCase))
.Select(p => Path.GetRelativePath(index.Root, p)).Take(8).ToArray();
index.Modules.Add(new(name, root, version, dependencies, assemblies, validation, complete));
}
foreach (var module in index.Modules)
foreach (var dep in module.Dependencies)
{
var split = dep.LastIndexOf('@');
if (split <= 0) continue;
var target = index.Modules.FirstOrDefault(m => m.Name == dep[..split]);
if (target == null) continue;
if (target.Version != dep[(split + 1)..]) index.Findings.Add(new("SHRINK101", "error", $"{module.Name} requires {dep}, workspace has {target.Version}", new(Path.Combine(module.Root, "package.json"), 1)));
if (!module.Name.Contains("integration") && !module.Name.Contains("starter") && target.Name.Contains("-integration-"))
index.Findings.Add(new("SHRINK102", "error", $"Main package {module.Name} depends on integration {target.Name}.", new(Path.Combine(module.Root, "package.json"), 1)));
}
foreach (var module in index.Modules)
Visit(module, new HashSet<string>(), new HashSet<string>());
void Visit(ModuleInfo current, HashSet<string> path, HashSet<string> seen)
{
if (!path.Add(current.Name)) { index.Findings.Add(new("SHRINK103", "error", "Package dependency cycle: " + string.Join(" -> ", path) + " -> " + current.Name, new(Path.Combine(current.Root, "package.json"), 1))); return; }
if (seen.Add(current.Name))
foreach (var dep in current.Dependencies)
{
var target = index.Modules.FirstOrDefault(m => dep.StartsWith(m.Name + "@", StringComparison.Ordinal));
if (target != null) Visit(target, path, seen);
}
path.Remove(current.Name);
}
}
private static bool ReadCapabilities(string module, string path, List<Capability> output)
{
var lines = File.ReadAllLines(path);
var start = Array.IndexOf(lines, "<!-- shrink:capabilities -->");
var end = Array.IndexOf(lines, "<!-- /shrink:capabilities -->");
if (start < 0 || end <= start) return false;
string Field(string key) => lines.Skip(start + 1).Take(end - start - 1)
.FirstOrDefault(s => s.StartsWith(key + ":", StringComparison.Ordinal))?[(key.Length + 1)..].Trim() ?? "";
output.Add(new(module, Field("Capability"), Field("Aliases"), Field("Limits"), Field("Extension"), Field("Evidence"), new(path, start + 1)));
return new[] { "Capability", "Limits", "Extension", "Evidence" }.All(key => Field(key).Length > 0);
}
public static IEnumerable<Capability> Search(Index index, string query, string? module)
{
var terms = Regex.Split(query.Trim(), @"[\s,,;;/]+").Where(t => t.Length > 0).ToArray();
return index.Capabilities.Where(c => module == null || c.Module.Contains(module, StringComparison.OrdinalIgnoreCase))
.Select(c => {
var text = c.Module + " " + c.Summary + " " + c.Aliases;
var matches = terms.Where(t => text.Contains(t, StringComparison.OrdinalIgnoreCase)).ToArray();
return c with { Score = matches.Length, MatchReason = string.Join(", ", matches) };
}).Where(c => terms.Length == 0 || c.Score > 0).OrderByDescending(c => c.Score).ThenBy(c => c.Module, StringComparer.Ordinal);
}
private static JsonElement Read(string path) { using var doc = JsonDocument.Parse(File.ReadAllText(path)); return doc.RootElement.Clone(); }
private static string String(JsonElement e, string name) => e.TryGetProperty(name, out var p) ? p.GetString() ?? "" : "";
}