chore(libs): infrastructure postgres host + attestation slicing + testkit

Shared infrastructure supporting the truthful runtime persistence cutover
sprints — no dedicated sprint owner, these libs are consumed by multiple
services.

- Infrastructure.Postgres: MigrationCategory + StartupMigrationHost +
  tests (MigrationExecution, Recording, Flags).
- AdvisoryAI.Attestation: slice AiAttestationService into partial files
  (Create/Read/Verify), align IAiAttestationStore + InMemory store,
  service tests.
- TestKit: ValkeyFixture for tests that need a shared valkey instance.
- Doctor/AdvisoryAI/IEvidenceSchemaRegistry: shared interface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-19 14:44:43 +03:00
parent 07cdba01cd
commit 87a5d2ee22
16 changed files with 230 additions and 142 deletions

View File

@@ -20,7 +20,7 @@ public enum MigrationCategory
Release,
/// <summary>
/// Seed data that is inserted once.
/// Optional seed/demo data that is inserted only when an operator runs it explicitly.
/// Prefix: S001-S999
/// </summary>
Seed,
@@ -83,10 +83,11 @@ public static class MigrationCategoryExtensions
/// Returns true if this migration should run automatically at startup.
/// </summary>
public static bool IsAutomatic(this MigrationCategory category) =>
category is MigrationCategory.Startup or MigrationCategory.Seed;
category is MigrationCategory.Startup;
/// <summary>
/// Returns true if this migration requires manual CLI execution.
/// Returns true if this migration requires manual CLI execution before startup may proceed.
/// Seed migrations are intentionally excluded because they are opt-in/manual only and must not block runtime.
/// </summary>
public static bool RequiresManualExecution(this MigrationCategory category) =>
category is MigrationCategory.Release or MigrationCategory.Data;

View File

@@ -15,8 +15,8 @@ namespace StellaOps.Infrastructure.Postgres.Migrations;
/// This service:
/// - Acquires an advisory lock to prevent concurrent migrations
/// - Validates checksums of already-applied migrations
/// - Blocks startup if pending release migrations exist
/// - Runs only Category A (startup) and seed migrations automatically
/// - Blocks startup if pending release/data migrations exist
/// - Runs only startup migrations automatically; seed migrations remain operator-invoked
/// </remarks>
public abstract class StartupMigrationHost : IHostedService
{
@@ -116,37 +116,64 @@ public abstract class StartupMigrationHost : IHostedService
}
}
// Step 5: Check for pending release migrations
var pendingRelease = allMigrations
// Step 5: Check for pending release/data migrations
var pendingManual = allMigrations
.Where(m => !appliedMigrations.ContainsKey(m.Name))
.Where(m => m.Category.RequiresManualExecution())
.ToList();
if (pendingRelease.Count > 0)
if (pendingManual.Count > 0)
{
_logger.LogError(
"Migration: {Count} pending release migration(s) require manual execution for {Module}:",
pendingRelease.Count, _moduleName);
"Migration: {Count} pending manual migration(s) require explicit execution before {Module} startup can converge:",
pendingManual.Count,
_moduleName);
foreach (var migration in pendingRelease)
foreach (var migration in pendingManual)
{
_logger.LogError(" - {Migration} (Category: {Category})", migration.Name, migration.Category);
}
_logger.LogError("Run: stellaops db migrate --module {Module} --category release", _moduleName);
foreach (var category in pendingManual
.Select(static migration => migration.Category)
.Distinct()
.OrderBy(static category => category.ToString(), StringComparer.Ordinal))
{
_logger.LogError(
"Run: stella system migrations-run --module {Module} --category {Category}",
_moduleName,
category.ToString().ToLowerInvariant());
}
if (_options.FailOnPendingReleaseMigrations)
{
_lifetime.StopApplication();
throw new InvalidOperationException(
$"Pending release migrations block startup for {_moduleName}. Run CLI migration first.");
$"Pending manual migrations block startup for {_moduleName}. Run CLI migration first.");
}
}
var pendingSeed = allMigrations
.Where(m => !appliedMigrations.ContainsKey(m.Name))
.Where(static m => m.Category == MigrationCategory.Seed)
.OrderBy(m => m.Name)
.ToList();
if (pendingSeed.Count > 0)
{
_logger.LogInformation(
"Migration: {Count} optional seed migration(s) are pending for {Module}. They remain manual-only and will not run at startup.",
pendingSeed.Count,
_moduleName);
_logger.LogInformation(
"Run manually when needed: stella system migrations-run --module {Module} --category seed",
_moduleName);
}
// Step 6: Execute pending startup migrations
var pendingStartup = allMigrations
.Where(m => !appliedMigrations.ContainsKey(m.Name))
.Where(m => m.Category.IsAutomatic())
.Where(static m => m.Category == MigrationCategory.Startup)
.OrderBy(m => m.Name)
.ToList();