feat(signals,reachgraph,airgap,zastava): postgres runtime persistence

Cross-module truthful runtime persistence supporting the sprint_20260415
and sprint_20260416 cutovers. These modules have no single dedicated
sprint owner in the current batch, but they unblock downstream wiring
in Policy (reachability facts), ReachGraph (signals adapter), and the
air-gap controller/time services.

- Signals.Persistence: migration 003 runtime_canonical_tables; Postgres
  repos (callgraph + projection, reachability fact/store, deployment refs,
  graph metrics); DB context factory + service collection extensions.
- Signals: swap in-memory callgraph/reachability repositories for Postgres
  wired via SignalsPersistenceExtensions; durable host tests.
- ReachGraph.WebService: SignalsHttpAdapter + program wiring; host wiring +
  adapter tests.
- AirGap.Controller: service-collection extensions + infrastructure wiring;
  endpoint + startup contract tests.
- AirGap.Time: PostgresTimeAnchorStore + startup service; runtime contract
  + persistence tests.
- AirGap.Persistence: persistence extensions.
- Zastava: csproj cleanup (Observer + Core).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-19 14:44:57 +03:00
parent 87a5d2ee22
commit ad62ba7f76
50 changed files with 2178 additions and 114 deletions

View File

@@ -85,7 +85,13 @@ builder.Services.AddScoped<IReachGraphReplayService, ReachGraphReplayService>();
// Reachability Core adapters and unified query interface
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
builder.Services.AddScoped<StellaOps.Reachability.Core.IReachGraphAdapter, ReachGraphStoreAdapter>();
builder.Services.AddSingleton<StellaOps.Reachability.Core.ISignalsAdapter, InMemorySignalsAdapter>();
builder.Services.AddHttpClient<StellaOps.Reachability.Core.ISignalsAdapter, SignalsHttpAdapter>((sp, client) =>
{
var baseUri = ResolveSignalsBaseUri(builder.Configuration);
client.BaseAddress = baseUri;
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.ConnectionClose = false;
});
builder.Services.AddScoped<StellaOps.Reachability.Core.IReachabilityIndex, StellaOps.Reachability.Core.ReachabilityIndex>();
// Rate limiting
@@ -153,6 +159,20 @@ app.TryRefreshStellaRouterEndpoints(routerEnabled);
await app.LoadTranslationsAsync();
app.Run();
static Uri ResolveSignalsBaseUri(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
var raw = configuration["Signals:BaseUrl"]
?? configuration["STELLAOPS_SIGNALS_URL"]
?? Environment.GetEnvironmentVariable("STELLAOPS_SIGNALS_URL")
?? "http://signals.stella-ops.local";
return Uri.TryCreate(raw, UriKind.Absolute, out var uri)
? uri
: new Uri("http://signals.stella-ops.local", UriKind.Absolute);
}
// Make Program class accessible for integration testing
namespace StellaOps.ReachGraph.WebService
{

View File

@@ -0,0 +1,384 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using System.Collections.Immutable;
using System.Net;
using System.Text.Json;
using StellaOps.Reachability.Core;
namespace StellaOps.ReachGraph.WebService.Services;
/// <summary>
/// Signals-backed implementation of <see cref="ISignalsAdapter"/> for runtime observation facts.
/// </summary>
public sealed class SignalsHttpAdapter : ISignalsAdapter
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly HttpClient _httpClient;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SignalsHttpAdapter> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SignalsHttpAdapter"/> class.
/// </summary>
public SignalsHttpAdapter(
HttpClient httpClient,
TimeProvider timeProvider,
ILogger<SignalsHttpAdapter> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc/>
public async Task<RuntimeReachabilityResult> QueryAsync(
SymbolRef symbol,
string artifactDigest,
TimeSpan observationWindow,
string? tenantId,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(symbol);
ArgumentException.ThrowIfNullOrWhiteSpace(artifactDigest);
var fact = await GetFactAsync(artifactDigest, tenantId, ct).ConfigureAwait(false);
var now = _timeProvider.GetUtcNow();
var windowStart = now - observationWindow;
if (fact is null)
{
return CreateNotFoundResult(symbol, artifactDigest, observationWindow, windowStart, now);
}
var matchingFacts = FindMatchingRuntimeFacts(fact, symbol).ToList();
var matchingState = FindMatchingState(fact, symbol);
if (matchingFacts.Count == 0 && matchingState is null)
{
return CreateNotFoundResult(symbol, artifactDigest, observationWindow, windowStart, now);
}
var observedAt = matchingFacts
.Select(f => f.ObservedAt)
.Where(t => t.HasValue)
.Select(t => t!.Value)
.ToList();
var contexts = matchingFacts
.Select(MapContext)
.Distinct()
.Take(10)
.ToImmutableArray();
var evidenceUris = matchingFacts
.Where(f => !string.IsNullOrWhiteSpace(f.EvidenceUri))
.Select(f => f.EvidenceUri!.Trim())
.Distinct(StringComparer.Ordinal)
.ToImmutableArray();
var hitCount = matchingFacts.Sum(f => Math.Max(0, f.HitCount));
if (hitCount == 0 && matchingState?.Evidence?.RuntimeHits is { Count: > 0 })
{
hitCount = matchingState.Evidence.RuntimeHits.Count;
}
return new RuntimeReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
WasObserved = hitCount > 0 || matchingState?.Reachable == true,
ObservationWindow = observationWindow,
WindowStart = windowStart,
WindowEnd = now,
HitCount = hitCount,
FirstSeen = observedAt.Count > 0 ? observedAt.Min() : matchingState?.LatticeTransitionAt,
LastSeen = observedAt.Count > 0 ? observedAt.Max() : matchingState?.LatticeTransitionAt,
Contexts = contexts,
EvidenceUris = evidenceUris,
AgentVersion = ExtractAgentVersion(fact)
};
}
/// <inheritdoc/>
public async Task<bool> HasFactsAsync(string artifactDigest, string? tenantId, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(artifactDigest);
return await GetFactAsync(artifactDigest, tenantId, ct).ConfigureAwait(false) is not null;
}
/// <inheritdoc/>
public async Task<SignalsMetadata?> GetMetadataAsync(string artifactDigest, string? tenantId, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(artifactDigest);
var fact = await GetFactAsync(artifactDigest, tenantId, ct).ConfigureAwait(false);
if (fact is null)
{
return null;
}
var runtimeFacts = fact.RuntimeFacts ?? [];
if (runtimeFacts.Count == 0 && fact.States.Count == 0)
{
return null;
}
var observationTimes = runtimeFacts
.Select(f => f.ObservedAt)
.Where(t => t.HasValue)
.Select(t => t!.Value)
.ToList();
if (observationTimes.Count == 0)
{
observationTimes.Add(fact.ComputedAt);
}
var environments = runtimeFacts
.SelectMany(ExtractEnvironments)
.Distinct(StringComparer.Ordinal)
.ToArray();
return new SignalsMetadata
{
ArtifactDigest = artifactDigest,
TenantId = tenantId,
EarliestObservation = observationTimes.Min(),
LatestObservation = observationTimes.Max(),
SymbolCount = runtimeFacts.Count > 0
? runtimeFacts.Select(f => f.SymbolId).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).Count()
: fact.States.Count,
TotalObservations = runtimeFacts.Sum(f => Math.Max(0, f.HitCount)),
Environments = environments,
AgentVersion = ExtractAgentVersion(fact)
};
}
private async Task<SignalsFactDocumentDto?> GetFactAsync(
string artifactDigest,
string? tenantId,
CancellationToken ct)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/signals/facts/{Uri.EscapeDataString(artifactDigest)}");
if (!string.IsNullOrWhiteSpace(tenantId))
{
request.Headers.TryAddWithoutValidation("X-Tenant-ID", tenantId);
}
using var response = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(payload))
{
return null;
}
try
{
return JsonSerializer.Deserialize<SignalsFactDocumentDto>(payload, JsonOptions);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Failed to deserialize Signals reachability fact for {ArtifactDigest}.", artifactDigest);
throw;
}
}
private static IEnumerable<RuntimeFactDocumentDto> FindMatchingRuntimeFacts(SignalsFactDocumentDto fact, SymbolRef symbol)
{
return fact.RuntimeFacts?.Where(runtimeFact => MatchesSymbol(runtimeFact.SymbolId, symbol)) ?? [];
}
private static ReachabilityStateDocumentDto? FindMatchingState(SignalsFactDocumentDto fact, SymbolRef symbol)
{
return fact.States?.FirstOrDefault(state => MatchesSymbol(state.Target, symbol));
}
private static bool MatchesSymbol(string? candidate, SymbolRef symbol)
{
if (string.IsNullOrWhiteSpace(candidate))
{
return false;
}
var symbolNames = new[]
{
symbol.CanonicalId,
symbol.DisplayName,
BuildSymbolFqn(symbol),
symbol.Method
};
return symbolNames.Any(name =>
!string.IsNullOrWhiteSpace(name) &&
string.Equals(candidate.Trim(), name, StringComparison.OrdinalIgnoreCase));
}
private static string BuildSymbolFqn(SymbolRef symbol)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(symbol.Namespace))
{
parts.Add(symbol.Namespace);
}
if (!string.IsNullOrWhiteSpace(symbol.Type) && !string.Equals(symbol.Type, "_", StringComparison.Ordinal))
{
parts.Add(symbol.Type);
}
if (!string.IsNullOrWhiteSpace(symbol.Method))
{
parts.Add(symbol.Method);
}
return string.Join('.', parts);
}
private static global::StellaOps.Reachability.Core.ExecutionContext MapContext(RuntimeFactDocumentDto fact)
{
return new global::StellaOps.Reachability.Core.ExecutionContext
{
ContainerId = fact.ContainerId ?? fact.ProcessName,
ProcessId = fact.ProcessId,
Route = fact.SocketAddress,
Environment = TryGetEnvironment(fact),
Frequency = 1.0
};
}
private static IEnumerable<string> ExtractEnvironments(RuntimeFactDocumentDto fact)
{
var environment = TryGetEnvironment(fact);
return string.IsNullOrWhiteSpace(environment)
? []
: [environment!];
}
private static string? TryGetEnvironment(RuntimeFactDocumentDto fact)
{
if (fact.Metadata is null)
{
return null;
}
if (fact.Metadata.TryGetValue("environment", out var environment) && !string.IsNullOrWhiteSpace(environment))
{
return environment.Trim();
}
if (fact.Metadata.TryGetValue("env", out var env) && !string.IsNullOrWhiteSpace(env))
{
return env.Trim();
}
return null;
}
private static string? ExtractAgentVersion(SignalsFactDocumentDto fact)
{
if (fact.Metadata is null)
{
return null;
}
return fact.Metadata.TryGetValue("agent.version", out var agentVersion) && !string.IsNullOrWhiteSpace(agentVersion)
? agentVersion.Trim()
: null;
}
private static RuntimeReachabilityResult CreateNotFoundResult(
SymbolRef symbol,
string artifactDigest,
TimeSpan observationWindow,
DateTimeOffset windowStart,
DateTimeOffset windowEnd)
{
return new RuntimeReachabilityResult
{
Symbol = symbol,
ArtifactDigest = artifactDigest,
WasObserved = false,
ObservationWindow = observationWindow,
WindowStart = windowStart,
WindowEnd = windowEnd,
HitCount = 0,
FirstSeen = null,
LastSeen = null,
Contexts = ImmutableArray<global::StellaOps.Reachability.Core.ExecutionContext>.Empty,
EvidenceUris = ImmutableArray<string>.Empty
};
}
private sealed class SignalsFactDocumentDto
{
public string SubjectKey { get; init; } = string.Empty;
public string? CallgraphId { get; init; }
public Dictionary<string, string?>? Metadata { get; init; }
public DateTimeOffset ComputedAt { get; init; }
public List<ReachabilityStateDocumentDto> States { get; init; } = [];
public List<RuntimeFactDocumentDto>? RuntimeFacts { get; init; }
}
private sealed class ReachabilityStateDocumentDto
{
public string Target { get; init; } = string.Empty;
public bool Reachable { get; init; }
public DateTimeOffset? LatticeTransitionAt { get; init; }
public ReachabilityEvidenceDocumentDto? Evidence { get; init; }
}
private sealed class ReachabilityEvidenceDocumentDto
{
public List<string> RuntimeHits { get; init; } = [];
}
private sealed class RuntimeFactDocumentDto
{
public string SymbolId { get; init; } = string.Empty;
public string? CodeId { get; init; }
public string? SymbolDigest { get; init; }
public string? Purl { get; init; }
public string? BuildId { get; init; }
public string? LoaderBase { get; init; }
public int? ProcessId { get; init; }
public string? ProcessName { get; init; }
public string? SocketAddress { get; init; }
public string? ContainerId { get; init; }
public string? EvidenceUri { get; init; }
public int HitCount { get; init; }
public DateTimeOffset? ObservedAt { get; init; }
public Dictionary<string, string?>? Metadata { get; init; }
}
}

View File

@@ -1,6 +1,6 @@
# StellaOps.ReachGraph.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`.
Source of truth: `docs/implplan/SPRINT_20260415_004_DOCS_runtime_data_plane_real_backend_cutover.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
@@ -8,3 +8,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| SPRINT_20260405_011-XPORT-VALKEY | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: named the ReachGraph Valkey client construction path. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReachGraph/StellaOps.ReachGraph.WebService/StellaOps.ReachGraph.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| RUNTIME-002 | DONE | `ISignalsAdapter` is now resolved from the durable `SignalsHttpAdapter` client path in live host mode; the full ReachGraph test project passed and in-memory adapter remains test-only. |

View File

@@ -0,0 +1,22 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using Microsoft.Extensions.DependencyInjection;
using StellaOps.ReachGraph.WebService.Services;
using StellaOps.Reachability.Core;
using Xunit;
namespace StellaOps.ReachGraph.WebService.Tests;
public sealed class ReachGraphHostWiringTests
{
[Fact]
public void Host_ResolvesSignalsHttpAdapter_InLiveMode()
{
using var factory = new ReachGraphTestFactory();
using var scope = factory.Services.CreateScope();
var adapter = scope.ServiceProvider.GetRequiredService<ISignalsAdapter>();
Assert.IsType<SignalsHttpAdapter>(adapter);
}
}

View File

@@ -0,0 +1,239 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using System.Net;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.ReachGraph.WebService.Services;
using StellaOps.Reachability.Core;
using Xunit;
namespace StellaOps.ReachGraph.WebService.Tests;
public sealed class SignalsHttpAdapterTests
{
[Fact]
public async Task QueryAsync_ReturnsObservedResult_FromSignalsPayload()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 4, 15, 12, 0, 0, TimeSpan.Zero));
var symbol = new SymbolRef
{
Purl = "pkg:nuget/test@1.0.0",
Namespace = "MyApp",
Type = "Service",
Method = "Process"
};
var symbolId = symbol.CanonicalId;
var payload = new
{
subjectKey = "sha256:test",
computedAt = timeProvider.GetUtcNow(),
metadata = new Dictionary<string, string?>
{
["agent.version"] = "signals-agent/1.2.3"
},
states = new[]
{
new
{
target = symbolId,
reachable = true,
latticeTransitionAt = timeProvider.GetUtcNow().AddMinutes(-5),
evidence = new
{
runtimeHits = new[] { symbolId }
}
}
},
runtimeFacts = new[]
{
new
{
symbolId,
hitCount = 7,
observedAt = timeProvider.GetUtcNow().AddMinutes(-2),
containerId = "container-a",
processId = 42,
socketAddress = "10.0.0.1:8080",
evidenceUri = "stella://signals/runtime/default/sha256:test?symbol=" + Uri.EscapeDataString(symbolId),
metadata = new Dictionary<string, string?>
{
["environment"] = "production"
}
},
new
{
symbolId,
hitCount = 5,
observedAt = timeProvider.GetUtcNow().AddMinutes(-1),
containerId = "container-b",
processId = 43,
socketAddress = "10.0.0.1:8081",
evidenceUri = "stella://signals/runtime/default/sha256:test?symbol=" + Uri.EscapeDataString(symbolId) + "&seq=2",
metadata = new Dictionary<string, string?>
{
["environment"] = "staging"
}
}
}
};
var adapter = CreateAdapter(
responseStatusCode: HttpStatusCode.OK,
responseJson: JsonSerializer.Serialize(payload),
timeProvider,
out var handler);
var result = await adapter.QueryAsync(symbol, "sha256:test", TimeSpan.FromDays(7), "tenant-a", CancellationToken.None);
result.WasObserved.Should().BeTrue();
result.HitCount.Should().Be(12);
result.FirstSeen.Should().Be(timeProvider.GetUtcNow().AddMinutes(-2));
result.LastSeen.Should().Be(timeProvider.GetUtcNow().AddMinutes(-1));
result.Contexts.Should().ContainSingle(c => c.ContainerId == "container-a");
result.Contexts.Should().ContainSingle(c => c.ContainerId == "container-b");
result.EvidenceUris.Should().Contain(uri => uri.Contains("symbol=", StringComparison.Ordinal));
result.AgentVersion.Should().Be("signals-agent/1.2.3");
handler.LastRequest.Should().NotBeNull();
handler.LastRequest!.RequestUri.Should().NotBeNull();
handler.LastRequest.RequestUri!.AbsolutePath.Should().Be($"/signals/facts/{Uri.EscapeDataString("sha256:test")}");
handler.LastRequest.Headers.TryGetValues("X-Tenant-ID", out var tenantValues).Should().BeTrue();
tenantValues.Should().ContainSingle(value => value == "tenant-a");
}
[Fact]
public async Task GetMetadataAsync_ReturnsAggregateMetadata_FromSignalsPayload()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 4, 15, 12, 0, 0, TimeSpan.Zero));
var symbol = new SymbolRef
{
Purl = "pkg:nuget/test@1.0.0",
Namespace = "MyApp",
Type = "Service",
Method = "Process"
};
var symbolId = symbol.CanonicalId;
var payload = new
{
subjectKey = "sha256:test",
computedAt = timeProvider.GetUtcNow(),
metadata = new Dictionary<string, string?>(),
states = new[]
{
new
{
target = symbolId,
reachable = true,
latticeTransitionAt = timeProvider.GetUtcNow().AddMinutes(-5),
evidence = new { runtimeHits = new[] { symbolId } }
}
},
runtimeFacts = new[]
{
new
{
symbolId,
hitCount = 7,
observedAt = timeProvider.GetUtcNow().AddMinutes(-2),
containerId = "container-a",
processId = 42,
socketAddress = "10.0.0.1:8080",
evidenceUri = "stella://signals/runtime/default/sha256:test?symbol=" + Uri.EscapeDataString(symbolId),
metadata = new Dictionary<string, string?>
{
["environment"] = "production"
}
},
new
{
symbolId,
hitCount = 5,
observedAt = timeProvider.GetUtcNow().AddMinutes(-1),
containerId = "container-b",
processId = 43,
socketAddress = "10.0.0.1:8081",
evidenceUri = "stella://signals/runtime/default/sha256:test?symbol=" + Uri.EscapeDataString(symbolId) + "&seq=2",
metadata = new Dictionary<string, string?>
{
["environment"] = "staging"
}
}
}
};
var adapter = CreateAdapter(
responseStatusCode: HttpStatusCode.OK,
responseJson: JsonSerializer.Serialize(payload),
timeProvider,
out _);
var metadata = await adapter.GetMetadataAsync("sha256:test", "tenant-a", CancellationToken.None);
metadata.Should().NotBeNull();
metadata!.ArtifactDigest.Should().Be("sha256:test");
metadata.SymbolCount.Should().Be(1);
metadata.TotalObservations.Should().Be(12);
metadata.EarliestObservation.Should().Be(timeProvider.GetUtcNow().AddMinutes(-2));
metadata.LatestObservation.Should().Be(timeProvider.GetUtcNow().AddMinutes(-1));
metadata.Environments.Should().BeEquivalentTo(["production", "staging"]);
}
[Fact]
public async Task HasFactsAsync_ReturnsFalse_WhenSignalsReturnsNotFound()
{
var adapter = CreateAdapter(
responseStatusCode: HttpStatusCode.NotFound,
responseJson: string.Empty,
new FakeTimeProvider(DateTimeOffset.UtcNow),
out _);
var result = await adapter.HasFactsAsync("sha256:missing", "tenant-a", CancellationToken.None);
result.Should().BeFalse();
}
private static SignalsHttpAdapter CreateAdapter(
HttpStatusCode responseStatusCode,
string responseJson,
FakeTimeProvider timeProvider,
out RecordingHandler handler)
{
handler = new RecordingHandler(responseStatusCode, responseJson);
var client = new HttpClient(handler)
{
BaseAddress = new Uri("http://signals.test", UriKind.Absolute)
};
return new SignalsHttpAdapter(client, timeProvider, NullLogger<SignalsHttpAdapter>.Instance);
}
private sealed class RecordingHandler : HttpMessageHandler
{
private readonly HttpStatusCode _statusCode;
private readonly string _responseJson;
public RecordingHandler(HttpStatusCode statusCode, string responseJson)
{
_statusCode = statusCode;
_responseJson = responseJson;
}
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
var response = new HttpResponseMessage(_statusCode)
{
Content = new StringContent(_responseJson, Encoding.UTF8, "application/json")
};
return Task.FromResult(response);
}
}
}

View File

@@ -1,8 +1,9 @@
# StellaOps.ReachGraph.WebService.Tests 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`.
Source of truth: `docs/implplan/SPRINT_20260415_004_DOCS_runtime_data_plane_real_backend_cutover.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/ReachGraph/__Tests/StellaOps.ReachGraph.WebService.Tests/StellaOps.ReachGraph.WebService.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| RUNTIME-002-T | DONE | Targeted proof for the Signals HTTP adapter and live host service resolution now passes in the full `StellaOps.ReachGraph.WebService.Tests` project (`30/30`). |