feat(scanner): Implement Deno analyzer and associated tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added Deno analyzer with comprehensive metadata and evidence structure.
- Created a detailed implementation plan for Sprint 130 focusing on Deno analyzer.
- Introduced AdvisoryAiGuardrailOptions for managing guardrail configurations.
- Developed GuardrailPhraseLoader for loading blocked phrases from JSON files.
- Implemented tests for AdvisoryGuardrailOptions binding and phrase loading.
- Enhanced telemetry for Advisory AI with metrics tracking.
- Added VexObservationProjectionService for querying VEX observations.
- Created extensive tests for VexObservationProjectionService functionality.
- Introduced Ruby language analyzer with tests for simple and complex workspaces.
- Added Ruby application fixtures for testing purposes.
This commit is contained in:
master
2025-11-12 10:01:54 +02:00
parent 0e8655cbb1
commit babb81af52
75 changed files with 3346 additions and 187 deletions

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace StellaOps.AdvisoryAI.Hosting;
public sealed class AdvisoryAiGuardrailOptions
{
private const int DefaultMaxPromptLength = 16000;
public int? MaxPromptLength { get; set; } = DefaultMaxPromptLength;
public bool RequireCitations { get; set; } = true;
public string? BlockedPhraseFile { get; set; }
= null;
public List<string> BlockedPhrases { get; set; } = new();
}

View File

@@ -18,6 +18,8 @@ public sealed class AdvisoryAiServiceOptions
public AdvisoryAiInferenceOptions Inference { get; set; } = new();
public AdvisoryAiGuardrailOptions Guardrails { get; set; } = new();
internal string ResolveQueueDirectory(string contentRoot)
{
ArgumentException.ThrowIfNullOrWhiteSpace(contentRoot);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using StellaOps.AdvisoryAI.Inference;
@@ -71,6 +72,15 @@ internal static class AdvisoryAiServiceOptionsValidator
}
}
options.Guardrails ??= new AdvisoryAiGuardrailOptions();
options.Guardrails.BlockedPhrases ??= new List<string>();
if (options.Guardrails.MaxPromptLength.HasValue && options.Guardrails.MaxPromptLength.Value <= 0)
{
error = "AdvisoryAI:Guardrails:MaxPromptLength must be greater than zero when specified.";
return false;
}
error = null;
return true;
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Hosting;
internal static class GuardrailPhraseLoader
{
public static IReadOnlyCollection<string> Load(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("Guardrail phrase file path must be provided.", nameof(path));
}
using var stream = File.OpenRead(path);
using var document = JsonDocument.Parse(stream);
var root = document.RootElement;
return root.ValueKind switch
{
JsonValueKind.Array => ExtractValues(root),
JsonValueKind.Object when root.TryGetProperty("phrases", out var phrases) => ExtractValues(phrases),
_ => throw new InvalidDataException($"Guardrail phrase file {path} must be a JSON array or object with a phrases array."),
};
}
private static IReadOnlyCollection<string> ExtractValues(JsonElement element)
{
var phrases = new List<string>();
foreach (var item in element.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.String)
{
var value = item.GetString();
if (!string.IsNullOrWhiteSpace(value))
{
phrases.Add(value.Trim());
}
}
}
return phrases;
}
}

View File

@@ -1,13 +1,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Caching;
using StellaOps.AdvisoryAI.DependencyInjection;
using StellaOps.AdvisoryAI.Inference;
using StellaOps.AdvisoryAI.Metrics;
using StellaOps.AdvisoryAI.Guardrails;
using StellaOps.AdvisoryAI.Outputs;
using StellaOps.AdvisoryAI.Providers;
using StellaOps.AdvisoryAI.Queue;
@@ -86,6 +91,12 @@ public static class ServiceCollectionExtensions
services.AddAdvisoryPipeline();
services.AddAdvisoryPipelineInfrastructure();
services.AddOptions<AdvisoryGuardrailOptions>()
.Configure<IOptions<AdvisoryAiServiceOptions>, IHostEnvironment>((options, aiOptions, environment) =>
{
ApplyGuardrailConfiguration(options, aiOptions.Value.Guardrails, environment);
});
services.Replace(ServiceDescriptor.Singleton<IAdvisoryTaskQueue, FileSystemAdvisoryTaskQueue>());
services.Replace(ServiceDescriptor.Singleton<IAdvisoryPlanCache, FileSystemAdvisoryPlanCache>());
services.Replace(ServiceDescriptor.Singleton<IAdvisoryOutputStore, FileSystemAdvisoryOutputStore>());
@@ -93,4 +104,87 @@ public static class ServiceCollectionExtensions
return services;
}
private static void ApplyGuardrailConfiguration(
AdvisoryGuardrailOptions target,
AdvisoryAiGuardrailOptions? source,
IHostEnvironment? environment)
{
if (source is null)
{
return;
}
if (source.MaxPromptLength.HasValue && source.MaxPromptLength.Value > 0)
{
target.MaxPromptLength = source.MaxPromptLength.Value;
}
target.RequireCitations = source.RequireCitations;
var defaults = target.BlockedPhrases.ToList();
var merged = new SortedSet<string>(defaults, StringComparer.OrdinalIgnoreCase);
if (source.BlockedPhrases is { Count: > 0 })
{
foreach (var phrase in source.BlockedPhrases)
{
if (!string.IsNullOrWhiteSpace(phrase))
{
merged.Add(phrase.Trim());
}
}
}
if (!string.IsNullOrWhiteSpace(source.BlockedPhraseFile))
{
var resolvedPath = ResolveGuardrailPath(source.BlockedPhraseFile!, environment);
foreach (var phrase in GuardrailPhraseLoader.Load(resolvedPath))
{
if (!string.IsNullOrWhiteSpace(phrase))
{
merged.Add(phrase.Trim());
}
}
}
if (merged.Count == 0)
{
return;
}
target.BlockedPhrases.Clear();
foreach (var phrase in merged)
{
target.BlockedPhrases.Add(phrase);
}
}
private static string ResolveGuardrailPath(string configuredPath, IHostEnvironment? environment)
{
var trimmed = configuredPath.Trim();
if (Path.IsPathRooted(trimmed))
{
if (!File.Exists(trimmed))
{
throw new FileNotFoundException($"Guardrail phrase file {trimmed} was not found.", trimmed);
}
return trimmed;
}
var root = environment?.ContentRootPath;
if (string.IsNullOrWhiteSpace(root))
{
root = AppContext.BaseDirectory;
}
var resolved = Path.GetFullPath(Path.Combine(root!, trimmed));
if (!File.Exists(resolved))
{
throw new FileNotFoundException($"Guardrail phrase file {resolved} was not found.", resolved);
}
return resolved;
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Guardrails;
using StellaOps.AdvisoryAI.Hosting;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class AdvisoryGuardrailOptionsBindingTests
{
[Fact]
public async Task AddAdvisoryAiCore_ConfiguresGuardrailOptionsFromServiceOptions()
{
var tempRoot = CreateTempDirectory();
var phrasePath = Path.Combine(tempRoot, "guardrail-phrases.json");
await File.WriteAllTextAsync(phrasePath, "{\n \"phrases\": [\"extract secrets\", \"dump cache\"]\n}");
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["AdvisoryAI:Guardrails:MaxPromptLength"] = "32000",
["AdvisoryAI:Guardrails:RequireCitations"] = "false",
["AdvisoryAI:Guardrails:BlockedPhraseFile"] = "guardrail-phrases.json",
["AdvisoryAI:Guardrails:BlockedPhrases:0"] = "custom override"
})
.Build();
var services = new ServiceCollection();
services.AddSingleton<IHostEnvironment>(new FakeHostEnvironment(tempRoot));
services.AddAdvisoryAiCore(configuration);
await using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<AdvisoryGuardrailOptions>>().Value;
options.MaxPromptLength.Should().Be(32000);
options.RequireCitations.Should().BeFalse();
options.BlockedPhrases.Should().Contain("custom override");
options.BlockedPhrases.Should().Contain("extract secrets");
options.BlockedPhrases.Should().Contain("dump cache");
}
[Fact]
public async Task AddAdvisoryAiCore_ThrowsWhenPhraseFileMissing()
{
var tempRoot = CreateTempDirectory();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["AdvisoryAI:Guardrails:BlockedPhraseFile"] = "missing.json"
})
.Build();
var services = new ServiceCollection();
services.AddSingleton<IHostEnvironment>(new FakeHostEnvironment(tempRoot));
services.AddAdvisoryAiCore(configuration);
await using var provider = services.BuildServiceProvider();
var action = () => provider.GetRequiredService<IOptions<AdvisoryGuardrailOptions>>().Value;
action.Should().Throw<FileNotFoundException>();
}
private static string CreateTempDirectory()
{
var path = Path.Combine(Path.GetTempPath(), "advisoryai-guardrails", Guid.NewGuid().ToString("n"));
Directory.CreateDirectory(path);
return path;
}
private sealed class FakeHostEnvironment : IHostEnvironment
{
public FakeHostEnvironment(string contentRoot)
{
ContentRootPath = contentRoot;
}
public string EnvironmentName { get; set; } = Environments.Development;
public string ApplicationName { get; set; } = "StellaOps.AdvisoryAI.Tests";
public string ContentRootPath { get; set; }
= string.Empty;
}
}

View File

@@ -7,10 +7,12 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />