save work
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Outcome classification for entrypoint resolution attempts.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum EntryTraceOutcome
|
||||
{
|
||||
Resolved,
|
||||
@@ -16,6 +18,7 @@ public enum EntryTraceOutcome
|
||||
/// <summary>
|
||||
/// Logical classification for nodes in the entry trace graph.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum EntryTraceNodeKind
|
||||
{
|
||||
Command,
|
||||
@@ -30,6 +33,7 @@ public enum EntryTraceNodeKind
|
||||
/// <summary>
|
||||
/// Interpreter categories supported by the analyzer.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum EntryTraceInterpreterKind
|
||||
{
|
||||
None,
|
||||
@@ -41,6 +45,7 @@ public enum EntryTraceInterpreterKind
|
||||
/// <summary>
|
||||
/// Diagnostic severity levels emitted by the analyzer.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum EntryTraceDiagnosticSeverity
|
||||
{
|
||||
Info,
|
||||
@@ -51,6 +56,7 @@ public enum EntryTraceDiagnosticSeverity
|
||||
/// <summary>
|
||||
/// Enumerates the canonical reasons for unresolved edges.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum EntryTraceUnknownReason
|
||||
{
|
||||
CommandNotFound,
|
||||
@@ -83,6 +89,7 @@ public enum EntryTraceUnknownReason
|
||||
/// <summary>
|
||||
/// Categorises terminal executable kinds.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum EntryTraceTerminalType
|
||||
{
|
||||
Unknown,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
@@ -175,9 +176,37 @@ public sealed class DotNetSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies
|
||||
var packageDependencies = new List<string>();
|
||||
if (context.Dependencies.TryGetValue("dotnet", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
packageDependencies.AddRange(deps);
|
||||
}
|
||||
|
||||
ProjectInfo? projectInfo = null;
|
||||
if (context.ManifestPaths.TryGetValue("project", out var projectPath))
|
||||
{
|
||||
projectInfo = await TryReadProjectInfoAsync(context, projectPath, cancellationToken);
|
||||
if (projectInfo is not null && projectInfo.PackageReferences.Count > 0)
|
||||
{
|
||||
packageDependencies.AddRange(projectInfo.PackageReferences);
|
||||
reasoningChain.Add($"Parsed project file ({projectInfo.PackageReferences.Count} PackageReference)");
|
||||
}
|
||||
|
||||
if (projectInfo?.IsWebSdk == true)
|
||||
{
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
framework = "aspnetcore";
|
||||
reasoningChain.Add("Project Sdk indicates Web -> WebServer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (packageDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in packageDependencies)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
@@ -186,19 +215,30 @@ public sealed class DotNetSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
framework = NormalizeFramework(normalizedDep);
|
||||
reasoningChain.Add($"Detected {normalizedDep} -> {intent}");
|
||||
}
|
||||
|
||||
if (mappedIntent is ApplicationIntent.WebServer or ApplicationIntent.RpcServer or ApplicationIntent.GraphQlServer)
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
else if (mappedIntent is ApplicationIntent.Worker or ApplicationIntent.StreamProcessor)
|
||||
builder.AddCapability(CapabilityClass.MessageQueue);
|
||||
}
|
||||
|
||||
if (PackageCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Package {dep} -> {capability}");
|
||||
reasoningChain.Add($"Package {normalizedDep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intent == ApplicationIntent.Unknown && projectInfo?.OutputTypeExe == true)
|
||||
{
|
||||
intent = ApplicationIntent.CliTool;
|
||||
reasoningChain.Add("Project OutputType=Exe -> CliTool");
|
||||
}
|
||||
|
||||
// Analyze entrypoint command
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
@@ -262,6 +302,17 @@ public sealed class DotNetSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
return parts[0].Trim();
|
||||
}
|
||||
|
||||
private static string? NormalizeFramework(string normalizedDependency)
|
||||
{
|
||||
if (normalizedDependency.StartsWith("Microsoft.AspNetCore", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(normalizedDependency, "Swashbuckle.AspNetCore", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "aspnetcore";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
{
|
||||
var priorityOrder = new[]
|
||||
@@ -358,4 +409,61 @@ public sealed class DotNetSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-dotnet-{hash[..12]}";
|
||||
}
|
||||
|
||||
private sealed record ProjectInfo(
|
||||
bool IsWebSdk,
|
||||
bool OutputTypeExe,
|
||||
IReadOnlyList<string> PackageReferences);
|
||||
|
||||
private static async Task<ProjectInfo?> TryReadProjectInfoAsync(
|
||||
SemanticAnalysisContext context,
|
||||
string projectPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var content = await context.FileSystem.TryReadFileAsync(projectPath, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var doc = XDocument.Parse(content);
|
||||
var root = doc.Root;
|
||||
|
||||
var sdk = root?.Attribute("Sdk")?.Value?.Trim();
|
||||
var isWebSdk = !string.IsNullOrWhiteSpace(sdk) &&
|
||||
sdk.Contains("Microsoft.NET.Sdk.Web", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var outputType = doc.Descendants()
|
||||
.FirstOrDefault(element => element.Name.LocalName == "OutputType")
|
||||
?.Value
|
||||
?.Trim();
|
||||
|
||||
var outputTypeExe = string.Equals(outputType, "Exe", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var packageReferences = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var element in doc.Descendants().Where(element => element.Name.LocalName == "PackageReference"))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var include = element.Attribute("Include")?.Value?.Trim();
|
||||
var update = element.Attribute("Update")?.Value?.Trim();
|
||||
var name = !string.IsNullOrWhiteSpace(include) ? include : update;
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
packageReferences.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return new ProjectInfo(
|
||||
IsWebSdk: isWebSdk,
|
||||
OutputTypeExe: outputTypeExe,
|
||||
PackageReferences: packageReferences.OrderBy(static name => name, StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
@@ -192,9 +193,31 @@ public sealed class GoSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies (go.mod imports)
|
||||
var moduleDependencies = new List<string>();
|
||||
if (context.Dependencies.TryGetValue("go", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
moduleDependencies.AddRange(deps);
|
||||
}
|
||||
|
||||
if (context.ManifestPaths.TryGetValue("go.mod", out var goModPath))
|
||||
{
|
||||
var goModDependencies = await TryReadGoModDependenciesAsync(context, goModPath, cancellationToken);
|
||||
if (goModDependencies.Count > 0)
|
||||
{
|
||||
moduleDependencies.AddRange(goModDependencies);
|
||||
reasoningChain.Add($"Parsed go.mod ({goModDependencies.Count} deps)");
|
||||
}
|
||||
|
||||
if (await DetectNetHttpUsageAsync(context, goModPath, cancellationToken))
|
||||
{
|
||||
moduleDependencies.Add("net/http");
|
||||
reasoningChain.Add("Detected net/http usage in source");
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in moduleDependencies)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
@@ -203,15 +226,20 @@ public sealed class GoSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
framework = normalizedDep;
|
||||
reasoningChain.Add($"Detected {normalizedDep} -> {intent}");
|
||||
}
|
||||
|
||||
if (mappedIntent is ApplicationIntent.WebServer or ApplicationIntent.RpcServer or ApplicationIntent.GraphQlServer)
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
else if (mappedIntent is ApplicationIntent.Worker or ApplicationIntent.StreamProcessor or ApplicationIntent.MessageBroker)
|
||||
builder.AddCapability(CapabilityClass.MessageQueue);
|
||||
}
|
||||
|
||||
if (ModuleCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Module {dep} -> {capability}");
|
||||
reasoningChain.Add($"Module {normalizedDep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,9 +291,20 @@ public sealed class GoSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
// Handle Go module paths with versions
|
||||
var parts = dep.Split('@');
|
||||
return parts[0].Trim();
|
||||
// Handle Go module paths with versions (both @ and whitespace forms):
|
||||
// - github.com/spf13/cobra@v1.7.0 -> github.com/spf13/cobra
|
||||
// - github.com/spf13/cobra v1.7.0 -> github.com/spf13/cobra
|
||||
var trimmed = dep.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
var whitespaceParts = trimmed.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
|
||||
trimmed = whitespaceParts.Length > 0 ? whitespaceParts[0] : trimmed;
|
||||
|
||||
var atParts = trimmed.Split('@');
|
||||
return atParts[0].Trim();
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
@@ -367,4 +406,120 @@ public sealed class GoSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-go-{hash[..12]}";
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> TryReadGoModDependenciesAsync(
|
||||
SemanticAnalysisContext context,
|
||||
string goModPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var content = await context.FileSystem.TryReadFileAsync(goModPath, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var dependencies = new HashSet<string>(StringComparer.Ordinal);
|
||||
using var reader = new StringReader(content);
|
||||
string? line;
|
||||
var inRequireBlock = false;
|
||||
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0 || trimmed.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inRequireBlock)
|
||||
{
|
||||
if (trimmed == ")")
|
||||
{
|
||||
inRequireBlock = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var parts = trimmed.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
dependencies.Add(parts[0]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("require (", StringComparison.Ordinal))
|
||||
{
|
||||
inRequireBlock = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("require ", StringComparison.Ordinal))
|
||||
{
|
||||
var rest = trimmed["require ".Length..].Trim();
|
||||
var parts = rest.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
dependencies.Add(parts[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencies.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return dependencies.OrderBy(static dependency => dependency, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static async Task<bool> DetectNetHttpUsageAsync(
|
||||
SemanticAnalysisContext context,
|
||||
string goModPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var directory = GetDirectory(goModPath);
|
||||
if (directory is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var goFiles = await context.FileSystem.ListFilesAsync(directory, "*.go", cancellationToken);
|
||||
foreach (var file in goFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var content = await context.FileSystem.TryReadFileAsync(file, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (content.Contains("net/http", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? GetDirectory(string path)
|
||||
{
|
||||
var normalized = path.Replace('\\', '/');
|
||||
var lastSlash = normalized.LastIndexOf('/');
|
||||
if (lastSlash < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastSlash == 0)
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
return normalized[..lastSlash];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
@@ -183,9 +184,25 @@ public sealed class JavaSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies
|
||||
var javaDependencies = new List<string>();
|
||||
if (context.Dependencies.TryGetValue("java", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
javaDependencies.AddRange(deps);
|
||||
}
|
||||
|
||||
if (context.ManifestPaths.TryGetValue("pom.xml", out var pomPath))
|
||||
{
|
||||
var pomDependencies = await TryReadPomDependenciesAsync(context, pomPath, cancellationToken);
|
||||
if (pomDependencies.Count > 0)
|
||||
{
|
||||
javaDependencies.AddRange(pomDependencies);
|
||||
reasoningChain.Add($"Parsed pom.xml ({pomDependencies.Count} deps)");
|
||||
}
|
||||
}
|
||||
|
||||
if (javaDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in javaDependencies)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
@@ -194,15 +211,20 @@ public sealed class JavaSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
framework = normalizedDep;
|
||||
reasoningChain.Add($"Detected {normalizedDep} -> {intent}");
|
||||
}
|
||||
|
||||
if (mappedIntent == ApplicationIntent.WebServer)
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
else if (mappedIntent is ApplicationIntent.Worker or ApplicationIntent.StreamProcessor)
|
||||
builder.AddCapability(CapabilityClass.MessageQueue);
|
||||
}
|
||||
|
||||
if (DependencyCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Dependency {dep} -> {capability}");
|
||||
reasoningChain.Add($"Dependency {normalizedDep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,4 +389,59 @@ public sealed class JavaSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-java-{hash[..12]}";
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> TryReadPomDependenciesAsync(
|
||||
SemanticAnalysisContext context,
|
||||
string pomPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var content = await context.FileSystem.TryReadFileAsync(pomPath, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tokens = new HashSet<string>(StringComparer.Ordinal);
|
||||
var doc = XDocument.Parse(content);
|
||||
|
||||
foreach (var dep in doc.Descendants().Where(element => element.Name.LocalName == "dependency"))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var groupId = dep.Elements().FirstOrDefault(element => element.Name.LocalName == "groupId")?.Value?.Trim();
|
||||
var artifactId = dep.Elements().FirstOrDefault(element => element.Name.LocalName == "artifactId")?.Value?.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(groupId))
|
||||
{
|
||||
if (groupId.StartsWith("org.springframework.boot", StringComparison.Ordinal))
|
||||
{
|
||||
tokens.Add("spring-boot");
|
||||
}
|
||||
|
||||
if (groupId.StartsWith("io.quarkus", StringComparison.Ordinal))
|
||||
{
|
||||
tokens.Add("quarkus");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artifactId))
|
||||
{
|
||||
tokens.Add(artifactId.ToLowerInvariant().Replace("_", "-"));
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return tokens.OrderBy(static token => token, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
@@ -209,9 +210,29 @@ public sealed class NodeSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies
|
||||
if (context.Dependencies.TryGetValue("node", out var deps))
|
||||
context.ManifestPaths.TryGetValue("package.json", out var packageJsonPath);
|
||||
|
||||
var nodeDependencies = new List<string>();
|
||||
if (context.Dependencies.TryGetValue("node", out var deps) ||
|
||||
context.Dependencies.TryGetValue("javascript", out deps) ||
|
||||
context.Dependencies.TryGetValue("typescript", out deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
nodeDependencies.AddRange(deps);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(packageJsonPath))
|
||||
{
|
||||
var manifestDependencies = await TryReadPackageJsonDependenciesAsync(context, packageJsonPath, cancellationToken);
|
||||
if (manifestDependencies.Count > 0)
|
||||
{
|
||||
nodeDependencies.AddRange(manifestDependencies);
|
||||
reasoningChain.Add($"Parsed package.json ({manifestDependencies.Count} deps)");
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in nodeDependencies)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
@@ -220,19 +241,31 @@ public sealed class NodeSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
framework = NormalizeFramework(normalizedDep);
|
||||
reasoningChain.Add($"Detected {normalizedDep} -> {intent}");
|
||||
}
|
||||
|
||||
if (mappedIntent is ApplicationIntent.WebServer or ApplicationIntent.RpcServer or ApplicationIntent.GraphQlServer)
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
else if (mappedIntent is ApplicationIntent.Worker or ApplicationIntent.StreamProcessor)
|
||||
builder.AddCapability(CapabilityClass.MessageQueue);
|
||||
}
|
||||
|
||||
if (PackageCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Package {dep} -> {capability}");
|
||||
reasoningChain.Add($"Package {normalizedDep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serverless manifest hint (e.g., serverless.yml discovered by earlier filesystem pass).
|
||||
if (intent == ApplicationIntent.Unknown && context.ManifestPaths.ContainsKey("serverless"))
|
||||
{
|
||||
intent = ApplicationIntent.Serverless;
|
||||
reasoningChain.Add("Manifest hint: serverless -> Serverless");
|
||||
}
|
||||
|
||||
// Analyze entrypoint command
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
@@ -247,9 +280,9 @@ public sealed class NodeSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
}
|
||||
|
||||
// Check package.json for bin entries -> CLI tool
|
||||
if (context.ManifestPaths.TryGetValue("package.json", out var pkgPath))
|
||||
if (!string.IsNullOrWhiteSpace(packageJsonPath))
|
||||
{
|
||||
if (await HasBinEntriesAsync(context, pkgPath, cancellationToken))
|
||||
if (await HasBinEntriesAsync(context, packageJsonPath, cancellationToken))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
@@ -286,10 +319,87 @@ public sealed class NodeSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
// Handle scoped packages and versions
|
||||
return dep.ToLowerInvariant()
|
||||
.Split('@')[0] // Remove version
|
||||
.Trim();
|
||||
// Handle scoped packages and versions:
|
||||
// - express@4.18.0 -> express
|
||||
// - @nestjs/core -> @nestjs/core
|
||||
// - @nestjs/core@10.0.0 -> @nestjs/core
|
||||
var normalized = dep.Trim().ToLowerInvariant();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (normalized.StartsWith("@", StringComparison.Ordinal))
|
||||
{
|
||||
var lastAt = normalized.LastIndexOf('@');
|
||||
return lastAt > 0 ? normalized[..lastAt] : normalized;
|
||||
}
|
||||
|
||||
var at = normalized.IndexOf('@', StringComparison.Ordinal);
|
||||
return at > 0 ? normalized[..at] : normalized;
|
||||
}
|
||||
|
||||
private static string NormalizeFramework(string normalizedDependency)
|
||||
{
|
||||
return normalizedDependency switch
|
||||
{
|
||||
"nest" or "@nestjs/core" or "@nestjs/platform-express" => "nestjs",
|
||||
_ => normalizedDependency
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> TryReadPackageJsonDependenciesAsync(
|
||||
SemanticAnalysisContext context,
|
||||
string pkgPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var content = await context.FileSystem.TryReadFileAsync(pkgPath, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var dependencies = new HashSet<string>(StringComparer.Ordinal);
|
||||
AddDependencyObjectKeys(doc.RootElement, "dependencies", dependencies);
|
||||
AddDependencyObjectKeys(doc.RootElement, "devDependencies", dependencies);
|
||||
AddDependencyObjectKeys(doc.RootElement, "peerDependencies", dependencies);
|
||||
AddDependencyObjectKeys(doc.RootElement, "optionalDependencies", dependencies);
|
||||
|
||||
if (dependencies.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return dependencies.OrderBy(static dep => dep, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddDependencyObjectKeys(JsonElement root, string propertyName, HashSet<string> dependencies)
|
||||
{
|
||||
if (!root.TryGetProperty(propertyName, out var section) || section.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var property in section.EnumerateObject())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(property.Name))
|
||||
{
|
||||
dependencies.Add(property.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
@@ -188,9 +189,29 @@ public sealed class PythonSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies to determine intent and capabilities
|
||||
var pythonDependencies = new List<string>();
|
||||
if (context.Dependencies.TryGetValue("python", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
pythonDependencies.AddRange(deps);
|
||||
}
|
||||
else
|
||||
{
|
||||
pythonDependencies = [];
|
||||
}
|
||||
|
||||
if (pythonDependencies.Count == 0)
|
||||
{
|
||||
var requirementsDeps = await TryReadRequirementsDependenciesAsync(context, cancellationToken);
|
||||
if (requirementsDeps.Count > 0)
|
||||
{
|
||||
pythonDependencies.AddRange(requirementsDeps);
|
||||
reasoningChain.Add($"Parsed requirements.txt ({requirementsDeps.Count} deps)");
|
||||
}
|
||||
}
|
||||
|
||||
if (pythonDependencies.Count > 0)
|
||||
{
|
||||
foreach (var dep in pythonDependencies)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
@@ -200,20 +221,33 @@ public sealed class PythonSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
framework = normalizedDep;
|
||||
reasoningChain.Add($"Detected {normalizedDep} -> {intent}");
|
||||
}
|
||||
|
||||
// Baseline capabilities implied by the inferred intent/framework.
|
||||
if (mappedIntent == ApplicationIntent.WebServer)
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
else if (mappedIntent is ApplicationIntent.Worker or ApplicationIntent.StreamProcessor)
|
||||
builder.AddCapability(CapabilityClass.MessageQueue);
|
||||
}
|
||||
|
||||
// Check capability imports
|
||||
if (ImportCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Import {dep} -> {capability}");
|
||||
reasoningChain.Add($"Import {normalizedDep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serverless manifest hint (e.g., Serverless Framework / SAM markers discovered earlier in the scan).
|
||||
if (intent == ApplicationIntent.Unknown && context.ManifestPaths.ContainsKey("serverless"))
|
||||
{
|
||||
intent = ApplicationIntent.Serverless;
|
||||
reasoningChain.Add("Manifest hint: serverless -> Serverless");
|
||||
}
|
||||
|
||||
// Analyze entrypoint command for additional signals
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
@@ -353,4 +387,87 @@ public sealed class PythonSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-py-{hash[..12]}";
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> TryReadRequirementsDependenciesAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entrypoint = context.Specification.Entrypoint.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(entrypoint) || !entrypoint.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var directory = GetDirectory(entrypoint);
|
||||
if (directory is null)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var candidate = directory == "/" ? "/requirements.txt" : $"{directory}/requirements.txt";
|
||||
var content = await context.FileSystem.TryReadFileAsync(candidate, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var dependencies = new HashSet<string>(StringComparer.Ordinal);
|
||||
using var reader = new StringReader(content);
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0 || trimmed.StartsWith("#", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var commentIndex = trimmed.IndexOf('#');
|
||||
if (commentIndex >= 0)
|
||||
{
|
||||
trimmed = trimmed[..commentIndex].Trim();
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("-", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalized = NormalizeDependency(trimmed);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
dependencies.Add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencies.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return dependencies.OrderBy(static dep => dep, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static string? GetDirectory(string path)
|
||||
{
|
||||
var normalized = path.Replace('\\', '/');
|
||||
var lastSlash = normalized.LastIndexOf('/');
|
||||
if (lastSlash < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastSlash == 0)
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
return normalized[..lastSlash];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user