consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,570 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Determinism;
using StellaOps.OpsMemory.Models;
using StellaOps.OpsMemory.Playbook;
using StellaOps.OpsMemory.Storage;
using StellaOps.OpsMemory.WebService.Security;
using StellaOps.Auth.ServerIntegration.Tenancy;
using System.Collections.Immutable;
namespace StellaOps.OpsMemory.WebService.Endpoints;
/// <summary>
/// OpsMemory API endpoints for decision recording and playbook suggestions.
/// Sprint: SPRINT_20260107_006_004 Task OM-006
/// </summary>
public static class OpsMemoryEndpoints
{
/// <summary>
/// Maps OpsMemory endpoints.
/// </summary>
public static void MapOpsMemoryEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/opsmemory")
.WithTags("OpsMemory")
.RequireAuthorization(OpsMemoryPolicies.Read)
.RequireTenant();
group.MapPost("/decisions", RecordDecisionAsync)
.WithName("RecordDecision")
.WithDescription("Records a security decision (accept, suppress, mitigate, escalate) for a CVE and component combination. The decision is stored with situational context for future playbook learning and similarity matching. Returns 201 Created with the new memory ID.")
.RequireAuthorization(OpsMemoryPolicies.Write);
group.MapGet("/decisions/{memoryId}", GetDecisionAsync)
.WithName("GetDecision")
.WithDescription("Returns the full decision record for a specific memory ID including situational context, decision details, mitigation information, and outcome if recorded. Returns 404 if not found.");
group.MapPost("/decisions/{memoryId}/outcome", RecordOutcomeAsync)
.WithName("RecordOutcome")
.WithDescription("Records the observed outcome of a previously stored security decision, capturing resolution time, actual impact, lessons learned, and whether the decision would be repeated. Returns 200 with the updated memory ID.")
.RequireAuthorization(OpsMemoryPolicies.Write);
group.MapGet("/suggestions", GetSuggestionsAsync)
.WithName("GetPlaybookSuggestions")
.WithDescription("Returns ranked playbook suggestions for a given situational context by matching against historical decisions using similarity scoring. Each suggestion includes confidence, success rate, and evidence from past decisions.");
group.MapGet("/decisions", QueryDecisionsAsync)
.WithName("QueryDecisions")
.WithDescription("Queries stored security decisions with optional filters by CVE ID, component prefix, action type, and outcome status. Supports cursor-based pagination.");
group.MapGet("/stats", GetStatsAsync)
.WithName("GetOpsMemoryStats")
.WithDescription("Returns aggregated decision statistics for the tenant including total decision count, decisions with recorded outcomes, and overall success rate.");
}
/// <summary>
/// Record a new security decision.
/// </summary>
private static async Task<Results<Created<RecordDecisionResponse>, BadRequest<ProblemDetails>>> RecordDecisionAsync(
RecordDecisionRequest request,
IOpsMemoryStore store,
TimeProvider timeProvider,
IGuidProvider guidProvider,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.TenantId))
{
return TypedResults.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "TenantId is required"
});
}
if (!Enum.TryParse<DecisionAction>(request.Action, ignoreCase: true, out var action))
{
return TypedResults.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = $"Invalid action: {request.Action}. Valid values: {string.Join(", ", Enum.GetNames<DecisionAction>())}"
});
}
var record = new OpsMemoryRecord
{
MemoryId = guidProvider.NewGuid().ToString("N"),
TenantId = request.TenantId,
RecordedAt = timeProvider.GetUtcNow(),
Situation = new SituationContext
{
CveId = request.CveId,
Component = request.ComponentPurl,
Severity = request.Severity,
Reachability = ParseReachability(request.Reachability),
EpssScore = request.EpssScore,
CvssScore = request.CvssScore,
ContextTags = request.ContextTags?.ToImmutableArray() ?? ImmutableArray<string>.Empty
},
Decision = new DecisionRecord
{
Action = action,
Rationale = request.Rationale ?? string.Empty,
DecidedBy = request.DecidedBy ?? "unknown",
DecidedAt = timeProvider.GetUtcNow(),
PolicyReference = request.PolicyReference,
Mitigation = !string.IsNullOrEmpty(request.MitigationType)
? new MitigationDetails
{
Type = request.MitigationType,
Description = request.MitigationDetails ?? string.Empty
}
: null
}
};
var saved = await store.RecordDecisionAsync(record, cancellationToken);
return TypedResults.Created(
$"/api/v1/opsmemory/decisions/{saved.MemoryId}",
new RecordDecisionResponse
{
MemoryId = saved.MemoryId,
RecordedAt = saved.RecordedAt
});
}
/// <summary>
/// Get a specific decision.
/// </summary>
private static async Task<Results<Ok<DecisionDetailsResponse>, NotFound>> GetDecisionAsync(
string memoryId,
string tenantId,
IOpsMemoryStore store,
CancellationToken cancellationToken)
{
var record = await store.GetByIdAsync(memoryId, tenantId, cancellationToken);
if (record == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(MapToDetailsResponse(record));
}
/// <summary>
/// Record the outcome of a decision.
/// </summary>
private static async Task<Results<Ok<OutcomeRecordedResponse>, NotFound, BadRequest<ProblemDetails>>> RecordOutcomeAsync(
string memoryId,
string tenantId,
RecordOutcomeRequest request,
IOpsMemoryStore store,
TimeProvider timeProvider,
CancellationToken cancellationToken)
{
if (!Enum.TryParse<OutcomeStatus>(request.Status, ignoreCase: true, out var status))
{
return TypedResults.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = $"Invalid status: {request.Status}. Valid values: {string.Join(", ", Enum.GetNames<OutcomeStatus>())}"
});
}
var outcome = new OutcomeRecord
{
Status = status,
ResolutionTime = request.ResolutionTimeMinutes.HasValue
? TimeSpan.FromMinutes(request.ResolutionTimeMinutes.Value)
: null,
ActualImpact = request.ActualImpact,
LessonsLearned = request.LessonsLearned,
RecordedBy = request.RecordedBy ?? "unknown",
RecordedAt = timeProvider.GetUtcNow()
};
var updated = await store.RecordOutcomeAsync(memoryId, tenantId, outcome, cancellationToken);
if (updated == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(new OutcomeRecordedResponse
{
MemoryId = memoryId,
Status = status.ToString(),
RecordedAt = outcome.RecordedAt
});
}
/// <summary>
/// Get playbook suggestions for a situation.
/// </summary>
private static async Task<Ok<PlaybookSuggestionsResponse>> GetSuggestionsAsync(
string tenantId,
PlaybookSuggestionService suggestionService,
string? cveId,
string? componentPurl,
string? severity,
string? reachability,
double? epssScore,
double? cvssScore,
int? limit,
CancellationToken cancellationToken)
{
var request = new PlaybookSuggestionRequest
{
TenantId = tenantId,
Situation = new SituationContext
{
CveId = cveId,
Component = componentPurl,
Severity = severity,
Reachability = ParseReachability(reachability),
EpssScore = epssScore,
CvssScore = cvssScore
},
MaxSuggestions = limit ?? 3
};
var result = await suggestionService.GetSuggestionsAsync(request, cancellationToken);
return TypedResults.Ok(new PlaybookSuggestionsResponse
{
Suggestions = result.Suggestions.Select(s => new PlaybookSuggestionDto
{
SuggestedAction = s.Action.ToString(),
Confidence = s.Confidence,
Rationale = s.Rationale,
SuccessRate = s.SuccessRate,
SimilarDecisionCount = s.SimilarDecisionCount,
AverageResolutionTimeMinutes = s.AverageResolutionTime?.TotalMinutes,
Evidence = s.Evidence.Select(e => new PlaybookEvidenceDto
{
MemoryId = e.MemoryId,
Similarity = e.Similarity,
Action = e.Action.ToString(),
Outcome = e.Outcome.ToString(),
DecidedAt = e.DecidedAt,
CveId = e.Cve,
ComponentPurl = e.Component
}).ToList(),
MatchingFactors = s.MatchingFactors.ToList()
}).ToList(),
AnalyzedRecords = result.AnalyzedRecords,
TopSimilarity = result.TopSimilarity
});
}
/// <summary>
/// Query past decisions.
/// </summary>
private static async Task<Ok<QueryDecisionsResponse>> QueryDecisionsAsync(
string tenantId,
IOpsMemoryStore store,
string? cveId,
string? componentPrefix,
string? action,
string? outcomeStatus,
int? pageSize,
string? cursor,
CancellationToken cancellationToken)
{
DecisionAction? actionFilter = null;
if (!string.IsNullOrEmpty(action) && Enum.TryParse<DecisionAction>(action, ignoreCase: true, out var a))
{
actionFilter = a;
}
OutcomeStatus? statusFilter = null;
if (!string.IsNullOrEmpty(outcomeStatus) && Enum.TryParse<OutcomeStatus>(outcomeStatus, ignoreCase: true, out var s))
{
statusFilter = s;
}
var query = new OpsMemoryQuery
{
TenantId = tenantId,
CveId = cveId,
ComponentPrefix = componentPrefix,
Action = actionFilter,
OutcomeStatus = statusFilter,
PageSize = pageSize ?? 20,
Cursor = cursor
};
var result = await store.QueryAsync(query, cancellationToken);
return TypedResults.Ok(new QueryDecisionsResponse
{
Decisions = result.Items.Select(MapToSummary).ToList(),
TotalCount = result.TotalCount,
NextCursor = result.NextCursor,
HasMore = result.HasMore
});
}
/// <summary>
/// Get statistics for a tenant.
/// </summary>
private static async Task<Ok<OpsMemoryStatsResponse>> GetStatsAsync(
string tenantId,
IOpsMemoryStore store,
CancellationToken cancellationToken)
{
var stats = await store.GetStatsAsync(tenantId, null, cancellationToken);
return TypedResults.Ok(new OpsMemoryStatsResponse
{
TenantId = tenantId,
TotalDecisions = stats.TotalDecisions,
DecisionsWithOutcomes = stats.DecisionsWithOutcomes,
SuccessRate = stats.SuccessRate
});
}
private static ReachabilityStatus ParseReachability(string? value)
{
if (string.IsNullOrEmpty(value))
{
return ReachabilityStatus.Unknown;
}
return Enum.TryParse<ReachabilityStatus>(value, ignoreCase: true, out var result)
? result
: ReachabilityStatus.Unknown;
}
private static DecisionDetailsResponse MapToDetailsResponse(OpsMemoryRecord record)
{
return new DecisionDetailsResponse
{
MemoryId = record.MemoryId,
TenantId = record.TenantId,
RecordedAt = record.RecordedAt,
Situation = new SituationDto
{
CveId = record.Situation.CveId,
Component = record.Situation.Component,
ComponentName = record.Situation.ComponentName,
Severity = record.Situation.Severity,
Reachability = record.Situation.Reachability.ToString(),
EpssScore = record.Situation.EpssScore,
CvssScore = record.Situation.CvssScore,
IsKev = record.Situation.IsKev,
ContextTags = record.Situation.ContextTags.ToList()
},
Decision = new DecisionDto
{
Action = record.Decision.Action.ToString(),
Rationale = record.Decision.Rationale,
DecidedBy = record.Decision.DecidedBy,
DecidedAt = record.Decision.DecidedAt,
PolicyReference = record.Decision.PolicyReference,
VexStatementId = record.Decision.VexStatementId,
Mitigation = record.Decision.Mitigation != null
? new MitigationDto
{
Type = record.Decision.Mitigation.Type,
Description = record.Decision.Mitigation.Description,
Effectiveness = record.Decision.Mitigation.Effectiveness,
ExpiresAt = record.Decision.Mitigation.ExpiresAt
}
: null
},
Outcome = record.Outcome != null
? new OutcomeDto
{
Status = record.Outcome.Status.ToString(),
ResolutionTimeMinutes = record.Outcome.ResolutionTime?.TotalMinutes,
ActualImpact = record.Outcome.ActualImpact,
LessonsLearned = record.Outcome.LessonsLearned,
RecordedBy = record.Outcome.RecordedBy,
RecordedAt = record.Outcome.RecordedAt,
WouldRepeat = record.Outcome.WouldRepeat,
AlternativeActions = record.Outcome.AlternativeActions
}
: null
};
}
private static DecisionSummaryDto MapToSummary(OpsMemoryRecord record)
{
return new DecisionSummaryDto
{
MemoryId = record.MemoryId,
RecordedAt = record.RecordedAt,
CveId = record.Situation.CveId,
Component = record.Situation.Component,
Action = record.Decision.Action.ToString(),
OutcomeStatus = record.Outcome?.Status.ToString(),
DecidedBy = record.Decision.DecidedBy
};
}
}
#region Request/Response DTOs
/// <summary>Request to record a decision.</summary>
public sealed record RecordDecisionRequest
{
public required string TenantId { get; init; }
public string? CveId { get; init; }
public string? ComponentPurl { get; init; }
public string? Severity { get; init; }
public string? Reachability { get; init; }
public double? EpssScore { get; init; }
public double? CvssScore { get; init; }
public List<string>? ContextTags { get; init; }
public required string Action { get; init; }
public string? Rationale { get; init; }
public string? DecidedBy { get; init; }
public string? PolicyReference { get; init; }
public string? MitigationType { get; init; }
public string? MitigationDetails { get; init; }
}
/// <summary>Response after recording a decision.</summary>
public sealed record RecordDecisionResponse
{
public required string MemoryId { get; init; }
public DateTimeOffset RecordedAt { get; init; }
}
/// <summary>Request to record an outcome.</summary>
public sealed record RecordOutcomeRequest
{
public required string Status { get; init; }
public int? ResolutionTimeMinutes { get; init; }
public string? ActualImpact { get; init; }
public string? LessonsLearned { get; init; }
public string? RecordedBy { get; init; }
}
/// <summary>Response after recording an outcome.</summary>
public sealed record OutcomeRecordedResponse
{
public required string MemoryId { get; init; }
public required string Status { get; init; }
public DateTimeOffset RecordedAt { get; init; }
}
/// <summary>Detailed decision information.</summary>
public sealed record DecisionDetailsResponse
{
public required string MemoryId { get; init; }
public required string TenantId { get; init; }
public DateTimeOffset RecordedAt { get; init; }
public required SituationDto Situation { get; init; }
public required DecisionDto Decision { get; init; }
public OutcomeDto? Outcome { get; init; }
}
/// <summary>Situation context DTO.</summary>
public sealed record SituationDto
{
public string? CveId { get; init; }
public string? Component { get; init; }
public string? ComponentName { get; init; }
public string? Severity { get; init; }
public string? Reachability { get; init; }
public double? EpssScore { get; init; }
public double? CvssScore { get; init; }
public bool IsKev { get; init; }
public List<string>? ContextTags { get; init; }
}
/// <summary>Decision DTO.</summary>
public sealed record DecisionDto
{
public required string Action { get; init; }
public required string Rationale { get; init; }
public required string DecidedBy { get; init; }
public DateTimeOffset DecidedAt { get; init; }
public string? PolicyReference { get; init; }
public string? VexStatementId { get; init; }
public MitigationDto? Mitigation { get; init; }
}
/// <summary>Mitigation DTO.</summary>
public sealed record MitigationDto
{
public required string Type { get; init; }
public required string Description { get; init; }
public double? Effectiveness { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>Outcome DTO.</summary>
public sealed record OutcomeDto
{
public required string Status { get; init; }
public double? ResolutionTimeMinutes { get; init; }
public string? ActualImpact { get; init; }
public string? LessonsLearned { get; init; }
public required string RecordedBy { get; init; }
public DateTimeOffset RecordedAt { get; init; }
public bool? WouldRepeat { get; init; }
public string? AlternativeActions { get; init; }
}
/// <summary>Playbook suggestions response.</summary>
public sealed record PlaybookSuggestionsResponse
{
public required List<PlaybookSuggestionDto> Suggestions { get; init; }
public int AnalyzedRecords { get; init; }
public double? TopSimilarity { get; init; }
}
/// <summary>A single playbook suggestion.</summary>
public sealed record PlaybookSuggestionDto
{
public required string SuggestedAction { get; init; }
public double Confidence { get; init; }
public required string Rationale { get; init; }
public double SuccessRate { get; init; }
public int SimilarDecisionCount { get; init; }
public double? AverageResolutionTimeMinutes { get; init; }
public required List<PlaybookEvidenceDto> Evidence { get; init; }
public List<string>? MatchingFactors { get; init; }
}
/// <summary>Evidence for a suggestion.</summary>
public sealed record PlaybookEvidenceDto
{
public required string MemoryId { get; init; }
public double Similarity { get; init; }
public required string Action { get; init; }
public required string Outcome { get; init; }
public DateTimeOffset DecidedAt { get; init; }
public string? CveId { get; init; }
public string? ComponentPurl { get; init; }
}
/// <summary>Query decisions response.</summary>
public sealed record QueryDecisionsResponse
{
public required List<DecisionSummaryDto> Decisions { get; init; }
public int? TotalCount { get; init; }
public string? NextCursor { get; init; }
public bool HasMore { get; init; }
}
/// <summary>Decision summary for lists.</summary>
public sealed record DecisionSummaryDto
{
public required string MemoryId { get; init; }
public DateTimeOffset RecordedAt { get; init; }
public string? CveId { get; init; }
public string? Component { get; init; }
public required string Action { get; init; }
public string? OutcomeStatus { get; init; }
public string? DecidedBy { get; init; }
}
/// <summary>Statistics response.</summary>
public sealed record OpsMemoryStatsResponse
{
public required string TenantId { get; init; }
public int TotalDecisions { get; init; }
public int DecisionsWithOutcomes { get; init; }
public double SuccessRate { get; init; }
}
#endregion

View File

@@ -0,0 +1,110 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using Npgsql;
using StellaOps.Determinism;
using StellaOps.OpsMemory.Playbook;
using StellaOps.OpsMemory.Similarity;
using StellaOps.OpsMemory.Storage;
using StellaOps.Localization;
using StellaOps.Router.AspNet;
using StellaOps.OpsMemory.WebService.Endpoints;
using StellaOps.OpsMemory.WebService.Security;
var builder = WebApplication.CreateBuilder(args);
// Add PostgreSQL data source
var connectionString = ResolveOpsMemoryConnectionString(builder);
builder.Services.AddSingleton<NpgsqlDataSource>(_ => NpgsqlDataSource.Create(connectionString));
// Add determinism abstractions (TimeProvider + IGuidProvider for endpoint parameter binding)
builder.Services.AddDeterminismDefaults();
// Add OpsMemory services
builder.Services.AddSingleton<IOpsMemoryStore, PostgresOpsMemoryStore>();
builder.Services.AddSingleton<SimilarityVectorGenerator>();
builder.Services.AddSingleton<PlaybookSuggestionService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new()
{
Title = "StellaOps OpsMemory API",
Version = "v1",
Description = "Decision ledger and playbook suggestions API for security operations learning"
});
});
builder.Services.AddHealthChecks();
// Authentication and authorization
builder.Services.AddStellaOpsResourceServerAuthentication(builder.Configuration);
builder.Services.AddAuthorization(options =>
{
options.AddStellaOpsScopePolicy(OpsMemoryPolicies.Read, StellaOpsScopes.OpsMemoryRead);
options.AddStellaOpsScopePolicy(OpsMemoryPolicies.Write, StellaOpsScopes.OpsMemoryWrite);
});
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
builder.Services.AddStellaOpsLocalization(builder.Configuration);
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
// Stella Router integration
var routerEnabled = builder.Services.AddRouterMicroservice(
builder.Configuration,
serviceName: "opsmemory",
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
routerOptionsSection: "Router");
builder.TryAddStellaOpsLocalBinding("opsmemory");
var app = builder.Build();
app.LogStellaOpsLocalHostname("opsmemory");
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
app.TryUseStellaRouter(routerEnabled);
// Map endpoints
app.MapOpsMemoryEndpoints();
app.MapHealthChecks("/health");
app.TryRefreshStellaRouterEndpoints(routerEnabled);
await app.LoadTranslationsAsync();
app.Run();
static string ResolveOpsMemoryConnectionString(WebApplicationBuilder builder)
{
// Explicit service connection has priority; shared default is the compose-compatible fallback.
var configuredConnectionString =
builder.Configuration.GetConnectionString("OpsMemory")
?? builder.Configuration["ConnectionStrings:OpsMemory"]
?? builder.Configuration.GetConnectionString("Default")
?? builder.Configuration["ConnectionStrings:Default"];
if (!string.IsNullOrWhiteSpace(configuredConnectionString))
{
return configuredConnectionString.Trim();
}
if (builder.Environment.IsDevelopment())
{
return "Host=localhost;Port=5432;Database=stellaops;Username=stellaops;Password=stellaops";
}
throw new InvalidOperationException(
"OpsMemory database connection string is required in non-development environments. Configure ConnectionStrings:OpsMemory or ConnectionStrings:Default.");
}

View File

@@ -0,0 +1,14 @@
{
"profiles": {
"StellaOps.OpsMemory.WebService": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"STELLAOPS_WEBSERVICES_CORS": "true",
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
},
"applicationUrl": "https://localhost:10270;http://localhost:10271"
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
namespace StellaOps.OpsMemory.WebService.Security;
/// <summary>
/// Named authorization policy constants for the OpsMemory service.
/// Policies are registered via AddStellaOpsScopePolicy in Program.cs.
/// </summary>
internal static class OpsMemoryPolicies
{
/// <summary>Policy for reading decisions and suggestions. Requires ops-memory:read scope.</summary>
public const string Read = "OpsMemory.Read";
/// <summary>Policy for recording decisions and outcomes. Requires ops-memory:write scope.</summary>
public const string Write = "OpsMemory.Write";
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RootNamespace>StellaOps.OpsMemory.WebService</RootNamespace>
<Description>StellaOps OpsMemory Service - Decision ledger and playbook suggestions API</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Translations\*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Npgsql" />
</ItemGroup>
<PropertyGroup Label="StellaOpsReleaseVersion">
<Version>1.0.0-alpha1</Version>
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,9 @@
# StellaOps.OpsMemory.WebService Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| S312-OPSMEMORY-CONNECTION | DONE | Sprint `docs/implplan/SPRINT_20260305_312_DOCS_storage_policy_postgres_rustfs_alignment.md` TASK-312-007: aligned connection resolution with compose defaults (`ConnectionStrings:Default` fallback) and added fail-fast behavior for non-development when DB config is missing. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/OpsMemory/StellaOps.OpsMemory.WebService/StellaOps.OpsMemory.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -0,0 +1,3 @@
{
"_meta": { "locale": "en-US", "namespace": "opsmemory", "version": "1.0" }
}