partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,137 @@
using StellaOps.AdvisoryAI.Explanation;
using System.ComponentModel.DataAnnotations;
namespace StellaOps.AdvisoryAI.WebService.Contracts;
/// <summary>
/// API request for Codex/Zastava companion explanation generation.
/// </summary>
public sealed record CompanionExplainRequest
{
[Required]
public required string FindingId { get; init; }
[Required]
public required string ArtifactDigest { get; init; }
[Required]
public required string Scope { get; init; }
[Required]
public required string ScopeId { get; init; }
public string ExplanationType { get; init; } = "full";
[Required]
public required string VulnerabilityId { get; init; }
public string? ComponentPurl { get; init; }
public bool PlainLanguage { get; init; }
public int MaxLength { get; init; }
public string? CorrelationId { get; init; }
public IReadOnlyList<CompanionRuntimeSignalRequest> RuntimeSignals { get; init; } = Array.Empty<CompanionRuntimeSignalRequest>();
public CodexCompanionRequest ToDomain()
{
if (!Enum.TryParse<ExplanationType>(ExplanationType, ignoreCase: true, out var parsedType))
{
parsedType = StellaOps.AdvisoryAI.Explanation.ExplanationType.Full;
}
return new CodexCompanionRequest
{
ExplanationRequest = new ExplanationRequest
{
FindingId = FindingId,
ArtifactDigest = ArtifactDigest,
Scope = Scope,
ScopeId = ScopeId,
ExplanationType = parsedType,
VulnerabilityId = VulnerabilityId,
ComponentPurl = ComponentPurl,
PlainLanguage = PlainLanguage,
MaxLength = MaxLength,
CorrelationId = CorrelationId,
},
RuntimeSignals = RuntimeSignals.Select(static signal => new CompanionRuntimeSignal
{
Source = signal.Source,
Signal = signal.Signal,
Value = signal.Value,
Path = signal.Path,
Confidence = signal.Confidence,
}).ToArray(),
};
}
}
/// <summary>
/// Runtime signal request payload.
/// </summary>
public sealed record CompanionRuntimeSignalRequest
{
[Required]
public required string Source { get; init; }
[Required]
public required string Signal { get; init; }
[Required]
public required string Value { get; init; }
public string? Path { get; init; }
public double Confidence { get; init; }
}
/// <summary>
/// API response for Codex/Zastava companion explanation generation.
/// </summary>
public sealed record CompanionExplainResponse
{
public required string CompanionId { get; init; }
public required string CompanionHash { get; init; }
public required ExplainResponse Explanation { get; init; }
public required ExplainSummaryResponse CompanionSummary { get; init; }
public required IReadOnlyList<CompanionRuntimeSignalResponse> RuntimeHighlights { get; init; }
public static CompanionExplainResponse FromDomain(CodexCompanionResponse response)
{
return new CompanionExplainResponse
{
CompanionId = response.CompanionId,
CompanionHash = response.CompanionHash,
Explanation = ExplainResponse.FromDomain(response.Explanation),
CompanionSummary = new ExplainSummaryResponse
{
Line1 = response.CompanionSummary.Line1,
Line2 = response.CompanionSummary.Line2,
Line3 = response.CompanionSummary.Line3,
},
RuntimeHighlights = response.RuntimeHighlights.Select(static signal => new CompanionRuntimeSignalResponse
{
Source = signal.Source,
Signal = signal.Signal,
Value = signal.Value,
Path = signal.Path,
Confidence = signal.Confidence,
}).ToArray(),
};
}
}
/// <summary>
/// Runtime signal response payload.
/// </summary>
public sealed record CompanionRuntimeSignalResponse
{
public required string Source { get; init; }
public required string Signal { get; init; }
public required string Value { get; init; }
public string? Path { get; init; }
public double Confidence { get; init; }
}

View File

@@ -42,6 +42,7 @@ builder.Configuration
builder.Services.AddAdvisoryAiCore(builder.Configuration);
builder.Services.AddAdvisoryChat(builder.Configuration);
builder.Services.TryAddSingleton<ICodexCompanionService, CodexZastavaCompanionService>();
// Authorization service
builder.Services.AddSingleton<StellaOps.AdvisoryAI.WebService.Services.IAuthorizationService, StellaOps.AdvisoryAI.WebService.Services.HeaderBasedAuthorizationService>();
@@ -140,6 +141,9 @@ app.MapPost("/v1/advisory-ai/explain", HandleExplain)
app.MapGet("/v1/advisory-ai/explain/{explanationId}/replay", HandleExplanationReplay)
.RequireRateLimiting("advisory-ai");
app.MapPost("/v1/advisory-ai/companion/explain", HandleCompanionExplain)
.RequireRateLimiting("advisory-ai");
// Remediation endpoints (SPRINT_20251226_016_AI_remedy_autopilot)
app.MapPost("/v1/advisory-ai/remediation/plan", HandleRemediationPlan)
.RequireRateLimiting("advisory-ai");
@@ -383,7 +387,9 @@ static bool EnsureExplainAuthorized(HttpContext context)
.SelectMany(value => value?.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? [])
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return allowed.Contains("advisory:run") || allowed.Contains("advisory:explain");
return allowed.Contains("advisory:run")
|| allowed.Contains("advisory:explain")
|| allowed.Contains("advisory:companion");
}
// ZASTAVA-13: POST /v1/advisory-ai/explain
@@ -450,6 +456,40 @@ static async Task<IResult> HandleExplanationReplay(
}
}
// SPRINT_20260208_003: POST /v1/advisory-ai/companion/explain
static async Task<IResult> HandleCompanionExplain(
HttpContext httpContext,
CompanionExplainRequest request,
ICodexCompanionService companionService,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.companion_explain", ActivityKind.Server);
activity?.SetTag("advisory.finding_id", request.FindingId);
activity?.SetTag("advisory.vulnerability_id", request.VulnerabilityId);
activity?.SetTag("advisory.runtime_signal_count", request.RuntimeSignals.Count);
if (!EnsureExplainAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
try
{
var domainRequest = request.ToDomain();
var result = await companionService.GenerateAsync(domainRequest, cancellationToken).ConfigureAwait(false);
activity?.SetTag("advisory.companion_id", result.CompanionId);
activity?.SetTag("advisory.companion_hash", result.CompanionHash);
activity?.SetTag("advisory.explanation_id", result.Explanation.ExplanationId);
return Results.Ok(CompanionExplainResponse.FromDomain(result));
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
static bool EnsureRemediationAuthorized(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))

View File

@@ -6,3 +6,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/StellaOps.AdvisoryAI.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260208_003-WEB | DONE | Companion explain endpoint/contracts for Codex/Zastava flow. |

View File

@@ -0,0 +1,174 @@
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.AdvisoryAI.Explanation;
/// <summary>
/// Runtime signal emitted by Zastava or compatible observers.
/// </summary>
public sealed record CompanionRuntimeSignal
{
public required string Source { get; init; }
public required string Signal { get; init; }
public required string Value { get; init; }
public string? Path { get; init; }
public double Confidence { get; init; }
}
/// <summary>
/// Request for Codex/Zastava companion explanation composition.
/// </summary>
public sealed record CodexCompanionRequest
{
public required ExplanationRequest ExplanationRequest { get; init; }
public IReadOnlyList<CompanionRuntimeSignal> RuntimeSignals { get; init; } = Array.Empty<CompanionRuntimeSignal>();
}
/// <summary>
/// Response containing base explanation plus deterministic runtime highlights.
/// </summary>
public sealed record CodexCompanionResponse
{
public required string CompanionId { get; init; }
public required string CompanionHash { get; init; }
public required ExplanationResult Explanation { get; init; }
public required ExplanationSummary CompanionSummary { get; init; }
public required IReadOnlyList<CompanionRuntimeSignal> RuntimeHighlights { get; init; }
}
/// <summary>
/// Service that combines explanation output with Zastava runtime signals.
/// </summary>
public interface ICodexCompanionService
{
Task<CodexCompanionResponse> GenerateAsync(
CodexCompanionRequest request,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Deterministic implementation of the Codex/Zastava companion.
/// </summary>
public sealed class CodexZastavaCompanionService : ICodexCompanionService
{
private readonly IExplanationGenerator _explanationGenerator;
public CodexZastavaCompanionService(IExplanationGenerator explanationGenerator)
{
_explanationGenerator = explanationGenerator ?? throw new ArgumentNullException(nameof(explanationGenerator));
}
public async Task<CodexCompanionResponse> GenerateAsync(
CodexCompanionRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.ExplanationRequest);
var explanation = await _explanationGenerator
.GenerateAsync(request.ExplanationRequest, cancellationToken)
.ConfigureAwait(false);
var highlights = NormalizeSignals(request.RuntimeSignals)
.Take(5)
.ToArray();
var companionSummary = BuildCompanionSummary(explanation.Summary, highlights);
var companionHash = ComputeCompanionHash(explanation.OutputHash, highlights);
return new CodexCompanionResponse
{
CompanionId = $"companion:{companionHash}",
CompanionHash = companionHash,
Explanation = explanation,
CompanionSummary = companionSummary,
RuntimeHighlights = highlights,
};
}
private static IReadOnlyList<CompanionRuntimeSignal> NormalizeSignals(
IReadOnlyList<CompanionRuntimeSignal> signals)
{
if (signals.Count == 0)
{
return Array.Empty<CompanionRuntimeSignal>();
}
var deduplicated = new Dictionary<string, CompanionRuntimeSignal>(StringComparer.Ordinal);
foreach (var signal in signals)
{
if (string.IsNullOrWhiteSpace(signal.Source) ||
string.IsNullOrWhiteSpace(signal.Signal) ||
string.IsNullOrWhiteSpace(signal.Value))
{
continue;
}
var normalized = new CompanionRuntimeSignal
{
Source = signal.Source.Trim(),
Signal = signal.Signal.Trim(),
Value = signal.Value.Trim(),
Path = string.IsNullOrWhiteSpace(signal.Path) ? null : signal.Path.Trim(),
Confidence = Math.Clamp(signal.Confidence, 0, 1),
};
var key = string.Join("|", normalized.Source, normalized.Signal, normalized.Value, normalized.Path ?? string.Empty);
if (deduplicated.TryGetValue(key, out var existing))
{
deduplicated[key] = normalized.Confidence >= existing.Confidence
? normalized
: existing;
}
else
{
deduplicated[key] = normalized;
}
}
return deduplicated.Values
.OrderByDescending(static value => value.Confidence)
.ThenBy(static value => value.Source, StringComparer.Ordinal)
.ThenBy(static value => value.Signal, StringComparer.Ordinal)
.ThenBy(static value => value.Value, StringComparer.Ordinal)
.ThenBy(static value => value.Path, StringComparer.Ordinal)
.ToArray();
}
private static ExplanationSummary BuildCompanionSummary(
ExplanationSummary baseSummary,
IReadOnlyList<CompanionRuntimeSignal> highlights)
{
var line2 = highlights.Count == 0
? "No Zastava runtime signals were provided; verdict is based on static evidence."
: $"Runtime signal {highlights[0].Source}/{highlights[0].Signal} indicates '{highlights[0].Value}'.";
return new ExplanationSummary
{
Line1 = $"Companion: {baseSummary.Line1}",
Line2 = line2,
Line3 = baseSummary.Line3,
};
}
private static string ComputeCompanionHash(
string explanationOutputHash,
IReadOnlyList<CompanionRuntimeSignal> highlights)
{
var builder = new StringBuilder();
builder.Append(explanationOutputHash).Append('\n');
foreach (var highlight in highlights)
{
builder.Append(highlight.Source).Append('|')
.Append(highlight.Signal).Append('|')
.Append(highlight.Value).Append('|')
.Append(highlight.Path ?? string.Empty).Append('|')
.Append(highlight.Confidence.ToString("F4", System.Globalization.CultureInfo.InvariantCulture))
.Append('\n');
}
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
return Convert.ToHexString(hash).ToLowerInvariant();
}
}

View File

@@ -11,3 +11,6 @@ Source of truth: `docs/implplan/SPRINT_20260113_005_ADVISORYAI_controlled_conver
| AIAI-CHAT-AUDIT-0001 | DONE | Persist chat audit tables and logger. |
| AUDIT-TESTGAP-ADVISORYAI-0001 | DONE | Added worker and unified plugin adapter tests. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260208_003-CORE | DONE | Codex/Zastava companion core service for deterministic runtime-aware explanation composition. |

View File

@@ -0,0 +1,201 @@
using FluentAssertions;
using StellaOps.AdvisoryAI.Explanation;
using StellaOps.AdvisoryAI.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.AdvisoryAI.Companion.Tests;
public sealed class CodexZastavaCompanionServiceTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GenerateAsync_IsDeterministicForPermutedSignals()
{
var explanation = CreateExplanationResult();
var service = new CodexZastavaCompanionService(new StubExplanationGenerator(explanation));
var explanationRequest = CreateExplanationRequest();
var requestA = new CodexCompanionRequest
{
ExplanationRequest = explanationRequest,
RuntimeSignals =
[
new CompanionRuntimeSignal { Source = "zastava", Signal = "entrypoint", Value = "public-api", Path = "/api", Confidence = 0.60 },
new CompanionRuntimeSignal { Source = "zastava", Signal = "reachable", Value = "true", Path = "/lib/a.cs", Confidence = 0.95 },
new CompanionRuntimeSignal { Source = "zastava", Signal = "reachable", Value = "true", Path = "/lib/a.cs", Confidence = 0.10 },
new CompanionRuntimeSignal { Source = "runtime", Signal = "exploit-path", Value = "direct", Path = "/proc", Confidence = 0.85 },
],
};
var requestB = new CodexCompanionRequest
{
ExplanationRequest = explanationRequest,
RuntimeSignals =
[
new CompanionRuntimeSignal { Source = "runtime", Signal = "exploit-path", Value = "direct", Path = "/proc", Confidence = 0.85 },
new CompanionRuntimeSignal { Source = "zastava", Signal = "reachable", Value = "true", Path = "/lib/a.cs", Confidence = 0.10 },
new CompanionRuntimeSignal { Source = "zastava", Signal = "entrypoint", Value = "public-api", Path = "/api", Confidence = 0.60 },
new CompanionRuntimeSignal { Source = "zastava", Signal = "reachable", Value = "true", Path = "/lib/a.cs", Confidence = 0.95 },
],
};
var resultA = await service.GenerateAsync(requestA);
var resultB = await service.GenerateAsync(requestB);
resultA.CompanionHash.Should().Be(resultB.CompanionHash);
resultA.RuntimeHighlights.Should().HaveCount(3);
resultA.RuntimeHighlights[0].Signal.Should().Be("reachable");
resultA.RuntimeHighlights[0].Confidence.Should().Be(0.95);
resultA.CompanionSummary.Line1.Should().StartWith("Companion:");
resultA.CompanionSummary.Line2.Should().Contain("zastava/reachable");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GenerateAsync_WithoutSignals_UsesStaticEvidenceSummary()
{
var explanation = CreateExplanationResult();
var service = new CodexZastavaCompanionService(new StubExplanationGenerator(explanation));
var response = await service.GenerateAsync(new CodexCompanionRequest
{
ExplanationRequest = CreateExplanationRequest(),
RuntimeSignals = [],
});
response.RuntimeHighlights.Should().BeEmpty();
response.CompanionSummary.Line2.Should().Contain("No Zastava runtime signals");
response.CompanionId.Should().StartWith("companion:");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompanionContracts_MapDomainRoundTrip()
{
var request = new CompanionExplainRequest
{
FindingId = "finding-1",
ArtifactDigest = "sha256:aaa",
Scope = "image",
ScopeId = "img:v1",
ExplanationType = "what",
VulnerabilityId = "CVE-2026-0001",
ComponentPurl = "pkg:npm/a@1.0.0",
PlainLanguage = true,
MaxLength = 120,
CorrelationId = "corr-1",
RuntimeSignals =
[
new CompanionRuntimeSignalRequest
{
Source = "zastava",
Signal = "reachable",
Value = "true",
Path = "/app/main.cs",
Confidence = 0.8,
},
],
};
var domainRequest = request.ToDomain();
domainRequest.ExplanationRequest.ExplanationType.Should().Be(ExplanationType.What);
domainRequest.RuntimeSignals.Should().HaveCount(1);
var domainResponse = new CodexCompanionResponse
{
CompanionId = "companion:abc",
CompanionHash = "abc",
Explanation = CreateExplanationResult(),
CompanionSummary = new ExplanationSummary
{
Line1 = "Companion: line1",
Line2 = "Companion: line2",
Line3 = "Companion: line3",
},
RuntimeHighlights =
[
new CompanionRuntimeSignal
{
Source = "zastava",
Signal = "reachable",
Value = "true",
Path = "/app/main.cs",
Confidence = 0.8,
},
],
};
var apiResponse = CompanionExplainResponse.FromDomain(domainResponse);
apiResponse.CompanionId.Should().Be("companion:abc");
apiResponse.Explanation.ExplanationId.Should().Be(CreateExplanationResult().ExplanationId);
apiResponse.RuntimeHighlights.Should().HaveCount(1);
}
private static ExplanationRequest CreateExplanationRequest()
{
return new ExplanationRequest
{
FindingId = "finding-1",
ArtifactDigest = "sha256:aaa",
Scope = "image",
ScopeId = "img:v1",
ExplanationType = ExplanationType.Full,
VulnerabilityId = "CVE-2026-0001",
ComponentPurl = "pkg:npm/a@1.0.0",
PlainLanguage = false,
MaxLength = 0,
CorrelationId = "corr-1",
};
}
private static ExplanationResult CreateExplanationResult()
{
return new ExplanationResult
{
ExplanationId = "sha256:1111111111111111111111111111111111111111111111111111111111111111",
Content = "example explanation",
Summary = new ExplanationSummary
{
Line1 = "Vulnerability is present.",
Line2 = "It is reachable from runtime entrypoints.",
Line3 = "Patch to the recommended fixed version.",
},
Citations = [],
ConfidenceScore = 0.9,
CitationRate = 1.0,
Authority = ExplanationAuthority.EvidenceBacked,
EvidenceRefs = ["ev-1"],
ModelId = "model-x",
PromptTemplateVersion = "explain-v1",
InputHashes = ["hash-a", "hash-b", "hash-c"],
GeneratedAt = "2026-02-08T00:00:00.0000000Z",
OutputHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
};
}
private sealed class StubExplanationGenerator : IExplanationGenerator
{
private readonly ExplanationResult _result;
public StubExplanationGenerator(ExplanationResult result)
{
_result = result;
}
public Task<ExplanationResult> GenerateAsync(ExplanationRequest request, CancellationToken cancellationToken = default)
{
return Task.FromResult(_result);
}
public Task<ExplanationResult> ReplayAsync(string explanationId, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<bool> ValidateAsync(ExplanationResult result, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,175 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.AdvisoryAI.Explanation;
using StellaOps.AdvisoryAI.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.AdvisoryAI.Companion.Tests;
[Trait("Category", TestCategories.Integration)]
public sealed class CompanionExplainEndpointTests
{
[Fact]
public async Task CompanionExplain_WithoutScopes_ReturnsForbidden()
{
await using var factory = new WebApplicationFactory<StellaOps.AdvisoryAI.WebService.Program>();
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Client", "companion-tests");
var request = new CompanionExplainRequest
{
FindingId = "finding-1",
ArtifactDigest = "sha256:aaa",
Scope = "tenant",
ScopeId = "tenant-a",
VulnerabilityId = "CVE-2026-0001",
};
var response = await client.PostAsJsonAsync("/v1/advisory-ai/companion/explain", request);
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}
[Fact]
public async Task CompanionExplain_WithScope_MapsRequestAndReturnsCompanionResponse()
{
var stub = new CapturingCompanionService();
await using var factory = CreateFactory(stub);
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Client", "companion-tests");
client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:companion");
var request = new CompanionExplainRequest
{
FindingId = "finding-1",
ArtifactDigest = "sha256:aaa",
Scope = "tenant",
ScopeId = "tenant-a",
ExplanationType = "what",
VulnerabilityId = "CVE-2026-0001",
RuntimeSignals =
[
new CompanionRuntimeSignalRequest
{
Source = "zastava",
Signal = "reachable",
Value = "true",
Path = "/app/main.cs",
Confidence = 0.9,
},
],
};
var response = await client.PostAsJsonAsync("/v1/advisory-ai/companion/explain", request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var payload = await response.Content.ReadFromJsonAsync<CompanionExplainResponse>();
payload.Should().NotBeNull();
payload!.CompanionId.Should().Be("companion:stub");
payload.RuntimeHighlights.Should().ContainSingle();
stub.LastRequest.Should().NotBeNull();
stub.LastRequest!.ExplanationRequest.Scope.Should().Be("tenant");
stub.LastRequest.ExplanationRequest.ScopeId.Should().Be("tenant-a");
stub.LastRequest.ExplanationRequest.ExplanationType.Should().Be(ExplanationType.What);
}
[Fact]
public async Task CompanionExplain_WhenServiceRejectsRequest_ReturnsBadRequest()
{
await using var factory = CreateFactory(new ThrowingCompanionService());
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Client", "companion-tests");
client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:companion");
var request = new CompanionExplainRequest
{
FindingId = "finding-1",
ArtifactDigest = "sha256:aaa",
Scope = "tenant",
ScopeId = "tenant-a",
VulnerabilityId = "CVE-2026-0001",
};
var response = await client.PostAsJsonAsync("/v1/advisory-ai/companion/explain", request);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
private static WebApplicationFactory<StellaOps.AdvisoryAI.WebService.Program> CreateFactory(ICodexCompanionService service)
{
return new WebApplicationFactory<StellaOps.AdvisoryAI.WebService.Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton(service);
services.AddSingleton<ICodexCompanionService>(service);
});
});
}
private sealed class CapturingCompanionService : ICodexCompanionService
{
public CodexCompanionRequest? LastRequest { get; private set; }
public Task<CodexCompanionResponse> GenerateAsync(CodexCompanionRequest request, CancellationToken cancellationToken = default)
{
LastRequest = request;
return Task.FromResult(new CodexCompanionResponse
{
CompanionId = "companion:stub",
CompanionHash = "stub",
Explanation = new ExplanationResult
{
ExplanationId = "sha256:stub",
Content = "stub explanation",
Summary = new ExplanationSummary
{
Line1 = "line1",
Line2 = "line2",
Line3 = "line3",
},
Citations = [],
ConfidenceScore = 1.0,
CitationRate = 1.0,
Authority = ExplanationAuthority.EvidenceBacked,
EvidenceRefs = [],
ModelId = "stub-model",
PromptTemplateVersion = "stub-template",
InputHashes = [],
GeneratedAt = "2026-02-08T00:00:00.0000000Z",
OutputHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
CompanionSummary = new ExplanationSummary
{
Line1 = "Companion: line1",
Line2 = "Companion: line2",
Line3 = "Companion: line3",
},
RuntimeHighlights =
[
new CompanionRuntimeSignal
{
Source = "zastava",
Signal = "reachable",
Value = "true",
Path = "/app/main.cs",
Confidence = 0.9,
},
],
});
}
}
private sealed class ThrowingCompanionService : ICodexCompanionService
{
public Task<CodexCompanionResponse> GenerateAsync(CodexCompanionRequest request, CancellationToken cancellationToken = default)
{
throw new InvalidOperationException("invalid companion request");
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.WebService\StellaOps.AdvisoryAI.WebService.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,3 +6,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260208_003-TESTS | DONE | Deterministic Codex/Zastava companion service, contract tests, and endpoint integration tests. |