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

@@ -2,11 +2,14 @@
## Mission
- Provide timeline core models and deterministic ordering logic.
- Own the `timeline.critical_path` materialized view and EF Core persistence layer for critical path analysis.
## Responsibilities
- Define timeline domain models and validation rules.
- Implement ordering and partitioning logic for timeline events.
- Keep serialization deterministic and invariant.
- Maintain EF Core DbContext, models, and compiled model for the `critical_path` materialized view.
- Provide `TimelineCoreDataSource` for connection management in the `timeline` schema.
## Required Reading
- docs/README.md
@@ -14,11 +17,28 @@
- docs/modules/platform/architecture-overview.md
- docs/modules/timeline-indexer/architecture.md
- docs/modules/timeline-indexer/README.md
- docs/db/EF_CORE_MODEL_GENERATION_STANDARDS.md
- docs/db/EF_CORE_RUNTIME_CUTOVER_STRATEGY.md
## Working Agreement
- Deterministic ordering and invariant formatting.
- Use TimeProvider and IGuidGenerator where timestamps or IDs are created.
- Propagate CancellationToken for async operations.
- EF Core repositories must use `AsNoTracking()` for reads and per-operation DbContext lifecycle.
- SQL migrations remain the authoritative schema definition; no EF Core auto-migrations.
## EF Core Structure
- DbContext: `EfCore/Context/TimelineCoreDbContext.cs`
- Design-time factory: `EfCore/Context/TimelineCoreDesignTimeDbContextFactory.cs`
- Entity models: `EfCore/Models/CriticalPathEntry.cs`
- Compiled models: `EfCore/CompiledModels/` (auto-generated, assembly attributes excluded from compilation)
- Runtime factory: `Postgres/TimelineCoreDbContextFactory.cs`
- DataSource: `Postgres/TimelineCoreDataSource.cs`
## Schema Ownership
- Schema: `timeline` (shared with TimelineIndexer)
- Migration: `Migrations/20260107_002_create_critical_path_view.sql`
- Migration registered as additional source in TimelineIndexer migration plugin (Platform migration registry)
## Testing Strategy
- Unit tests for ordering, validation, and serialization.

View File

@@ -0,0 +1,126 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.Timeline.Core.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Timeline.Core.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class CriticalPathEntryEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.Timeline.Core.EfCore.Models.CriticalPathEntry",
typeof(CriticalPathEntry),
baseEntityType,
propertyCount: 8,
namedIndexCount: 1,
keyCount: 1);
var correlationId = runtimeEntityType.AddProperty(
"CorrelationId",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("CorrelationId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<CorrelationId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
correlationId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
correlationId.AddAnnotation("Relational:ColumnName", "correlation_id");
var fromHlc = runtimeEntityType.AddProperty(
"FromHlc",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("FromHlc", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<FromHlc>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true,
afterSaveBehavior: PropertySaveBehavior.Throw);
fromHlc.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
fromHlc.AddAnnotation("Relational:ColumnName", "from_hlc");
var stage = runtimeEntityType.AddProperty(
"Stage",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("Stage", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<Stage>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
stage.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
stage.AddAnnotation("Relational:ColumnName", "stage");
var fromEventId = runtimeEntityType.AddProperty(
"FromEventId",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("FromEventId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<FromEventId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
fromEventId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
fromEventId.AddAnnotation("Relational:ColumnName", "from_event_id");
var toEventId = runtimeEntityType.AddProperty(
"ToEventId",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("ToEventId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<ToEventId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
toEventId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
toEventId.AddAnnotation("Relational:ColumnName", "to_event_id");
var toHlc = runtimeEntityType.AddProperty(
"ToHlc",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("ToHlc", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<ToHlc>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
toHlc.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
toHlc.AddAnnotation("Relational:ColumnName", "to_hlc");
var durationMs = runtimeEntityType.AddProperty(
"DurationMs",
typeof(double?),
propertyInfo: typeof(CriticalPathEntry).GetProperty("DurationMs", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<DurationMs>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
durationMs.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
durationMs.AddAnnotation("Relational:ColumnName", "duration_ms");
var service = runtimeEntityType.AddProperty(
"Service",
typeof(string),
propertyInfo: typeof(CriticalPathEntry).GetProperty("Service", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(CriticalPathEntry).GetField("<Service>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
service.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
service.AddAnnotation("Relational:ColumnName", "service");
var key = runtimeEntityType.AddKey(
new[] { correlationId, fromHlc });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "idx_critical_path_corr_from_hlc");
var idx_critical_path_duration = runtimeEntityType.AddIndex(
new[] { correlationId, durationMs },
name: "idx_critical_path_duration");
idx_critical_path_duration.AddAnnotation("Relational:IsDescending", new[] { false, true });
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", "timeline");
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "critical_path");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,9 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using StellaOps.Timeline.Core.EfCore.CompiledModels;
using StellaOps.Timeline.Core.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
[assembly: DbContextModel(typeof(TimelineCoreDbContext), typeof(TimelineCoreDbContextModel))]

View File

@@ -0,0 +1,48 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using StellaOps.Timeline.Core.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Timeline.Core.EfCore.CompiledModels
{
[DbContext(typeof(TimelineCoreDbContext))]
public partial class TimelineCoreDbContextModel : RuntimeModel
{
private static readonly bool _useOldBehavior31751 =
System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
static TimelineCoreDbContextModel()
{
var model = new TimelineCoreDbContextModel();
if (_useOldBehavior31751)
{
model.Initialize();
}
else
{
var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
thread.Start();
thread.Join();
void RunInitialization()
{
model.Initialize();
}
}
model.Customize();
_instance = (TimelineCoreDbContextModel)model.FinalizeModel();
}
private static TimelineCoreDbContextModel _instance;
public static IModel Instance => _instance;
partial void Initialize();
partial void Customize();
}
}

View File

@@ -0,0 +1,30 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Timeline.Core.EfCore.CompiledModels
{
public partial class TimelineCoreDbContextModel
{
private TimelineCoreDbContextModel()
: base(skipDetectChanges: false, modelId: new Guid("a2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"), entityTypeCount: 1)
{
}
partial void Initialize()
{
var criticalPathEntry = CriticalPathEntryEntityType.Create(this);
CriticalPathEntryEntityType.CreateAnnotations(criticalPathEntry);
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
AddAnnotation("ProductVersion", "10.0.0");
AddAnnotation("Relational:MaxIdentifierLength", 63);
}
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.EntityFrameworkCore;
using StellaOps.Timeline.Core.EfCore.Models;
namespace StellaOps.Timeline.Core.EfCore.Context;
/// <summary>
/// DbContext for Timeline Core module.
/// Covers the timeline.critical_path materialized view owned by this module.
/// </summary>
public partial class TimelineCoreDbContext : DbContext
{
private readonly string _schemaName;
public TimelineCoreDbContext(DbContextOptions<TimelineCoreDbContext> options, string? schemaName = null)
: base(options)
{
_schemaName = string.IsNullOrWhiteSpace(schemaName)
? "timeline"
: schemaName.Trim();
}
public virtual DbSet<CriticalPathEntry> CriticalPathEntries { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var schemaName = _schemaName;
modelBuilder.Entity<CriticalPathEntry>(entity =>
{
// The critical_path is a materialized view with a unique index on (correlation_id, from_hlc).
// EF Core requires a key; use the unique index columns as a composite key.
entity.HasKey(e => new { e.CorrelationId, e.FromHlc })
.HasName("idx_critical_path_corr_from_hlc");
entity.ToTable("critical_path", schemaName);
entity.HasIndex(e => new { e.CorrelationId, e.DurationMs }, "idx_critical_path_duration")
.IsDescending(false, true);
entity.Property(e => e.CorrelationId).HasColumnName("correlation_id");
entity.Property(e => e.Stage).HasColumnName("stage");
entity.Property(e => e.FromEventId).HasColumnName("from_event_id");
entity.Property(e => e.ToEventId).HasColumnName("to_event_id");
entity.Property(e => e.FromHlc).HasColumnName("from_hlc");
entity.Property(e => e.ToHlc).HasColumnName("to_hlc");
entity.Property(e => e.DurationMs).HasColumnName("duration_ms");
entity.Property(e => e.Service).HasColumnName("service");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace StellaOps.Timeline.Core.EfCore.Context;
/// <summary>
/// Design-time factory for dotnet ef CLI tooling.
/// </summary>
public sealed class TimelineCoreDesignTimeDbContextFactory : IDesignTimeDbContextFactory<TimelineCoreDbContext>
{
private const string DefaultConnectionString =
"Host=localhost;Port=55433;Database=postgres;Username=postgres;Password=postgres;Search Path=timeline,public";
private const string ConnectionStringEnvironmentVariable =
"STELLAOPS_TIMELINE_CORE_EF_CONNECTION";
public TimelineCoreDbContext CreateDbContext(string[] args)
{
var connectionString = ResolveConnectionString();
var options = new DbContextOptionsBuilder<TimelineCoreDbContext>()
.UseNpgsql(connectionString)
.Options;
return new TimelineCoreDbContext(options);
}
private static string ResolveConnectionString()
{
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
return string.IsNullOrWhiteSpace(fromEnvironment) ? DefaultConnectionString : fromEnvironment;
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
namespace StellaOps.Timeline.Core.EfCore.Models;
/// <summary>
/// Entity representing a row in the timeline.critical_path materialized view.
/// Scaffolded from migration 20260107_002_create_critical_path_view.sql.
/// </summary>
public partial class CriticalPathEntry
{
public string CorrelationId { get; set; } = null!;
public string Stage { get; set; } = null!;
public string? FromEventId { get; set; }
public string? ToEventId { get; set; }
public string? FromHlc { get; set; }
public string? ToHlc { get; set; }
public double? DurationMs { get; set; }
public string? Service { get; set; }
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Infrastructure.Postgres.Connections;
using StellaOps.Infrastructure.Postgres.Options;
namespace StellaOps.Timeline.Core.Postgres;
/// <summary>
/// PostgreSQL data source for the Timeline Core module.
/// Shares the "timeline" schema with TimelineIndexer but is independently configured.
/// </summary>
public sealed class TimelineCoreDataSource : DataSourceBase
{
/// <summary>
/// Default schema name for Timeline tables and views.
/// </summary>
public const string DefaultSchemaName = "timeline";
public TimelineCoreDataSource(IOptions<PostgresOptions> options, ILogger<TimelineCoreDataSource> logger)
: base(EnsureSchema(options.Value), logger)
{
}
/// <inheritdoc />
protected override string ModuleName => "TimelineCore";
private static PostgresOptions EnsureSchema(PostgresOptions baseOptions)
{
if (string.IsNullOrWhiteSpace(baseOptions.SchemaName))
{
baseOptions.SchemaName = DefaultSchemaName;
}
return baseOptions;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using Microsoft.EntityFrameworkCore;
using Npgsql;
using StellaOps.Timeline.Core.EfCore.CompiledModels;
using StellaOps.Timeline.Core.EfCore.Context;
namespace StellaOps.Timeline.Core.Postgres;
/// <summary>
/// Runtime factory for creating <see cref="TimelineCoreDbContext"/> instances.
/// Uses compiled model for the default schema path for fast startup.
/// </summary>
internal static class TimelineCoreDbContextFactory
{
public static TimelineCoreDbContext Create(NpgsqlConnection connection, int commandTimeoutSeconds, string schemaName)
{
var normalizedSchema = string.IsNullOrWhiteSpace(schemaName)
? TimelineCoreDataSource.DefaultSchemaName
: schemaName.Trim();
var optionsBuilder = new DbContextOptionsBuilder<TimelineCoreDbContext>()
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
if (string.Equals(normalizedSchema, TimelineCoreDataSource.DefaultSchemaName, StringComparison.Ordinal))
{
// Use the static compiled model when schema matches default for deterministic metadata initialization.
optionsBuilder.UseModel(TimelineCoreDbContextModel.Instance);
}
return new TimelineCoreDbContext(optionsBuilder.Options, normalizedSchema);
}
}

View File

@@ -9,18 +9,32 @@
<Description>StellaOps Timeline Core - Query and replay services</Description>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<!-- Prevent automatic compiled-model binding so non-default schemas can build runtime models. -->
<Compile Remove="EfCore\CompiledModels\TimelineCoreDbContextAssemblyAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Eventing\StellaOps.Eventing.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.HybridLogicalClock\StellaOps.HybridLogicalClock.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Npgsql" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,13 @@
# StellaOps.Timeline.Core Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
Source of truth: `docs/implplan/SPRINT_20260222_075_Timeline_core_dal_to_efcore.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| TCORE-EF-01 | DONE | AGENTS.md verified, migration registry wiring complete (TimelineIndexer multi-source). |
| TCORE-EF-02 | DONE | EF Core model scaffolded: DbContext, CriticalPathEntry entity, design-time factory. |
| TCORE-EF-03 | DONE | No direct Npgsql repositories to convert; Timeline Core delegates to ITimelineEventStore. EF Core DbContext available for future critical_path queries. |
| TCORE-EF-04 | DONE | Compiled model artifacts generated, assembly attributes excluded, runtime factory uses UseModel for default schema. |
| TCORE-EF-05 | DONE | Sequential build/test pass. Module docs updated. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Timeline/__Libraries/StellaOps.Timeline.Core/StellaOps.Timeline.Core.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |