texts fixes, search bar fixes, global menu fixes.

This commit is contained in:
master
2026-03-05 18:10:56 +02:00
parent 8e1cb9448d
commit a918d39a61
101 changed files with 3543 additions and 534 deletions

View File

@@ -55,13 +55,13 @@ public sealed class KnowledgeSearchOptions
public List<string> OpenApiRoots { get; set; } = ["src", "devops/compose"];
public string UnifiedFindingsSnapshotPath { get; set; } =
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/findings.snapshot.json";
"UnifiedSearch/Snapshots/findings.snapshot.json";
public string UnifiedVexSnapshotPath { get; set; } =
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/vex.snapshot.json";
"UnifiedSearch/Snapshots/vex.snapshot.json";
public string UnifiedPolicySnapshotPath { get; set; } =
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/policy.snapshot.json";
"UnifiedSearch/Snapshots/policy.snapshot.json";
public bool UnifiedAutoIndexEnabled { get; set; }

View File

@@ -13,7 +13,7 @@
<InternalsVisibleTo Include="StellaOps.AdvisoryAI.WebService" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Storage\Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
<EmbeddedResource Include="Storage\Migrations\**\*.sql" />
<EmbeddedResource Include="UnifiedSearch\Synthesis\synthesis-system-prompt.txt" LogicalName="synthesis-system-prompt.txt" />
</ItemGroup>
<ItemGroup>
@@ -34,6 +34,18 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>models/all-MiniLM-L6-v2.onnx</TargetPath>
</None>
<None Update="UnifiedSearch/Snapshots/findings.snapshot.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>UnifiedSearch/Snapshots/findings.snapshot.json</TargetPath>
</None>
<None Update="UnifiedSearch/Snapshots/vex.snapshot.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>UnifiedSearch/Snapshots/vex.snapshot.json</TargetPath>
</None>
<None Update="UnifiedSearch/Snapshots/policy.snapshot.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>UnifiedSearch/Snapshots/policy.snapshot.json</TargetPath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />

View File

@@ -16,7 +16,7 @@ internal sealed class FindingsSearchAdapter : ISearchIngestionAdapter
{
private const string TenantHeader = "X-StellaOps-Tenant";
private const string HttpClientName = "scanner-internal";
private const string FindingsEndpoint = "/api/v1/scanner/security/findings";
private const string FindingsEndpoint = "/api/v1/security/findings";
private const int MaxPages = 20;
private const int PageSize = 100;

View File

@@ -12,16 +12,19 @@ namespace StellaOps.AdvisoryAI.UnifiedSearch;
internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
{
private readonly KnowledgeSearchOptions _options;
private readonly IKnowledgeSearchStore _store;
private readonly IEnumerable<ISearchIngestionAdapter> _adapters;
private readonly ILogger<UnifiedSearchIndexer> _logger;
public UnifiedSearchIndexer(
IOptions<KnowledgeSearchOptions> options,
IKnowledgeSearchStore store,
IEnumerable<ISearchIngestionAdapter> adapters,
ILogger<UnifiedSearchIndexer> logger)
{
ArgumentNullException.ThrowIfNull(options);
_options = options.Value ?? new KnowledgeSearchOptions();
_store = store ?? throw new ArgumentNullException(nameof(store));
_adapters = adapters ?? throw new ArgumentNullException(nameof(adapters));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -39,6 +42,8 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
return new UnifiedSearchIndexSummary(0, 0, 0);
}
await _store.EnsureSchemaAsync(cancellationToken).ConfigureAwait(false);
var stopwatch = Stopwatch.StartNew();
var domains = 0;
var chunks = 0;
@@ -131,6 +136,8 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
return new UnifiedSearchIndexSummary(0, 0, 0);
}
await _store.EnsureSchemaAsync(cancellationToken).ConfigureAwait(false);
var stopwatch = Stopwatch.StartNew();
var domains = 0;
var chunks = 0;
@@ -348,11 +355,17 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
UnifiedChunk chunk,
CancellationToken cancellationToken)
{
var sourceRef = ResolveSourceRef(chunk);
var sourcePath = ResolveSourcePath(chunk);
const string sql = """
INSERT INTO advisoryai.kb_doc
(doc_id, doc_type, product, version, source_ref, path, title, content_hash, metadata, indexed_at)
VALUES (@doc_id, @doc_type, @product, @version, @source_ref, @path, @title, @content_hash, '{}'::jsonb, NOW())
ON CONFLICT (doc_id) DO NOTHING;
ON CONFLICT (doc_id) DO UPDATE SET
title = EXCLUDED.title,
content_hash = EXCLUDED.content_hash,
indexed_at = NOW();
""";
await using var command = connection.CreateCommand();
@@ -362,14 +375,34 @@ internal sealed class UnifiedSearchIndexer : IUnifiedSearchIndexer
command.Parameters.AddWithValue("doc_type", chunk.Domain);
command.Parameters.AddWithValue("product", "stella-ops");
command.Parameters.AddWithValue("version", "local");
command.Parameters.AddWithValue("source_ref", chunk.Domain);
command.Parameters.AddWithValue("path", chunk.Kind);
command.Parameters.AddWithValue("source_ref", sourceRef);
command.Parameters.AddWithValue("path", sourcePath);
command.Parameters.AddWithValue("title", chunk.Title);
command.Parameters.AddWithValue("content_hash", KnowledgeSearchText.StableId(chunk.Body));
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
private static string ResolveSourceRef(UnifiedChunk chunk)
{
if (!string.IsNullOrWhiteSpace(chunk.EntityKey))
{
return chunk.EntityKey.Trim();
}
return chunk.DocId;
}
private static string ResolveSourcePath(UnifiedChunk chunk)
{
if (!string.IsNullOrWhiteSpace(chunk.DocId))
{
return chunk.DocId;
}
return $"{chunk.Domain}/{chunk.Kind}";
}
private static IReadOnlyList<UnifiedChunk> DeduplicateChunks(IEnumerable<UnifiedChunk> chunks)
{
var byChunkId = new SortedDictionary<string, UnifiedChunk>(StringComparer.Ordinal);

View File

@@ -62,7 +62,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
handler.Requests.Should().ContainSingle();
handler.Requests[0].Tenant.Should().Be("global");
handler.Requests[0].Uri.Should().Contain("/api/v1/scanner/security/findings");
handler.Requests[0].Uri.Should().Contain("/api/v1/security/findings");
}
[Fact]
@@ -326,6 +326,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
var indexer = new UnifiedSearchIndexer(
options,
store,
[
new FindingsSearchAdapter(
new SingleClientFactory(findingsHandler, "http://scanner.local"),
@@ -357,6 +358,35 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
(await CountDomainChunksAsync(connection, "policy")).Should().Be(4);
}
[Fact]
public async Task UnifiedSearchIndexer_RebuildAllAsync_EnsuresSchema_WhenTablesAreMissing()
{
await using var fixture = await StartPostgresOrSkipAsync();
var options = Options.Create(new KnowledgeSearchOptions
{
Enabled = true,
ConnectionString = fixture.ConnectionString,
FtsLanguageConfig = "simple"
});
await using var store = new PostgresKnowledgeSearchStore(options, NullLogger<PostgresKnowledgeSearchStore>.Instance);
var adapter = new MutableAdapter("findings", [BuildFindingChunk("finding-seed", "CVE-2026-3000", "Schema bootstrap finding.")]);
var indexer = new UnifiedSearchIndexer(
options,
store,
[adapter],
NullLogger<UnifiedSearchIndexer>.Instance);
var summary = await indexer.RebuildAllAsync(CancellationToken.None);
summary.DomainCount.Should().Be(1);
summary.ChunkCount.Should().Be(1);
await using var connection = new NpgsqlConnection(fixture.ConnectionString);
await connection.OpenAsync();
(await CountDomainChunksAsync(connection, "findings")).Should().Be(1);
}
[Fact]
public async Task UnifiedSearchIndexer_IndexAll_UpsertsOnlyChangedChunks_AndFindsNewFinding()
{
@@ -378,6 +408,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
var adapter = new MutableAdapter("findings", [unchangedChunk]);
var indexer = new UnifiedSearchIndexer(
options,
store,
[adapter],
NullLogger<UnifiedSearchIndexer>.Instance);
@@ -502,6 +533,7 @@ public sealed class UnifiedSearchLiveAdapterIntegrationTests
var adapter = new MutableAdapter("findings", [chunkTenantA]);
var indexer = new UnifiedSearchIndexer(
options,
store,
[adapter],
NullLogger<UnifiedSearchIndexer>.Instance);