save checkpoint

This commit is contained in:
master
2026-02-11 01:32:14 +02:00
parent 5593212b41
commit cf5b72974f
2316 changed files with 68799 additions and 3808 deletions

View File

@@ -2,9 +2,9 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using StellaOps.SbomService.Lineage.Domain;
using StellaOps.SbomService.Lineage.Services;
namespace StellaOps.SbomService.Controllers;
@@ -59,7 +59,7 @@ public sealed class LineageStreamController : ControllerBase
var digestList = string.IsNullOrWhiteSpace(watchDigests)
? null
: watchDigests.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
: (IReadOnlyList<string>)watchDigests.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
try
{
@@ -67,7 +67,6 @@ public sealed class LineageStreamController : ControllerBase
{
var eventData = System.Text.Json.JsonSerializer.Serialize(new
{
id = evt.EventId,
type = evt.EventType.ToString(),
digest = evt.AffectedDigest,
parentDigest = evt.ParentDigest,
@@ -139,52 +138,40 @@ public sealed class LineageStreamController : ControllerBase
if (fullResult.Graph.Nodes.Count == 0)
return NotFound(new { error = "LINEAGE_NOT_FOUND", artifactDigest });
// Convert to optimizer format
var allNodes = fullResult.Graph.Nodes
.Select(n => new LineageNode(n.Digest, n.Name, n.Version, n.ComponentCount))
.ToImmutableArray();
var allEdges = fullResult.Graph.Edges
.Select(e => new LineageEdge(e.FromDigest, e.ToDigest))
.ToImmutableArray();
var request = new LineageOptimizationRequest
{
TenantId = tenantId,
CenterDigest = artifactDigest,
AllNodes = allNodes,
AllEdges = allEdges,
MaxDepth = maxDepth,
PageSize = pageSize,
PageNumber = pageNumber,
Offset = pageNumber * pageSize,
Limit = pageSize,
SearchTerm = searchTerm
};
var optimized = _optimizer.Optimize(request);
var optimized = _optimizer.Optimize(fullResult.Graph, request);
return Ok(new OptimizedLineageGraphDto
{
CenterDigest = artifactDigest,
Nodes = optimized.Nodes.Select(n => new LineageNodeDto
{
Digest = n.Digest,
Name = n.Name,
Version = n.Version,
ComponentCount = n.ComponentCount
Digest = n.ArtifactDigest,
Name = n.Metadata?.ImageReference ?? n.ArtifactDigest,
Version = n.Metadata?.Tag ?? string.Empty,
ComponentCount = 0
}).ToList(),
Edges = optimized.Edges.Select(e => new LineageEdgeDto
{
FromDigest = e.FromDigest,
ToDigest = e.ToDigest
FromDigest = e.ParentDigest,
ToDigest = e.ChildDigest
}).ToList(),
BoundaryNodes = optimized.BoundaryNodes.Select(b => new BoundaryNodeDto
{
Digest = b.Digest,
HiddenChildrenCount = b.HiddenChildrenCount,
HiddenParentsCount = b.HiddenParentsCount
HiddenChildrenCount = b.HiddenChildCount,
HiddenParentsCount = b.HiddenParentCount
}).ToList(),
TotalNodes = optimized.TotalNodes,
HasMorePages = optimized.HasMorePages,
TotalNodes = optimized.TotalNodeCount,
HasMorePages = optimized.HasMore,
PageNumber = pageNumber,
PageSize = pageSize
});
@@ -219,7 +206,7 @@ public sealed class LineageStreamController : ControllerBase
return;
}
if (!Enum.TryParse<TraversalDirection>(direction, ignoreCase: true, out var traversalDir))
if (!Enum.TryParse<TraversalDirection>(direction, ignoreCase: true, out _))
{
Response.StatusCode = 400;
await Response.WriteAsync("Invalid direction. Use: Children, Parents, or Center");
@@ -243,28 +230,41 @@ public sealed class LineageStreamController : ControllerBase
return;
}
var allNodes = fullResult.Graph.Nodes
.Select(n => new LineageNode(n.Digest, n.Name, n.Version, n.ComponentCount))
.ToImmutableArray();
// Build lookup maps from the full graph for the callback functions
var nodesByDigest = fullResult.Graph.Nodes.ToDictionary(n => n.ArtifactDigest, StringComparer.Ordinal);
var childrenByDigest = fullResult.Graph.Edges
.GroupBy(e => e.ParentDigest, StringComparer.Ordinal)
.ToDictionary(
g => g.Key,
g => (IReadOnlyList<LineageNode>)g
.Select(e => nodesByDigest.GetValueOrDefault(e.ChildDigest))
.Where(n => n is not null)
.ToList()!,
StringComparer.Ordinal);
var parentsByDigest = fullResult.Graph.Edges
.GroupBy(e => e.ChildDigest, StringComparer.Ordinal)
.ToDictionary(
g => g.Key,
g => (IReadOnlyList<LineageNode>)g
.Select(e => nodesByDigest.GetValueOrDefault(e.ParentDigest))
.Where(n => n is not null)
.ToList()!,
StringComparer.Ordinal);
var allEdges = fullResult.Graph.Edges
.Select(e => new LineageEdge(e.FromDigest, e.ToDigest))
.ToImmutableArray();
Task<IReadOnlyList<LineageNode>> GetChildren(string digest, CancellationToken token) =>
Task.FromResult(childrenByDigest.GetValueOrDefault(digest, (IReadOnlyList<LineageNode>)Array.Empty<LineageNode>()));
Task<IReadOnlyList<LineageNode>> GetParents(string digest, CancellationToken token) =>
Task.FromResult(parentsByDigest.GetValueOrDefault(digest, (IReadOnlyList<LineageNode>)Array.Empty<LineageNode>()));
await foreach (var level in _optimizer.TraverseLevelsAsync(
artifactDigest, allNodes, allEdges, traversalDir, maxDepth, ct))
artifactDigest, GetChildren, GetParents, maxDepth, ct))
{
var levelData = System.Text.Json.JsonSerializer.Serialize(new
{
depth = level.Depth,
nodes = level.Nodes.Select(n => new
{
digest = n.Digest,
name = n.Name,
version = n.Version,
componentCount = n.ComponentCount
}),
isComplete = level.IsComplete
depth = level.Level,
direction = level.Direction.ToString(),
nodeDigests = level.NodeDigests
});
await Response.WriteAsync($"event: level\n", ct);
@@ -315,16 +315,22 @@ public sealed class LineageStreamController : ControllerBase
if (fullResult.Graph.Nodes.Count == 0)
return NotFound(new { error = "LINEAGE_NOT_FOUND", artifactDigest });
var allNodes = fullResult.Graph.Nodes
.Select(n => new LineageNode(n.Digest, n.Name, n.Version, n.ComponentCount))
.ToImmutableArray();
var allEdges = fullResult.Graph.Edges
.Select(e => new LineageEdge(e.FromDigest, e.ToDigest))
.ToImmutableArray();
var metadata = await _optimizer.GetOrComputeMetadataAsync(
tenantId, artifactDigest, allNodes, allEdges, ct);
artifactDigest,
tenantId,
async innerCt =>
{
// Compute metadata from the full graph
return new LineageGraphMetadata
{
ArtifactDigest = artifactDigest,
TotalNodes = fullResult.Graph.Nodes.Count,
TotalEdges = fullResult.Graph.Edges.Count,
MaxDepth = ComputeMaxDepth(fullResult.Graph, artifactDigest),
LastUpdated = DateTimeOffset.UtcNow
};
},
ct);
return Ok(new LineageGraphMetadataDto
{
@@ -332,7 +338,7 @@ public sealed class LineageStreamController : ControllerBase
TotalNodes = metadata.TotalNodes,
TotalEdges = metadata.TotalEdges,
MaxDepth = metadata.MaxDepth,
ComputedAt = metadata.ComputedAt
ComputedAt = metadata.LastUpdated
});
}
catch (Exception ex)
@@ -362,7 +368,7 @@ public sealed class LineageStreamController : ControllerBase
if (tenantId == Guid.Empty)
return Unauthorized();
await _optimizer.InvalidateCacheAsync(tenantId, artifactDigest, ct);
await _optimizer.InvalidateCacheAsync(artifactDigest, tenantId, ct);
return NoContent();
}
@@ -371,6 +377,35 @@ public sealed class LineageStreamController : ControllerBase
// TODO: Extract from claims or headers
return Guid.Parse("00000000-0000-0000-0000-000000000001");
}
private static int ComputeMaxDepth(LineageGraph graph, string centerDigest)
{
if (graph.Nodes.Count == 0)
return 0;
var childrenMap = graph.Edges
.GroupBy(e => e.ParentDigest)
.ToDictionary(g => g.Key, g => g.Select(e => e.ChildDigest).ToList());
var visited = new HashSet<string>(StringComparer.Ordinal);
var maxDepth = 0;
void Dfs(string digest, int depth)
{
if (!visited.Add(digest))
return;
if (depth > maxDepth)
maxDepth = depth;
if (childrenMap.TryGetValue(digest, out var children))
{
foreach (var child in children)
Dfs(child, depth + 1);
}
}
Dfs(centerDigest, 0);
return maxDepth;
}
}
// DTOs for API responses

View File

@@ -2,7 +2,7 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using StellaOps.SbomService.Lineage.Domain;
namespace StellaOps.SbomService.Lineage.Services;
@@ -14,43 +14,34 @@ public interface ILineageGraphOptimizer
/// <summary>
/// Optimize a lineage graph by applying depth pruning, search filtering, and pagination.
/// </summary>
/// <param name="request">The optimization request parameters.</param>
/// <returns>Optimized graph with boundary information.</returns>
OptimizedLineageGraph Optimize(LineageOptimizationRequest request);
OptimizedLineageGraph Optimize(
LineageGraph fullGraph,
LineageOptimizationRequest request);
/// <summary>
/// Traverse the graph level by level for progressive rendering.
/// </summary>
/// <param name="centerDigest">Starting node digest.</param>
/// <param name="nodes">All nodes in the graph.</param>
/// <param name="edges">All edges in the graph.</param>
/// <param name="direction">Direction to traverse (Children, Parents, or Center).</param>
/// <param name="maxDepth">Maximum depth to traverse.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Async enumerable of levels.</returns>
IAsyncEnumerable<LineageLevel> TraverseLevelsAsync(
string centerDigest,
ImmutableArray<LineageNode> nodes,
ImmutableArray<LineageEdge> edges,
TraversalDirection direction,
int maxDepth = 10,
Func<string, CancellationToken, Task<IReadOnlyList<LineageNode>>> getChildrenAsync,
Func<string, CancellationToken, Task<IReadOnlyList<LineageNode>>> getParentsAsync,
int maxDepth = 5,
CancellationToken ct = default);
/// <summary>
/// Get or compute cached metadata about a lineage graph.
/// </summary>
Task<LineageGraphMetadata> GetOrComputeMetadataAsync(
string artifactDigest,
Guid tenantId,
string centerDigest,
ImmutableArray<LineageNode> nodes,
ImmutableArray<LineageEdge> edges,
Func<CancellationToken, Task<LineageGraphMetadata>> computeAsync,
CancellationToken ct = default);
/// <summary>
/// Invalidate cached metadata for an artifact.
/// </summary>
Task InvalidateCacheAsync(
string artifactDigest,
Guid tenantId,
string centerDigest,
CancellationToken ct = default);
}

View File

@@ -12,10 +12,6 @@ public interface ILineageStreamService : IDisposable
/// <summary>
/// Subscribe to lineage updates for a tenant and optionally specific artifacts.
/// </summary>
/// <param name="tenantId">The tenant ID.</param>
/// <param name="watchDigests">Optional list of artifact digests to watch.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Async enumerable of lineage update events.</returns>
IAsyncEnumerable<LineageUpdateEvent> SubscribeAsync(
Guid tenantId,
IReadOnlyList<string>? watchDigests = null,
@@ -24,12 +20,12 @@ public interface ILineageStreamService : IDisposable
/// <summary>
/// Publish an update event to all relevant subscribers.
/// </summary>
Task PublishAsync(Guid tenantId, LineageUpdateEvent evt, CancellationToken ct = default);
ValueTask PublishAsync(LineageUpdateEvent updateEvent, CancellationToken ct = default);
/// <summary>
/// Notify subscribers about a new SBOM version.
/// </summary>
Task NotifySbomAddedAsync(
ValueTask NotifySbomAddedAsync(
Guid tenantId,
string artifactDigest,
string? parentDigest,
@@ -39,7 +35,7 @@ public interface ILineageStreamService : IDisposable
/// <summary>
/// Notify subscribers about a VEX status change.
/// </summary>
Task NotifyVexChangedAsync(
ValueTask NotifyVexChangedAsync(
Guid tenantId,
string artifactDigest,
VexChangeData change,
@@ -48,7 +44,7 @@ public interface ILineageStreamService : IDisposable
/// <summary>
/// Notify subscribers about reachability updates.
/// </summary>
Task NotifyReachabilityUpdatedAsync(
ValueTask NotifyReachabilityUpdatedAsync(
Guid tenantId,
string artifactDigest,
ReachabilityUpdateData update,
@@ -57,7 +53,7 @@ public interface ILineageStreamService : IDisposable
/// <summary>
/// Notify subscribers about lineage edge changes.
/// </summary>
Task NotifyEdgeChangedAsync(
ValueTask NotifyEdgeChangedAsync(
Guid tenantId,
string fromDigest,
string toDigest,

View File

@@ -65,7 +65,7 @@ public sealed class LineageGraphOptimizer : ILineageGraphOptimizer
// 4. Get edges only for visible nodes
var visibleDigests = paginatedNodes.Select(n => n.ArtifactDigest).ToHashSet(StringComparer.Ordinal);
var visibleEdges = fullGraph.Edges
.Where(e => visibleDigests.Contains(e.FromDigest) || visibleDigests.Contains(e.ToDigest))
.Where(e => visibleDigests.Contains(e.ParentDigest) || visibleDigests.Contains(e.ChildDigest))
.ToList();
// 5. Compute boundary nodes (nodes with edges outside visible set)
@@ -102,7 +102,6 @@ public sealed class LineageGraphOptimizer : ILineageGraphOptimizer
{
var visited = new HashSet<string>(StringComparer.Ordinal);
var currentLevel = new List<string> { centerDigest };
var level = 0;
// Yield the center node first
yield return new LineageLevel
@@ -260,19 +259,19 @@ public sealed class LineageGraphOptimizer : ILineageGraphOptimizer
var adjacency = new Dictionary<string, List<string>>(StringComparer.Ordinal);
foreach (var edge in graph.Edges)
{
if (!adjacency.TryGetValue(edge.FromDigest, out var fromList))
if (!adjacency.TryGetValue(edge.ParentDigest, out var fromList))
{
fromList = new List<string>();
adjacency[edge.FromDigest] = fromList;
adjacency[edge.ParentDigest] = fromList;
}
fromList.Add(edge.ToDigest);
fromList.Add(edge.ChildDigest);
if (!adjacency.TryGetValue(edge.ToDigest, out var toList))
if (!adjacency.TryGetValue(edge.ChildDigest, out var toList))
{
toList = new List<string>();
adjacency[edge.ToDigest] = toList;
adjacency[edge.ChildDigest] = toList;
}
toList.Add(edge.FromDigest);
toList.Add(edge.ParentDigest);
}
while (queue.Count > 0)
@@ -303,9 +302,10 @@ public sealed class LineageGraphOptimizer : ILineageGraphOptimizer
var term = searchTerm.ToLowerInvariant();
return nodes
.Where(n =>
n.Name?.Contains(term, StringComparison.OrdinalIgnoreCase) == true ||
n.ArtifactDigest.Contains(term, StringComparison.OrdinalIgnoreCase) ||
n.Version?.Contains(term, StringComparison.OrdinalIgnoreCase) == true)
n.Metadata?.ImageReference?.Contains(term, StringComparison.OrdinalIgnoreCase) == true ||
n.Metadata?.Repository?.Contains(term, StringComparison.OrdinalIgnoreCase) == true ||
n.Metadata?.Tag?.Contains(term, StringComparison.OrdinalIgnoreCase) == true)
.ToList();
}
@@ -319,11 +319,11 @@ public sealed class LineageGraphOptimizer : ILineageGraphOptimizer
foreach (var node in visibleNodes)
{
var hiddenChildren = allEdges
.Where(e => e.FromDigest == node.ArtifactDigest && !visibleDigests.Contains(e.ToDigest))
.Where(e => e.ParentDigest == node.ArtifactDigest && !visibleDigests.Contains(e.ChildDigest))
.Count();
var hiddenParents = allEdges
.Where(e => e.ToDigest == node.ArtifactDigest && !visibleDigests.Contains(e.FromDigest))
.Where(e => e.ChildDigest == node.ArtifactDigest && !visibleDigests.Contains(e.ParentDigest))
.Count();
if (hiddenChildren > 0 || hiddenParents > 0)

View File

@@ -46,14 +46,9 @@ public sealed class LineageStreamControllerTests
{
// Arrange
var digest = "sha256:abc123";
_graphService.SetupGraph(digest, new LineageGraphResponse(
new LineageGraphDto(
Nodes: ImmutableArray.Create(
new LineageNodeDto(digest, "app", "1.0.0", 10),
new LineageNodeDto("sha256:child", "lib", "1.0.0", 5)),
Edges: ImmutableArray.Create(
new LineageEdgeDto(digest, "sha256:child"))),
Enrichment: null));
_graphService.SetupGraph(digest, CreateGraphResponse(
new[] { CreateNode(digest), CreateNode("sha256:child") },
new[] { CreateEdge(digest, "sha256:child") }));
// Act
var result = await _controller.GetOptimizedLineage(digest, maxDepth: 3, pageSize: 50, pageNumber: 0);
@@ -103,12 +98,9 @@ public sealed class LineageStreamControllerTests
{
// Arrange
var digest = "sha256:meta123";
_graphService.SetupGraph(digest, new LineageGraphResponse(
new LineageGraphDto(
Nodes: ImmutableArray.Create(
new LineageNodeDto(digest, "app", "1.0.0", 10)),
Edges: ImmutableArray<LineageEdgeDto>.Empty),
Enrichment: null));
_graphService.SetupGraph(digest, CreateGraphResponse(
new[] { CreateNode(digest) },
Array.Empty<LineageEdge>()));
// Act
var result = await _controller.GetMetadata(digest);
@@ -145,16 +137,18 @@ public sealed class LineageStreamControllerTests
{
// Arrange
var digest = "sha256:center";
_graphService.SetupGraph(digest, new LineageGraphResponse(
new LineageGraphDto(
Nodes: ImmutableArray.Create(
new LineageNodeDto(digest, "center-app", "1.0.0", 10),
new LineageNodeDto("sha256:logging", "logging-lib", "1.0.0", 5),
new LineageNodeDto("sha256:database", "database-lib", "1.0.0", 8)),
Edges: ImmutableArray.Create(
new LineageEdgeDto(digest, "sha256:logging"),
new LineageEdgeDto(digest, "sha256:database"))),
Enrichment: null));
_graphService.SetupGraph(digest, CreateGraphResponse(
new[]
{
CreateNode(digest),
CreateNode("sha256:logging"),
CreateNode("sha256:database")
},
new[]
{
CreateEdge(digest, "sha256:logging"),
CreateEdge(digest, "sha256:database")
}));
// Act
var result = await _controller.GetOptimizedLineage(digest, searchTerm: "log");
@@ -172,24 +166,17 @@ public sealed class LineageStreamControllerTests
{
// Arrange
var digest = "sha256:center";
var nodes = new List<LineageNodeDto>
{
new(digest, "center", "1.0.0", 10)
};
var edges = new List<LineageEdgeDto>();
var nodes = new List<LineageNode> { CreateNode(digest) };
var edges = new List<LineageEdge>();
for (int i = 0; i < 20; i++)
{
var childDigest = $"sha256:child{i:D2}";
nodes.Add(new LineageNodeDto(childDigest, $"child-{i}", "1.0.0", i + 1));
edges.Add(new LineageEdgeDto(digest, childDigest));
nodes.Add(CreateNode(childDigest));
edges.Add(CreateEdge(digest, childDigest));
}
_graphService.SetupGraph(digest, new LineageGraphResponse(
new LineageGraphDto(
Nodes: nodes.ToImmutableArray(),
Edges: edges.ToImmutableArray()),
Enrichment: null));
_graphService.SetupGraph(digest, CreateGraphResponse(nodes, edges));
// Act
var result = await _controller.GetOptimizedLineage(digest, pageSize: 5, pageNumber: 0);
@@ -201,6 +188,19 @@ public sealed class LineageStreamControllerTests
graph.PageNumber.Should().Be(0);
}
// Helper methods
private static LineageNode CreateNode(string artifactDigest) =>
new(artifactDigest, Guid.NewGuid(), 1, DateTimeOffset.UtcNow, null);
private static LineageEdge CreateEdge(string parent, string child) =>
new(Guid.NewGuid(), parent, child, LineageRelationship.Parent, Guid.Empty, DateTimeOffset.UtcNow);
private static LineageGraphResponse CreateGraphResponse(
IEnumerable<LineageNode> nodes,
IEnumerable<LineageEdge> edges) =>
new(new LineageGraph(nodes.ToList(), edges.ToList()),
new Dictionary<string, NodeEnrichment>());
// Test helper implementations
private sealed class InMemoryLineageStreamService : ILineageStreamService
{
@@ -216,76 +216,80 @@ public sealed class LineageStreamControllerTests
public async IAsyncEnumerable<LineageUpdateEvent> SubscribeAsync(
Guid tenantId,
IReadOnlyList<string>? watchDigests = null,
CancellationToken ct = default)
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
await Task.CompletedTask;
yield break;
}
public Task PublishAsync(Guid tenantId, LineageUpdateEvent evt, CancellationToken ct = default)
=> Task.CompletedTask;
public ValueTask PublishAsync(LineageUpdateEvent updateEvent, CancellationToken ct = default)
=> ValueTask.CompletedTask;
public Task NotifySbomAddedAsync(Guid tenantId, string artifactDigest, string? parentDigest,
public ValueTask NotifySbomAddedAsync(Guid tenantId, string artifactDigest, string? parentDigest,
SbomVersionSummary summary, CancellationToken ct = default)
=> Task.CompletedTask;
=> ValueTask.CompletedTask;
public Task NotifyVexChangedAsync(Guid tenantId, string artifactDigest, VexChangeData change,
public ValueTask NotifyVexChangedAsync(Guid tenantId, string artifactDigest, VexChangeData change,
CancellationToken ct = default)
=> Task.CompletedTask;
=> ValueTask.CompletedTask;
public Task NotifyReachabilityUpdatedAsync(Guid tenantId, string artifactDigest, ReachabilityUpdateData update,
public ValueTask NotifyReachabilityUpdatedAsync(Guid tenantId, string artifactDigest, ReachabilityUpdateData update,
CancellationToken ct = default)
=> Task.CompletedTask;
=> ValueTask.CompletedTask;
public Task NotifyEdgeChangedAsync(Guid tenantId, string fromDigest, string toDigest,
public ValueTask NotifyEdgeChangedAsync(Guid tenantId, string fromDigest, string toDigest,
LineageEdgeChangeType changeType, CancellationToken ct = default)
=> Task.CompletedTask;
=> ValueTask.CompletedTask;
}
private sealed class InMemoryLineageGraphOptimizer : ILineageGraphOptimizer
{
public LineageOptimizationRequest? LastRequest { get; private set; }
public OptimizedLineageGraph Optimize(LineageOptimizationRequest request)
public OptimizedLineageGraph Optimize(
LineageGraph fullGraph,
LineageOptimizationRequest request)
{
LastRequest = request;
return new OptimizedLineageGraph
{
Nodes = request.AllNodes,
Edges = request.AllEdges,
Nodes = fullGraph.Nodes,
Edges = fullGraph.Edges,
BoundaryNodes = ImmutableArray<BoundaryNodeInfo>.Empty,
TotalNodes = request.AllNodes.Length,
HasMorePages = false
TotalNodeCount = fullGraph.Nodes.Count,
HasMore = false,
Offset = request.Offset,
Limit = request.Limit,
OptimizationTimeMs = 0
};
}
public async IAsyncEnumerable<LineageLevel> TraverseLevelsAsync(
string centerDigest,
ImmutableArray<LineageNode> nodes,
ImmutableArray<LineageEdge> edges,
TraversalDirection direction,
int maxDepth = 10,
CancellationToken ct = default)
Func<string, CancellationToken, Task<IReadOnlyList<LineageNode>>> getChildrenAsync,
Func<string, CancellationToken, Task<IReadOnlyList<LineageNode>>> getParentsAsync,
int maxDepth = 5,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
await Task.CompletedTask;
yield return new LineageLevel(0, nodes, true);
yield return new LineageLevel
{
Level = 0,
Direction = TraversalDirection.Center,
NodeDigests = ImmutableArray.Create(centerDigest)
};
}
public Task<LineageGraphMetadata> GetOrComputeMetadataAsync(
string artifactDigest,
Guid tenantId,
string centerDigest,
ImmutableArray<LineageNode> nodes,
ImmutableArray<LineageEdge> edges,
Func<CancellationToken, Task<LineageGraphMetadata>> computeAsync,
CancellationToken ct = default)
{
return Task.FromResult(new LineageGraphMetadata(
TotalNodes: nodes.Length,
TotalEdges: edges.Length,
MaxDepth: 1,
ComputedAt: DateTimeOffset.UtcNow));
return computeAsync(ct);
}
public Task InvalidateCacheAsync(Guid tenantId, string centerDigest, CancellationToken ct = default)
public Task InvalidateCacheAsync(string artifactDigest, Guid tenantId, CancellationToken ct = default)
=> Task.CompletedTask;
}
@@ -308,8 +312,10 @@ public sealed class LineageStreamControllerTests
return ValueTask.FromResult(response);
return ValueTask.FromResult(new LineageGraphResponse(
new LineageGraphDto(ImmutableArray<LineageNodeDto>.Empty, ImmutableArray<LineageEdgeDto>.Empty),
null));
new LineageGraph(
ImmutableArray<LineageNode>.Empty,
ImmutableArray<LineageEdge>.Empty),
new Dictionary<string, NodeEnrichment>()));
}
public ValueTask<LineageDiffResponse> GetDiffAsync(
@@ -319,9 +325,15 @@ public sealed class LineageStreamControllerTests
CancellationToken ct = default)
{
return ValueTask.FromResult(new LineageDiffResponse(
ImmutableArray<LineageChangeSummary>.Empty,
ImmutableArray<LineageChangeSummary>.Empty,
ImmutableArray<LineageChangeSummary>.Empty));
fromDigest,
toDigest,
new SbomDiff(
ImmutableArray<ComponentChange>.Empty,
ImmutableArray<ComponentChange>.Empty,
ImmutableArray<ComponentChange>.Empty),
new VexDiff(
ImmutableArray<VexDelta>.Empty, 0, 0, 0, 0),
null));
}
public ValueTask<ExportResult> ExportEvidencePackAsync(
@@ -329,21 +341,11 @@ public sealed class LineageStreamControllerTests
Guid tenantId,
CancellationToken ct = default)
{
return ValueTask.FromResult(new ExportResult("https://example.com/pack.zip", 1024));
return ValueTask.FromResult(new ExportResult(
"https://example.com/pack.zip",
DateTimeOffset.UtcNow.AddHours(1),
1024,
null));
}
}
}
// Placeholder types to match interface expectations
file record LineageNodeDto(string Digest, string Name, string Version, int ComponentCount);
file record LineageEdgeDto(string FromDigest, string ToDigest);
file record LineageGraphDto(ImmutableArray<LineageNodeDto> Nodes, ImmutableArray<LineageEdgeDto> Edges);
file record LineageGraphResponse(LineageGraphDto Graph, object? Enrichment);
file record LineageDiffResponse(
ImmutableArray<LineageChangeSummary> Added,
ImmutableArray<LineageChangeSummary> Removed,
ImmutableArray<LineageChangeSummary> Modified);
file record LineageChangeSummary(string Digest, string Name);
file record ExportRequest(string ArtifactDigest, int MaxDepth);
file record ExportResult(string DownloadUrl, long SizeBytes);
file record LineageQueryOptions(int MaxDepth, bool IncludeVerdicts, bool IncludeBadges);