partly or unimplemented features - now implemented
This commit is contained in:
@@ -23,6 +23,8 @@ namespace StellaOps.Cli.Commands;
|
||||
/// </summary>
|
||||
public static class UnknownsCommandGroup
|
||||
{
|
||||
private const string DefaultUnknownsExportSchemaVersion = "unknowns.export.v1";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
@@ -395,6 +397,13 @@ public static class UnknownsCommandGroup
|
||||
Description = "Output format: json, csv, ndjson"
|
||||
};
|
||||
formatOption.SetDefaultValue("json");
|
||||
formatOption.FromAmong("json", "csv", "ndjson");
|
||||
|
||||
var schemaVersionOption = new Option<string>("--schema-version")
|
||||
{
|
||||
Description = "Schema version to stamp into exported artifacts."
|
||||
};
|
||||
schemaVersionOption.SetDefaultValue(DefaultUnknownsExportSchemaVersion);
|
||||
|
||||
var outputOption = new Option<string?>("--output", new[] { "-o" })
|
||||
{
|
||||
@@ -404,6 +413,7 @@ public static class UnknownsCommandGroup
|
||||
var exportCommand = new Command("export", "Export unknowns with fingerprints and triggers for offline analysis");
|
||||
exportCommand.Add(bandOption);
|
||||
exportCommand.Add(formatOption);
|
||||
exportCommand.Add(schemaVersionOption);
|
||||
exportCommand.Add(outputOption);
|
||||
exportCommand.Add(verboseOption);
|
||||
|
||||
@@ -411,10 +421,11 @@ public static class UnknownsCommandGroup
|
||||
{
|
||||
var band = parseResult.GetValue(bandOption);
|
||||
var format = parseResult.GetValue(formatOption) ?? "json";
|
||||
var schemaVersion = parseResult.GetValue(schemaVersionOption) ?? DefaultUnknownsExportSchemaVersion;
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await HandleExportAsync(services, band, format, output, verbose, cancellationToken);
|
||||
return await HandleExportAsync(services, band, format, schemaVersion, output, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return exportCommand;
|
||||
@@ -1009,6 +1020,7 @@ public static class UnknownsCommandGroup
|
||||
IServiceProvider services,
|
||||
string? band,
|
||||
string format,
|
||||
string schemaVersion,
|
||||
string? outputPath,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
@@ -1052,9 +1064,15 @@ public static class UnknownsCommandGroup
|
||||
|
||||
// Deterministic ordering by band priority, then score descending
|
||||
var sorted = result.Items
|
||||
.OrderBy(u => u.Band switch { "hot" => 0, "warm" => 1, "cold" => 2, _ => 3 })
|
||||
.OrderBy(u => u.Band.ToLowerInvariant() switch { "hot" => 0, "warm" => 1, "cold" => 2, _ => 3 })
|
||||
.ThenByDescending(u => u.Score)
|
||||
.ThenBy(u => u.PackageId, StringComparer.Ordinal)
|
||||
.ThenBy(u => u.PackageVersion, StringComparer.Ordinal)
|
||||
.ThenBy(u => u.Id)
|
||||
.ToList();
|
||||
var exportedAt = sorted.Count == 0
|
||||
? DateTimeOffset.UnixEpoch
|
||||
: sorted.Max(u => u.LastEvaluatedAt).ToUniversalTime();
|
||||
|
||||
TextWriter writer = outputPath is not null
|
||||
? new StreamWriter(outputPath)
|
||||
@@ -1065,17 +1083,33 @@ public static class UnknownsCommandGroup
|
||||
switch (format.ToLowerInvariant())
|
||||
{
|
||||
case "csv":
|
||||
await WriteCsvAsync(writer, sorted);
|
||||
await WriteCsvAsync(writer, sorted, schemaVersion, exportedAt);
|
||||
break;
|
||||
case "ndjson":
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(new
|
||||
{
|
||||
schemaVersion,
|
||||
exportedAt,
|
||||
itemCount = sorted.Count
|
||||
}, JsonOptions));
|
||||
foreach (var item in sorted)
|
||||
{
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(item, JsonOptions));
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(new
|
||||
{
|
||||
schemaVersion,
|
||||
item
|
||||
}, JsonOptions));
|
||||
}
|
||||
break;
|
||||
case "json":
|
||||
default:
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(sorted, JsonOptions));
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(new UnknownsExportEnvelope
|
||||
{
|
||||
SchemaVersion = schemaVersion,
|
||||
ExportedAt = exportedAt,
|
||||
ItemCount = sorted.Count,
|
||||
Items = sorted
|
||||
}, JsonOptions));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1102,8 +1136,13 @@ public static class UnknownsCommandGroup
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteCsvAsync(TextWriter writer, IReadOnlyList<UnknownDto> items)
|
||||
private static async Task WriteCsvAsync(
|
||||
TextWriter writer,
|
||||
IReadOnlyList<UnknownDto> items,
|
||||
string schemaVersion,
|
||||
DateTimeOffset exportedAt)
|
||||
{
|
||||
await writer.WriteLineAsync($"# schema_version={schemaVersion}; exported_at={exportedAt:O}; item_count={items.Count}");
|
||||
// CSV header
|
||||
await writer.WriteLineAsync("id,package_id,package_version,band,score,reason_code,fingerprint_id,first_seen_at,last_evaluated_at");
|
||||
|
||||
@@ -1590,6 +1629,14 @@ public static class UnknownsCommandGroup
|
||||
public int TotalCount { get; init; }
|
||||
}
|
||||
|
||||
private sealed record UnknownsExportEnvelope
|
||||
{
|
||||
public string SchemaVersion { get; init; } = DefaultUnknownsExportSchemaVersion;
|
||||
public DateTimeOffset ExportedAt { get; init; }
|
||||
public int ItemCount { get; init; }
|
||||
public IReadOnlyList<UnknownDto> Items { get; init; } = [];
|
||||
}
|
||||
|
||||
private sealed record UnknownDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user