wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10

This commit is contained in:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -1,11 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Doctor.Engine;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -19,6 +23,12 @@ internal static class KnowledgeSearchCommandGroup
WriteIndented = true
};
private const string DefaultDocsAllowListPath = "src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/knowledge-docs-allowlist.json";
private const string DefaultDocsManifestPath = "src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/knowledge-docs-manifest.json";
private const string DefaultDoctorSeedPath = "src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/doctor-search-seed.json";
private const string DefaultDoctorControlsPath = "src/AdvisoryAI/StellaOps.AdvisoryAI/KnowledgeSearch/doctor-search-controls.json";
private const string DefaultOpenApiAggregatePath = "devops/compose/openapi_current.json";
private static readonly HashSet<string> AllowedTypes = new(StringComparer.Ordinal)
{
"docs",
@@ -124,22 +134,97 @@ internal static class KnowledgeSearchCommandGroup
var advisoryAi = new Command("advisoryai", "AdvisoryAI maintenance commands.");
var index = new Command("index", "Knowledge index operations.");
var rebuild = new Command("rebuild", "Rebuild AdvisoryAI deterministic knowledge index.");
var jsonOption = new Option<bool>("--json")
var rebuildJsonOption = new Option<bool>("--json")
{
Description = "Emit machine-readable JSON output."
};
rebuild.Add(jsonOption);
rebuild.Add(rebuildJsonOption);
rebuild.Add(verboseOption);
rebuild.SetAction(async (parseResult, _) =>
{
var emitJson = parseResult.GetValue(jsonOption);
var emitJson = parseResult.GetValue(rebuildJsonOption);
var verbose = parseResult.GetValue(verboseOption);
await ExecuteRebuildAsync(services, emitJson, verbose, cancellationToken).ConfigureAwait(false);
});
var sources = new Command("sources", "Prepare deterministic knowledge source artifacts.");
var prepare = new Command("prepare", "Aggregate docs allowlist, OpenAPI snapshot, and doctor controls seed data.");
var repoRootOption = new Option<string>("--repo-root")
{
Description = "Repository root used to resolve relative source paths."
};
repoRootOption.SetDefaultValue(".");
var docsAllowListOption = new Option<string>("--docs-allowlist")
{
Description = "Path to docs allowlist JSON (include/includes/paths)."
};
docsAllowListOption.SetDefaultValue(DefaultDocsAllowListPath);
var docsManifestOutputOption = new Option<string>("--docs-manifest-output")
{
Description = "Output path for resolved markdown manifest JSON."
};
docsManifestOutputOption.SetDefaultValue(DefaultDocsManifestPath);
var openApiOutputOption = new Option<string>("--openapi-output")
{
Description = "Output path for aggregated OpenAPI snapshot."
};
openApiOutputOption.SetDefaultValue(DefaultOpenApiAggregatePath);
var doctorSeedOption = new Option<string>("--doctor-seed")
{
Description = "Input doctor seed JSON used to derive controls."
};
doctorSeedOption.SetDefaultValue(DefaultDoctorSeedPath);
var doctorControlsOutputOption = new Option<string>("--doctor-controls-output")
{
Description = "Output path for doctor controls JSON."
};
doctorControlsOutputOption.SetDefaultValue(DefaultDoctorControlsPath);
var overwriteOption = new Option<bool>("--overwrite")
{
Description = "Overwrite generated artifacts and OpenAPI snapshot."
};
var prepareJsonOption = new Option<bool>("--json")
{
Description = "Emit machine-readable JSON output."
};
prepare.Add(repoRootOption);
prepare.Add(docsAllowListOption);
prepare.Add(docsManifestOutputOption);
prepare.Add(openApiOutputOption);
prepare.Add(doctorSeedOption);
prepare.Add(doctorControlsOutputOption);
prepare.Add(overwriteOption);
prepare.Add(prepareJsonOption);
prepare.Add(verboseOption);
prepare.SetAction(async (parseResult, _) =>
{
await ExecutePrepareSourcesAsync(
services,
repoRoot: parseResult.GetValue(repoRootOption) ?? ".",
docsAllowListPath: parseResult.GetValue(docsAllowListOption) ?? DefaultDocsAllowListPath,
docsManifestOutputPath: parseResult.GetValue(docsManifestOutputOption) ?? DefaultDocsManifestPath,
openApiOutputPath: parseResult.GetValue(openApiOutputOption) ?? DefaultOpenApiAggregatePath,
doctorSeedPath: parseResult.GetValue(doctorSeedOption) ?? DefaultDoctorSeedPath,
doctorControlsOutputPath: parseResult.GetValue(doctorControlsOutputOption) ?? DefaultDoctorControlsPath,
overwrite: parseResult.GetValue(overwriteOption),
emitJson: parseResult.GetValue(prepareJsonOption),
verbose: parseResult.GetValue(verboseOption),
cancellationToken).ConfigureAwait(false);
});
sources.Add(prepare);
index.Add(rebuild);
advisoryAi.Add(index);
advisoryAi.Add(sources);
return advisoryAi;
}
@@ -318,6 +403,585 @@ internal static class KnowledgeSearchCommandGroup
}
}
private static async Task ExecutePrepareSourcesAsync(
IServiceProvider services,
string repoRoot,
string docsAllowListPath,
string docsManifestOutputPath,
string openApiOutputPath,
string doctorSeedPath,
string doctorControlsOutputPath,
bool overwrite,
bool emitJson,
bool verbose,
CancellationToken cancellationToken)
{
try
{
var repositoryRoot = ResolvePath(Directory.GetCurrentDirectory(), repoRoot);
var docsAllowListAbsolute = ResolvePath(repositoryRoot, docsAllowListPath);
var docsManifestAbsolute = ResolvePath(repositoryRoot, docsManifestOutputPath);
var openApiAbsolute = ResolvePath(repositoryRoot, openApiOutputPath);
var doctorSeedAbsolute = ResolvePath(repositoryRoot, doctorSeedPath);
var doctorControlsAbsolute = ResolvePath(repositoryRoot, doctorControlsOutputPath);
var includeEntries = LoadAllowListIncludes(docsAllowListAbsolute);
var markdownFiles = ResolveMarkdownFiles(repositoryRoot, includeEntries);
var markdownDocuments = markdownFiles
.Select(path => new DocsManifestDocument(
path,
ComputeSha256Hex(ResolvePath(repositoryRoot, path))))
.ToArray();
if (verbose)
{
Console.WriteLine($"Docs allowlist: {docsAllowListAbsolute}");
Console.WriteLine($"Resolved markdown files: {markdownFiles.Count}");
}
WriteDocsManifest(
docsManifestAbsolute,
includeEntries,
markdownDocuments,
overwrite);
var backend = services.GetRequiredService<IBackendOperationsClient>();
var apiResult = await backend.DownloadApiSpecAsync(
new ApiSpecDownloadRequest
{
OutputPath = openApiAbsolute,
Service = null,
Format = "openapi-json",
Overwrite = overwrite,
ChecksumAlgorithm = "sha256"
},
cancellationToken).ConfigureAwait(false);
if (!apiResult.Success)
{
Console.Error.WriteLine($"OpenAPI aggregation failed: {apiResult.Error ?? "unknown error"}");
Environment.ExitCode = CliExitCodes.GeneralError;
return;
}
var configuredDoctorSeedEntries = LoadDoctorSeedEntries(doctorSeedAbsolute);
var discoveredDoctorEntries = LoadDoctorSeedEntriesFromEngine(services, verbose);
var doctorSeedEntries = MergeDoctorSeedEntries(configuredDoctorSeedEntries, discoveredDoctorEntries);
var doctorControlEntries = BuildDoctorControls(doctorSeedEntries);
WriteDoctorControls(
doctorControlsAbsolute,
doctorControlEntries,
overwrite);
if (emitJson)
{
WriteJson(new
{
repositoryRoot,
docs = new
{
allowListPath = ToRelativePath(repositoryRoot, docsAllowListAbsolute),
manifestPath = ToRelativePath(repositoryRoot, docsManifestAbsolute),
includeCount = includeEntries.Count,
documentCount = markdownDocuments.Length
},
openApi = new
{
path = ToRelativePath(repositoryRoot, openApiAbsolute),
fromCache = apiResult.FromCache,
checksum = apiResult.Checksum
},
doctor = new
{
seedPath = ToRelativePath(repositoryRoot, doctorSeedAbsolute),
configuredSeedCount = configuredDoctorSeedEntries.Count,
discoveredSeedCount = discoveredDoctorEntries.Count,
mergedSeedCount = doctorSeedEntries.Count,
controlsPath = ToRelativePath(repositoryRoot, doctorControlsAbsolute),
controlCount = doctorControlEntries.Length
}
});
Environment.ExitCode = 0;
return;
}
Console.WriteLine("AdvisoryAI source artifacts prepared.");
Console.WriteLine($" Docs allowlist: {ToRelativePath(repositoryRoot, docsAllowListAbsolute)}");
Console.WriteLine($" Docs manifest: {ToRelativePath(repositoryRoot, docsManifestAbsolute)} ({markdownDocuments.Length} markdown files)");
Console.WriteLine($" OpenAPI aggregate: {ToRelativePath(repositoryRoot, openApiAbsolute)}");
Console.WriteLine($" Doctor seed (configured/discovered/merged): {configuredDoctorSeedEntries.Count}/{discoveredDoctorEntries.Count}/{doctorSeedEntries.Count}");
Console.WriteLine($" Doctor controls: {ToRelativePath(repositoryRoot, doctorControlsAbsolute)} ({doctorControlEntries.Length} checks)");
Environment.ExitCode = 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"AdvisoryAI source preparation failed: {ex.Message}");
Environment.ExitCode = CliExitCodes.GeneralError;
}
}
private static string ResolvePath(string repositoryRoot, string configuredPath)
{
if (string.IsNullOrWhiteSpace(configuredPath))
{
return repositoryRoot;
}
return Path.IsPathRooted(configuredPath)
? Path.GetFullPath(configuredPath)
: Path.GetFullPath(Path.Combine(repositoryRoot, configuredPath));
}
private static string ToRelativePath(string repositoryRoot, string absolutePath)
{
var relative = Path.GetRelativePath(repositoryRoot, absolutePath).Replace('\\', '/');
return string.IsNullOrWhiteSpace(relative) ? "." : relative;
}
private static IReadOnlyList<string> LoadAllowListIncludes(string allowListPath)
{
if (!File.Exists(allowListPath))
{
throw new FileNotFoundException($"Docs allowlist file was not found: {allowListPath}", allowListPath);
}
using var stream = File.OpenRead(allowListPath);
using var document = JsonDocument.Parse(stream);
var root = document.RootElement;
if (root.ValueKind == JsonValueKind.Array)
{
return ReadStringList(root);
}
if (root.ValueKind != JsonValueKind.Object)
{
return [];
}
if (TryGetStringListProperty(root, "include", out var include))
{
return include;
}
if (TryGetStringListProperty(root, "includes", out include))
{
return include;
}
if (TryGetStringListProperty(root, "paths", out include))
{
return include;
}
return [];
}
private static IReadOnlyList<string> ResolveMarkdownFiles(string repositoryRoot, IReadOnlyList<string> includeEntries)
{
var files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var include in includeEntries)
{
if (string.IsNullOrWhiteSpace(include))
{
continue;
}
var absolutePath = ResolvePath(repositoryRoot, include);
if (File.Exists(absolutePath))
{
if (Path.GetExtension(absolutePath).Equals(".md", StringComparison.OrdinalIgnoreCase))
{
files.Add(ToRelativePath(repositoryRoot, Path.GetFullPath(absolutePath)));
}
continue;
}
if (!Directory.Exists(absolutePath))
{
continue;
}
foreach (var file in Directory.EnumerateFiles(absolutePath, "*.md", SearchOption.AllDirectories))
{
files.Add(ToRelativePath(repositoryRoot, Path.GetFullPath(file)));
}
}
return files.OrderBy(static file => file, StringComparer.Ordinal).ToArray();
}
private static void WriteDocsManifest(
string outputPath,
IReadOnlyList<string> includeEntries,
IReadOnlyList<DocsManifestDocument> documents,
bool overwrite)
{
if (File.Exists(outputPath) && !overwrite)
{
return;
}
var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var payload = new
{
schema = "stellaops.advisoryai.docs-manifest.v1",
include = includeEntries.OrderBy(static entry => entry, StringComparer.Ordinal).ToArray(),
documents = documents.OrderBy(static entry => entry.Path, StringComparer.Ordinal).ToArray()
};
File.WriteAllText(outputPath, JsonSerializer.Serialize(payload, JsonOutputOptions));
}
private static IReadOnlyList<DoctorSeedEntry> LoadDoctorSeedEntries(string doctorSeedPath)
{
if (!File.Exists(doctorSeedPath))
{
return [];
}
using var stream = File.OpenRead(doctorSeedPath);
using var document = JsonDocument.Parse(stream);
if (document.RootElement.ValueKind != JsonValueKind.Array)
{
return [];
}
var entries = new List<DoctorSeedEntry>();
foreach (var item in document.RootElement.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.Object)
{
continue;
}
var checkCode = ReadString(item, "checkCode");
if (string.IsNullOrWhiteSpace(checkCode))
{
continue;
}
entries.Add(new DoctorSeedEntry(
checkCode,
ReadString(item, "title") ?? checkCode,
ReadString(item, "severity") ?? "warn",
ReadString(item, "description") ?? string.Empty,
ReadString(item, "remediation") ?? string.Empty,
ReadString(item, "runCommand") ?? $"stella doctor run --check {checkCode}",
TryGetStringListProperty(item, "symptoms", out var symptoms) ? symptoms : [],
TryGetStringListProperty(item, "tags", out var tags) ? tags : [],
TryGetStringListProperty(item, "references", out var references) ? references : []));
}
return entries
.OrderBy(static entry => entry.CheckCode, StringComparer.Ordinal)
.ToArray();
}
private static IReadOnlyList<DoctorSeedEntry> LoadDoctorSeedEntriesFromEngine(IServiceProvider services, bool verbose)
{
var engine = services.GetService<DoctorEngine>();
if (engine is null)
{
return [];
}
try
{
var checks = engine.ListChecks();
return checks
.OrderBy(static check => check.CheckId, StringComparer.Ordinal)
.Select(static check =>
{
var runCommand = $"stella doctor run --check {check.CheckId}";
var references = MergeUniqueStrings(
string.IsNullOrWhiteSpace(check.Category) ? [] : [$"category:{check.Category}"],
string.IsNullOrWhiteSpace(check.PluginId) ? [] : [$"plugin:{check.PluginId}"]);
var tags = MergeUniqueStrings(
check.Tags,
string.IsNullOrWhiteSpace(check.Category) ? [] : [check.Category],
["doctor", "diagnostics"]);
return new DoctorSeedEntry(
check.CheckId,
string.IsNullOrWhiteSpace(check.Name) ? check.CheckId : check.Name.Trim(),
check.DefaultSeverity.ToString().ToLowerInvariant(),
(check.Description ?? string.Empty).Trim(),
$"Run `{runCommand}` and follow the diagnosis output.",
runCommand,
BuildSymptomsFromCheckText(check.Name, check.Description, check.Tags),
tags,
references);
})
.ToArray();
}
catch (Exception ex)
{
if (verbose)
{
Console.Error.WriteLine($"Doctor check discovery from engine failed: {ex.Message}");
}
return [];
}
}
private static IReadOnlyList<DoctorSeedEntry> MergeDoctorSeedEntries(
IReadOnlyList<DoctorSeedEntry> configuredEntries,
IReadOnlyList<DoctorSeedEntry> discoveredEntries)
{
var merged = discoveredEntries
.ToDictionary(static entry => entry.CheckCode, StringComparer.OrdinalIgnoreCase);
foreach (var configured in configuredEntries)
{
if (!merged.TryGetValue(configured.CheckCode, out var discovered))
{
merged[configured.CheckCode] = configured;
continue;
}
merged[configured.CheckCode] = new DoctorSeedEntry(
configured.CheckCode,
PreferConfigured(configured.Title, discovered.Title, configured.CheckCode),
PreferConfigured(configured.Severity, discovered.Severity, "warn"),
PreferConfigured(configured.Description, discovered.Description, string.Empty),
PreferConfigured(configured.Remediation, discovered.Remediation, string.Empty),
PreferConfigured(configured.RunCommand, discovered.RunCommand, $"stella doctor run --check {configured.CheckCode}"),
MergeUniqueStrings(configured.Symptoms, discovered.Symptoms),
MergeUniqueStrings(configured.Tags, discovered.Tags),
MergeUniqueStrings(configured.References, discovered.References));
}
return merged.Values
.OrderBy(static entry => entry.CheckCode, StringComparer.Ordinal)
.ToArray();
}
private static DoctorControlEntry[] BuildDoctorControls(IReadOnlyList<DoctorSeedEntry> seedEntries)
{
return seedEntries
.OrderBy(static entry => entry.CheckCode, StringComparer.Ordinal)
.Select(static entry =>
{
var mode = InferControlMode(entry.Severity);
var requiresConfirmation = !mode.Equals("safe", StringComparison.Ordinal);
return new DoctorControlEntry(
entry.CheckCode,
mode,
requiresConfirmation,
IsDestructive: false,
RequiresBackup: false,
InspectCommand: $"stella doctor run --check {entry.CheckCode} --mode quick",
VerificationCommand: string.IsNullOrWhiteSpace(entry.RunCommand)
? $"stella doctor run --check {entry.CheckCode}"
: entry.RunCommand.Trim(),
Keywords: BuildKeywordList(entry),
Title: entry.Title,
Severity: entry.Severity,
Description: entry.Description,
Remediation: entry.Remediation,
RunCommand: string.IsNullOrWhiteSpace(entry.RunCommand)
? $"stella doctor run --check {entry.CheckCode}"
: entry.RunCommand.Trim(),
Symptoms: entry.Symptoms,
Tags: entry.Tags,
References: entry.References);
})
.ToArray();
}
private static string InferControlMode(string severity)
{
var normalized = (severity ?? string.Empty).Trim().ToLowerInvariant();
return normalized switch
{
"critical" => "manual",
"error" => "manual",
"fail" => "manual",
"failure" => "manual",
"high" => "manual",
_ => "safe"
};
}
private static IReadOnlyList<string> BuildKeywordList(DoctorSeedEntry entry)
{
var terms = new SortedSet<string>(StringComparer.Ordinal);
foreach (var symptom in entry.Symptoms)
{
if (!string.IsNullOrWhiteSpace(symptom))
{
terms.Add(symptom.Trim().ToLowerInvariant());
}
}
foreach (var token in (entry.Title + " " + entry.Description)
.Split(new[] { ' ', '\t', '\r', '\n', ',', ';', ':', '.', '(', ')', '[', ']', '{', '}', '"', '\'' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (token.Length >= 4)
{
terms.Add(token.Trim().ToLowerInvariant());
}
}
return terms.Take(12).ToArray();
}
private static IReadOnlyList<string> BuildSymptomsFromCheckText(
string? title,
string? description,
IReadOnlyList<string>? tags)
{
var terms = new SortedSet<string>(StringComparer.Ordinal);
foreach (var token in (title + " " + description)
.Split(new[] { ' ', '\t', '\r', '\n', ',', ';', ':', '.', '(', ')', '[', ']', '{', '}', '"', '\'' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (token.Length >= 5)
{
terms.Add(token.Trim().ToLowerInvariant());
}
}
if (tags is not null)
{
foreach (var tag in tags)
{
if (!string.IsNullOrWhiteSpace(tag))
{
terms.Add(tag.Trim().ToLowerInvariant());
}
}
}
return terms.Take(12).ToArray();
}
private static IReadOnlyList<string> MergeUniqueStrings(params IReadOnlyList<string>?[] values)
{
var merged = new SortedSet<string>(StringComparer.Ordinal);
foreach (var source in values)
{
if (source is null)
{
continue;
}
foreach (var value in source)
{
if (!string.IsNullOrWhiteSpace(value))
{
merged.Add(value.Trim());
}
}
}
return merged.ToArray();
}
private static string PreferConfigured(string? configured, string? discovered, string fallback)
{
if (!string.IsNullOrWhiteSpace(configured))
{
return configured.Trim();
}
if (!string.IsNullOrWhiteSpace(discovered))
{
return discovered.Trim();
}
return fallback;
}
private static void WriteDoctorControls(string outputPath, IReadOnlyList<DoctorControlEntry> controls, bool overwrite)
{
if (File.Exists(outputPath) && !overwrite)
{
return;
}
var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var payload = controls
.OrderBy(static entry => entry.CheckCode, StringComparer.Ordinal)
.Select(static entry => new
{
checkCode = entry.CheckCode,
control = entry.Control,
requiresConfirmation = entry.RequiresConfirmation,
isDestructive = entry.IsDestructive,
requiresBackup = entry.RequiresBackup,
inspectCommand = entry.InspectCommand,
verificationCommand = entry.VerificationCommand,
keywords = entry.Keywords,
title = entry.Title,
severity = entry.Severity,
description = entry.Description,
remediation = entry.Remediation,
runCommand = entry.RunCommand,
symptoms = entry.Symptoms,
tags = entry.Tags,
references = entry.References
})
.ToArray();
File.WriteAllText(outputPath, JsonSerializer.Serialize(payload, JsonOutputOptions));
}
private static string ComputeSha256Hex(string absolutePath)
{
using var stream = File.OpenRead(absolutePath);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static bool TryGetStringListProperty(JsonElement element, string propertyName, out IReadOnlyList<string> values)
{
if (element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(propertyName, out var property) &&
property.ValueKind == JsonValueKind.Array)
{
values = ReadStringList(property);
return true;
}
values = [];
return false;
}
private static string? ReadString(JsonElement element, string propertyName)
{
return element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(propertyName, out var property) &&
property.ValueKind == JsonValueKind.String
? property.GetString()
: null;
}
private static IReadOnlyList<string> ReadStringList(JsonElement array)
{
return array.EnumerateArray()
.Where(static item => item.ValueKind == JsonValueKind.String)
.Select(static item => item.GetString())
.Where(static item => !string.IsNullOrWhiteSpace(item))
.Select(static item => item!.Trim())
.Distinct(StringComparer.Ordinal)
.OrderBy(static item => item, StringComparer.Ordinal)
.ToArray();
}
private static AdvisoryKnowledgeSearchFilterModel? BuildFilter(
IReadOnlyList<string> types,
IReadOnlyList<string> tags,
@@ -518,7 +1182,7 @@ internal static class KnowledgeSearchCommandGroup
if (result.Type.Equals("doctor", StringComparison.OrdinalIgnoreCase) && result.Open.Doctor is not null)
{
var doctor = result.Open.Doctor;
return $"doctor: {doctor.CheckCode} severity={doctor.Severity} run=\"{doctor.RunCommand}\"";
return $"doctor: {doctor.CheckCode} severity={doctor.Severity} control={doctor.Control} confirm={doctor.RequiresConfirmation.ToString().ToLowerInvariant()} run=\"{doctor.RunCommand}\"";
}
return string.Empty;
@@ -580,6 +1244,39 @@ internal static class KnowledgeSearchCommandGroup
};
}
private sealed record DocsManifestDocument(
string Path,
string Sha256);
private sealed record DoctorSeedEntry(
string CheckCode,
string Title,
string Severity,
string Description,
string Remediation,
string RunCommand,
IReadOnlyList<string> Symptoms,
IReadOnlyList<string> Tags,
IReadOnlyList<string> References);
private sealed record DoctorControlEntry(
string CheckCode,
string Control,
bool RequiresConfirmation,
bool IsDestructive,
bool RequiresBackup,
string InspectCommand,
string VerificationCommand,
IReadOnlyList<string> Keywords,
string Title,
string Severity,
string Description,
string Remediation,
string RunCommand,
IReadOnlyList<string> Symptoms,
IReadOnlyList<string> Tags,
IReadOnlyList<string> References);
private static void WriteJson(object payload)
{
Console.WriteLine(JsonSerializer.Serialize(payload, JsonOutputOptions));