From a859f2dd8ed25b46fc709e36c9b3a8733fe6a1e4 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Tue, 7 Jul 2026 23:44:07 -0500 Subject: [PATCH] feat(artifacts): make EvidenceRecord an Evidence Graph node (CR-14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes provenance/integrity to first-class fields (sourceProvider, producer, contentHash, classification [validated], retentionPolicyRef, immutable) and adds lineage edges references[] with a domain AddReference operation (non-empty, dedup) plus Seal — the adjacency that makes it a graph. contentHash is now a first-class field (CreateEvidenceRecordCommandHandler computes SHA-256 into it, keeping the legacy metadata copy). New POST /{id}/references command+endpoint; read model + DTO surface the 7 fields. Persistence: 7 new columns + classification index + additive migration AddEvidenceGraphFields (all NOT NULL with defaults / jsonb '[]', so existing rows stay valid). Build Release -warnaserror 0 warnings; 443→458 tests. Marks CR-14 DONE in the gap board + catalog. Co-Authored-By: Claude Opus 4.8 --- docs/audit/tracker-gap-reference-catalog.md | 2 +- docs/audit/tracker-gap-tracking.md | 2 +- .../AddEvidenceReferenceCommand.cs | 13 + .../AddEvidenceReferenceCommandHandler.cs | 41 + .../CreateEvidenceRecordCommand.cs | 7 +- .../CreateEvidenceRecordCommandHandler.cs | 15 +- .../EvidenceRecord/DTOs/EvidenceRecordDto.cs | 9 + .../GetEvidenceRecordQueryHandler.cs | 9 +- .../EvidenceRecord/EvidenceRecord.cs | 59 +- .../EvidenceRecord/EvidenceRecordProps.cs | 18 +- .../EvidenceRecord/EvidenceValues.cs | 18 + ...8044013_AddEvidenceGraphFields.Designer.cs | 2779 +++++++++++++++++ .../20260708044013_AddEvidenceGraphFields.cs | 120 + .../TrackerDbContextModelSnapshot.cs | 50 + .../EvidenceRecordRecordConfiguration.cs | 9 + .../Entities/EvidenceRecordRecord.cs | 9 + .../PostgreSqlEvidenceRecordRepository.cs | 41 +- .../Artifacts/EvidenceRecordEndpoints.cs | 45 +- ...AddEvidenceReferenceCommandHandlerTests.cs | 86 + .../EvidenceHashCommandHandlerTests.cs | 7 +- .../Domain/Artifacts/EvidenceRecordTests.cs | 110 + .../EvidenceGraphRoundTripTests.cs | 68 + 22 files changed, 3502 insertions(+), 15 deletions(-) create mode 100644 src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommand.cs create mode 100644 src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommandHandler.cs create mode 100644 src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceValues.cs create mode 100644 src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.Designer.cs create mode 100644 src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.cs create mode 100644 src/apps/tracker-api/Tracker.Tests/Application/Artifacts/AddEvidenceReferenceCommandHandlerTests.cs create mode 100644 src/apps/tracker-api/Tracker.Tests/Infrastructure/Persistence/EvidenceGraphRoundTripTests.cs diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index 8da000eb..a7de74ab 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -247,7 +247,7 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Acceptance criteria:** - [ ] Evidence is a first-class immutable entity with lineage/integrity, referenced by gate decisions. - **Dependencies:** [CR-13](#cr-13), [CR-15](#cr-15). - - **Status:** `IN-PROGRESS` — `EvidenceRecord` es un agregado persistido (Create/Verify/Reject + hash); falta el Evidence Graph (aristas de linaje + integridad/clasificación/retención). + - **Status:** `DONE` — `EvidenceRecord` como nodo de Evidence Graph: procedencia/integridad de primera clase (sourceProvider/producer/contentHash/classification/retentionPolicyRef/immutable), aristas `references[]` vía `AddReference` (dedup) + `Seal`, endpoint `POST /{id}/references`, migración aditiva `AddEvidenceGraphFields`; 458 tests. #### CR-15 diff --git a/docs/audit/tracker-gap-tracking.md b/docs/audit/tracker-gap-tracking.md index 822e76c6..0d20f7f8 100644 --- a/docs/audit/tracker-gap-tracking.md +++ b/docs/audit/tracker-gap-tracking.md @@ -27,7 +27,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`CR-21`](./tracker-gap-reference-catalog.md#cr-21) | Las 5 pantallas canónicas están ruteadas y accesibles (Gate Decision/Evaluation = `GateEvaluationMd3` ya existe — la nota "pendiente" estaba desactualizada); único faltante: un **browser de ADRs** dedicado (hoy los ADRs son solo tags en blueprints). | `WEB` | Cross | P1 | S | `IN-PROGRESS` | | [`GAP-010`](./tracker-gap-reference-catalog.md#gap-010) | **Hecho:** existe `reference/knowledge/domain/PRODUCT_VISION.md` (EN) con navegación bilingüe a `PRODUCT_VISION.es.md`. | `Docs` | Cross | P1 | M | `DONE` | | [`CR-10`](./tracker-gap-reference-catalog.md#cr-10) | Pantalla + endpoint de navegación de arquitectura (independiente del SDLC) existen para topologías/blueprints/evidencia/providers; faltan registro/browser de ADRs (estado/supersesión) y sync respaldado por Core (el catálogo hoy es estático). | `WEB/Backend` | Cross | P1 | M | `IN-PROGRESS` | -| [`CR-14`](./tracker-gap-reference-catalog.md#cr-14) | "evidencia = conteos enteros" desactualizado: `EvidenceRecord` es un agregado persistido (Create/Verify/Reject + hash). Falta el Evidence **Graph**: aristas de linaje/referencia y campos de integridad/clasificación/retención en el agregado (hoy solo en mock frontend + doc de diseño). | `Backend` | Cross | P1 | M | `IN-PROGRESS` | +| [`CR-14`](./tracker-gap-reference-catalog.md#cr-14) | **Hecho:** `EvidenceRecord` es ahora un nodo de Evidence **Graph** — campos de primera clase `sourceProvider`/`producer`/`contentHash`/`classification`(validada)/`retentionPolicyRef`/`immutable`, aristas de linaje `references[]` con operación de dominio `AddReference` (dedup) + `Seal`, comando/endpoint `POST /{id}/references`, migración aditiva `AddEvidenceGraphFields`. Build 0-warnings, 458 tests (+15). | `Backend` | Cross | P1 | M | `DONE` | | [`CR-15`](./tracker-gap-reference-catalog.md#cr-15) | Solo existe un display estático de Provider Connections (catálogo de referencia + panel UI). El agregado `ProviderConnection`, los ports por capacidad, el mapeo ACL y los adaptadores siguen **sin construir** en Domain/Application/Infrastructure. | `Backend/Integration` | Cross | P1 | M | `IN-PROGRESS` | | [`GAP-019`](./tracker-gap-reference-catalog.md#gap-019) | **Hecho:** requisitos Discovery desglosados a detalle implementable — PRD §2 REQ-DIS-01…13, cada uno con cláusula de aceptación, + catálogo de artefactos por gate (`functional-scope.md` §6). Sin user-stories atómicas, coherente con "sin backlog propio". | `Docs` | Cross | P1 | M | `DONE` | | [`CR-27`](./tracker-gap-reference-catalog.md#cr-27) | Phantom navigation: `aigov` + 4 nav items have no screen | `WEB` | Cross | P2 | S | `PENDING` | diff --git a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommand.cs b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommand.cs new file mode 100644 index 00000000..6f4b3f53 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommand.cs @@ -0,0 +1,13 @@ +using Tracker.Application.Abstractions.Messaging; + +namespace Tracker.Application.Artifacts.EvidenceRecord.Commands.AddEvidenceReference; + +/// +/// Adds a lineage edge from an evidence node to another Evidence Graph node +/// (evidence, gate decision, ADR, context, …). +/// +public sealed record AddEvidenceReferenceCommand( + Guid EvidenceId, + Guid TenantId, + string Reference +) : ICommand; diff --git a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommandHandler.cs b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommandHandler.cs new file mode 100644 index 00000000..42c52573 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/AddEvidenceReference/AddEvidenceReferenceCommandHandler.cs @@ -0,0 +1,41 @@ +using Tracker.Application.Abstractions.Messaging; +using Tracker.Domain.Kernel; + +namespace Tracker.Application.Artifacts.EvidenceRecord.Commands.AddEvidenceReference; + +internal sealed class AddEvidenceReferenceCommandHandler + : ICommandHandler +{ + private readonly Tracker.Domain.Artifacts.EvidenceRecord.IEvidenceRecordRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public AddEvidenceReferenceCommandHandler( + Tracker.Domain.Artifacts.EvidenceRecord.IEvidenceRecordRepository repository, + IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task Handle( + AddEvidenceReferenceCommand request, + CancellationToken cancellationToken) + { + var record = await _repository.GetByIdAsync(request.EvidenceId, cancellationToken); + + if (record is null) + return Result.Failure("EvidenceRecord.NotFound"); + + if (record.TenantId != request.TenantId) + return Result.Failure("EvidenceRecord.AccessDenied"); + + var result = record.AddReference(request.Reference); + if (result.IsFailure) + return Result.Failure(result.Error); + + await _repository.UpdateAsync(record, cancellationToken); + await _unitOfWork.SaveEntitiesAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommand.cs b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommand.cs index 56466769..02aa2893 100644 --- a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommand.cs +++ b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommand.cs @@ -8,5 +8,10 @@ public sealed record CreateEvidenceRecordCommand( string ArtifactId, string EvidenceType, string Content, - Guid SubmittedBy + Guid SubmittedBy, + string SourceProvider = "", + string Producer = "", + string Classification = Tracker.Domain.Artifacts.EvidenceRecord.EvidenceClassification.Internal, + string RetentionPolicyRef = "", + IReadOnlyList? References = null ) : ICommand; diff --git a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommandHandler.cs b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommandHandler.cs index 838e174c..4afed2dc 100644 --- a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommandHandler.cs +++ b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Commands/CreateEvidenceRecord/CreateEvidenceRecordCommandHandler.cs @@ -23,21 +23,30 @@ public async Task> Handle( CreateEvidenceRecordCommand request, CancellationToken cancellationToken) { + // Compute an integrity hash of the content so evidence is tamper-evident, then + // promote it to the first-class ContentHash node field of the Evidence Graph. + var contentSha256 = ComputeSha256Hex(request.Content); + var result = Tracker.Domain.Artifacts.EvidenceRecord.EvidenceRecord.Create( request.TenantId, request.ArtifactType, request.ArtifactId, request.EvidenceType, request.Content, - request.SubmittedBy); + request.SubmittedBy, + request.SourceProvider, + request.Producer, + contentSha256, + request.Classification, + request.RetentionPolicyRef, + request.References?.ToList()); if (result.IsFailure) return Result.Failure(result.Error); var evidence = result.Value; - // Carry an integrity hash of the content so evidence is tamper-evident. - var contentSha256 = ComputeSha256Hex(request.Content); + // Keep the legacy metadata copy for backward compatibility with existing readers. var metadata = new Dictionary(evidence.Metadata) { ["contentSha256"] = contentSha256 diff --git a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/DTOs/EvidenceRecordDto.cs b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/DTOs/EvidenceRecordDto.cs index 240385cf..b75deb2f 100644 --- a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/DTOs/EvidenceRecordDto.cs +++ b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/DTOs/EvidenceRecordDto.cs @@ -10,4 +10,13 @@ public sealed class EvidenceRecordDto public string Status { get; init; } = string.Empty; public Guid SubmittedBy { get; init; } public DateTime SubmittedAtUtc { get; init; } + + // Evidence Graph node: provenance / integrity / lineage. + public string SourceProvider { get; init; } = string.Empty; + public string Producer { get; init; } = string.Empty; + public string ContentHash { get; init; } = string.Empty; + public string Classification { get; init; } = string.Empty; + public string RetentionPolicyRef { get; init; } = string.Empty; + public IReadOnlyList References { get; init; } = new List(); + public bool Immutable { get; init; } } diff --git a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Queries/GetEvidenceRecord/GetEvidenceRecordQueryHandler.cs b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Queries/GetEvidenceRecord/GetEvidenceRecordQueryHandler.cs index 3ab128cd..d2ac3ea1 100644 --- a/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Queries/GetEvidenceRecord/GetEvidenceRecordQueryHandler.cs +++ b/src/apps/tracker-api/Tracker.Application/Artifacts/EvidenceRecord/Queries/GetEvidenceRecord/GetEvidenceRecordQueryHandler.cs @@ -32,7 +32,14 @@ public GetEvidenceRecordQueryHandler(Tracker.Domain.Artifacts.EvidenceRecord.IEv Content = record.Content, Status = record.Status, SubmittedBy = record.SubmittedBy, - SubmittedAtUtc = record.SubmittedAtUtc + SubmittedAtUtc = record.SubmittedAtUtc, + SourceProvider = record.SourceProvider, + Producer = record.Producer, + ContentHash = record.ContentHash, + Classification = record.Classification, + RetentionPolicyRef = record.RetentionPolicyRef, + References = record.References, + Immutable = record.Immutable }; } } diff --git a/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecord.cs b/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecord.cs index e68cf5d8..093f6ab8 100644 --- a/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecord.cs +++ b/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecord.cs @@ -17,14 +17,31 @@ private EvidenceRecord(EvidenceRecordProps props) : base(props) { } public DateTime SubmittedAtUtc => Props.SubmittedAtUtc; public Dictionary Metadata => Props.Metadata; + public string SourceProvider => Props.SourceProvider; + public string Producer => Props.Producer; + public string ContentHash => Props.ContentHash; + public string Classification => Props.Classification; + public string RetentionPolicyRef => Props.RetentionPolicyRef; + public IReadOnlyList References => Props.References; + public bool Immutable => Props.Immutable; + public static Result Create( Guid tenantId, string artifactType, string artifactId, string evidenceType, string content, - Guid submittedBy) + Guid submittedBy, + string sourceProvider = "", + string producer = "", + string contentHash = "", + string classification = EvidenceClassification.Internal, + string retentionPolicyRef = "", + List? references = null) { + if (!EvidenceClassification.IsValid(classification)) + return Result.Failure("EvidenceRecord.InvalidClassification"); + var props = new EvidenceRecordProps { Id = TrackerId.Create(), @@ -37,12 +54,52 @@ public static Result Create( SubmittedBy = submittedBy, SubmittedAtUtc = DateTime.UtcNow, Metadata = new(), + SourceProvider = sourceProvider, + Producer = producer, + ContentHash = contentHash, + Classification = classification, + RetentionPolicyRef = retentionPolicyRef, + References = references is null ? new() : new List(references), + Immutable = false, Audit = new AuditProps(submittedBy.ToString(), DateTime.UtcNow) }; return Result.Success(new EvidenceRecord(props)); } + /// + /// Adds a lineage edge from this evidence node to another graph node (evidence, + /// gate decision, ADR, context, …). Idempotent: adding a reference that is already + /// present is a no-op success, so the adjacency list stays a set of distinct edges. + /// + public Result AddReference(string reference) + { + if (string.IsNullOrWhiteSpace(reference)) + return Result.Failure("EvidenceRecord.EmptyReference"); + + if (Props.References.Contains(reference)) + return Result.Success(); + + var references = new List(Props.References) { reference }; + SetProps(Props with { References = references }); + TrackingState.MarkAsDirty(); + return Result.Success(); + } + + /// + /// Seals the evidence node: once sealed the content and integrity hash are frozen by + /// convention. Idempotent — sealing an already-sealed node is a no-op success. + /// + public Result Seal() + { + if (Props.Immutable) + return Result.Success(); + + SetProps(Props with { Immutable = true }); + TrackingState.MarkAsDirty(); + return Result.Success(); + } + public Result Verify(Guid verifiedBy) { if (Props.Status != "submitted") diff --git a/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecordProps.cs b/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecordProps.cs index 176f9d02..3ab211b9 100644 --- a/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecordProps.cs +++ b/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceRecordProps.cs @@ -16,7 +16,21 @@ public sealed record EvidenceRecordProps : IProps public DateTime SubmittedAtUtc { get; init; } public Dictionary Metadata { get; init; } = new(); + // Evidence Graph: provenance / integrity fields promoting the record to a first-class node. + public string SourceProvider { get; init; } = string.Empty; + public string Producer { get; init; } = string.Empty; + public string ContentHash { get; init; } = string.Empty; + public string Classification { get; init; } = string.Empty; + public string RetentionPolicyRef { get; init; } = string.Empty; + + // Evidence Graph: lineage edges (adjacency list) to other evidence, gate decisions, ADRs, contexts. + public List References { get; init; } = new(); + + // Evidence Graph: integrity seal — once true the content/hash are frozen by convention. + public bool Immutable { get; init; } + /// Copy hook required by the Core shared-kernel IProps : ICloneable. Explicit - /// interface impl because records cannot declare a member literally named Clone (CS8859). - object ICloneable.Clone() => this with { }; + /// interface impl because records cannot declare a member literally named Clone (CS8859). + /// The list is copied so clones never share the mutable backing store. + object ICloneable.Clone() => this with { References = new List(References) }; } diff --git a/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceValues.cs b/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceValues.cs new file mode 100644 index 00000000..2b2b1451 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Domain/Artifacts/EvidenceRecord/EvidenceValues.cs @@ -0,0 +1,18 @@ +namespace Tracker.Domain.Artifacts.EvidenceRecord; + +/// +/// Canonical, Tracker-owned evidence classification levels. This is the authoritative +/// data-sensitivity taxonomy for an node in the Evidence Graph, +/// governing how provenance/integrity of the artifact is disclosed and retained. +/// +public static class EvidenceClassification +{ + public const string Public = "public"; + public const string Internal = "internal"; + public const string Confidential = "confidential"; + public const string Restricted = "restricted"; + + /// True if is one of the canonical classification levels. + public static bool IsValid(string classification) + => classification is Public or Internal or Confidential or Restricted; +} diff --git a/src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.Designer.cs b/src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.Designer.cs new file mode 100644 index 00000000..3ba2c789 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.Designer.cs @@ -0,0 +1,2779 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tracker.Infrastructure.Persistence; + +#nullable disable + +namespace Tracker.Infrastructure.Migrations +{ + [DbContext(typeof(TrackerDbContext))] + [Migration("20260708044013_AddEvidenceGraphFields")] + partial class AddEvidenceGraphFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("tracker_governance") + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "ltree"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Artifacts.Entities.EvidenceRecordRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ArtifactId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("artifact_id"); + + b.Property("ArtifactType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("artifact_type"); + + b.Property("Classification") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("classification"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("ContentHash") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("content_hash"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("EvidenceType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("evidence_type"); + + b.Property("Immutable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("immutable"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("metadata"); + + b.Property("Producer") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("producer"); + + b.Property("ReferencesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("references"); + + b.Property("RetentionPolicyRef") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("retention_policy_ref"); + + b.Property("SourceProvider") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("source_provider"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("SubmittedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted_at"); + + b.Property("SubmittedBy") + .HasColumnType("uuid") + .HasColumnName("submitted_by"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Classification"); + + b.HasIndex("TenantId"); + + b.HasIndex("ArtifactType", "ArtifactId"); + + b.ToTable("evidence_records", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Audit.Entities.AuditEntryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("action"); + + b.Property("ActorId") + .HasColumnType("uuid") + .HasColumnName("actor_id"); + + b.Property("ChangesJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("changes"); + + b.Property("CorrelationId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("correlation_id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("entity_id"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("entity_type"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimestampUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("timestamp"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("TenantId"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("audit_entries", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Design.Entities.TechnicalBlueprintRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("FacetsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("facets"); + + b.Property("InitiativeId") + .HasColumnType("uuid") + .HasColumnName("initiative_id"); + + b.Property("InitiativeType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("initiative_type"); + + b.Property("IntegrationAdrRef") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("integration_adr_ref"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Notes") + .HasColumnType("text") + .HasColumnName("notes"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("InitiativeId") + .IsUnique(); + + b.HasIndex("TenantId"); + + b.ToTable("technical_blueprints", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Discovery.Entities.InitiativeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApprovalComplete") + .HasColumnType("boolean") + .HasColumnName("approval_complete"); + + b.Property("ApprovedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("approved_at"); + + b.Property("ApprovedBy") + .HasColumnType("uuid") + .HasColumnName("approved_by"); + + b.Property("BusinessCaseJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("business_case"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("code"); + + b.Property("ConstituentsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("constituents"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FacetsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("facets"); + + b.Property("FunctionalBriefJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("functional_brief"); + + b.Property("InitiativeType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("initiative_type"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("metadata"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Origin") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("origin"); + + b.Property("OriginRef") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("origin_ref"); + + b.Property("ProductScopeJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("product_scope"); + + b.Property("RejectionReason") + .HasColumnType("text") + .HasColumnName("rejection_reason"); + + b.Property("RequiredApproversJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("required_approvers"); + + b.Property("StakeholdersJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("stakeholders"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("SupersededByInitiativeId") + .HasColumnType("uuid") + .HasColumnName("superseded_by_initiative_id"); + + b.Property("TechnicalBriefJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("technical_brief"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TypeFrozen") + .HasColumnType("boolean") + .HasColumnName("type_frozen"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code"); + + b.ToTable("initiatives", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Discovery.Entities.OpportunityRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("code"); + + b.Property("ConsolidatedInitiativeId") + .HasColumnType("uuid") + .HasColumnName("consolidated_initiative_id"); + + b.Property("ContextJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("{}") + .HasColumnName("context"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CustomFieldsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("{}") + .HasColumnName("custom_fields"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("metadata"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("priority"); + + b.Property("PromotedInitiativeId") + .HasColumnType("uuid") + .HasColumnName("promoted_initiative_id"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("title"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("opportunities", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.CatalogReleaseRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("code"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("PublishedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("source"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("catalog_release", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.CountryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DefaultCurrencyCode") + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("default_currency_code"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsoAlpha2") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character(2)") + .HasColumnName("iso_alpha2") + .IsFixedLength(); + + b.Property("IsoAlpha3") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("iso_alpha3") + .IsFixedLength(); + + b.Property("Name") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("name"); + + b.Property("NumericCode") + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("numeric_code") + .IsFixedLength(); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("IsoAlpha2") + .IsUnique(); + + b.HasIndex("IsoAlpha3") + .IsUnique(); + + b.ToTable("country", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.CurrencyRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("code"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("MinorUnits") + .HasColumnType("smallint") + .HasColumnName("minor_units"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("symbol"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("currency", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.ExchangeRateRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BaseCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("base_code"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("QuoteCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("quote_code"); + + b.Property("Rate") + .HasColumnType("numeric(20,10)") + .HasColumnName("rate"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("source"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.Property("ValidFrom") + .HasColumnType("date") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("date") + .HasColumnName("valid_to"); + + b.HasKey("Id"); + + b.HasIndex("BaseCode", "QuoteCode", "ValidFrom") + .HasDatabaseName("ix_exchange_rate_pair"); + + b.ToTable("exchange_rate", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LanguageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("code"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("language", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocaleProfileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("code"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DateFormat") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("date_format"); + + b.Property("FirstDayOfWeek") + .HasColumnType("smallint") + .HasColumnName("first_day_of_week"); + + b.Property("Name") + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("name"); + + b.Property("NumberFormat") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("number_format"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CountryId"); + + b.ToTable("locale_profile", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationCodeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LocationId") + .HasColumnType("uuid") + .HasColumnName("location_id"); + + b.Property("Scheme") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("scheme"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("LocationId", "Scheme") + .IsUnique(); + + b.HasIndex("Scheme", "Value") + .IsUnique(); + + b.ToTable("location_code", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationI18nRecord", b => + { + b.Property("LocationId") + .HasColumnType("uuid") + .HasColumnName("location_id"); + + b.Property("Locale") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("locale"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("name"); + + b.HasKey("LocationId", "Locale"); + + b.ToTable("location_i18n", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationLevelRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("code"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Ordinal") + .HasColumnType("smallint") + .HasColumnName("ordinal"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("CountryId", "Ordinal") + .IsUnique(); + + b.ToTable("location_level", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CatalogReleaseId") + .HasColumnType("uuid") + .HasColumnName("catalog_release_id"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("LevelId") + .HasColumnType("uuid") + .HasColumnName("level_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("uuid") + .HasColumnName("parent_id"); + + b.Property("Path") + .IsRequired() + .HasColumnType("ltree") + .HasColumnName("path"); + + b.Property("SupersedesId") + .HasColumnType("uuid") + .HasColumnName("supersedes_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.Property("ValidFrom") + .HasColumnType("date") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("date") + .HasColumnName("valid_to"); + + b.HasKey("Id"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_location_parent"); + + b.HasIndex("Path") + .HasDatabaseName("ix_location_path"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Path"), "gist"); + + b.HasIndex("CountryId", "ParentId") + .HasDatabaseName("ix_location_country_parent"); + + b.ToTable("location", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.TaxRateRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("LocationId") + .HasColumnType("uuid") + .HasColumnName("location_id"); + + b.Property("OperationType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation_type"); + + b.Property("Rate") + .HasColumnType("numeric(6,4)") + .HasColumnName("rate"); + + b.Property("TaxSchemeId") + .HasColumnType("uuid") + .HasColumnName("tax_scheme_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.Property("ValidFrom") + .HasColumnType("date") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("date") + .HasColumnName("valid_to"); + + b.HasKey("Id"); + + b.HasIndex("TaxSchemeId", "LocationId", "ValidFrom") + .HasDatabaseName("ix_tax_rate_scheme_location"); + + b.ToTable("tax_rate", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.TaxSchemeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("code"); + + b.Property("CountryId") + .HasColumnType("uuid") + .HasColumnName("country_id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("name"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("CountryId", "Code") + .IsUnique(); + + b.ToTable("tax_scheme", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.TimeZoneRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CountryCode") + .HasMaxLength(2) + .HasColumnType("character varying(2)") + .HasColumnName("country_code"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IanaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("iana_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("CountryCode"); + + b.HasIndex("IanaId") + .IsUnique(); + + b.ToTable("time_zone", "geo"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Governance.Entities.ArtifactFieldSchemaRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ArtifactType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("artifact_type"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CustomFieldsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("custom_fields"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ArtifactType") + .IsUnique(); + + b.ToTable("artifact_field_schemas", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Governance.Entities.GateDecisionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApprovalIdsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("approval_ids"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DecidedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("decided_at"); + + b.Property("DecisionBy") + .HasColumnType("uuid") + .HasColumnName("decision_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("EvaluationIdsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("evaluation_ids"); + + b.Property("EvidenceSnapshotRef") + .HasColumnType("text") + .HasColumnName("evidence_snapshot_ref"); + + b.Property("ExceptionIdsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("exception_ids"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Phase") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("phase"); + + b.Property("PolicySnapshotRef") + .HasColumnType("text") + .HasColumnName("policy_snapshot_ref"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rationale"); + + b.Property("SdlcExecutionId") + .HasColumnType("uuid") + .HasColumnName("sdlc_execution_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Phase"); + + b.HasIndex("SdlcExecutionId"); + + b.HasIndex("TenantId"); + + b.ToTable("gate_decisions", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Governance.Entities.GatePolicyRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("ApprovalPolicyJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("approval_policy"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("EvaluationCriteriaJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("evaluation_criteria"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("mode"); + + b.Property("Phase") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phase"); + + b.Property("RequiredEvidenceJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("required_evidence"); + + b.Property("RequiresCoreVerdict") + .HasColumnType("boolean") + .HasColumnName("requires_core_verdict"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Phase") + .IsUnique(); + + b.ToTable("gate_policies", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Governance.Entities.GateSubmissionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApprovalsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("approvals"); + + b.Property("CoreVerdictJson") + .HasColumnType("jsonb") + .HasColumnName("core_verdict"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CriteriaEvaluationJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("criteria_evaluation"); + + b.Property("DecidedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("decided_at"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("EvidenceJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("evidence"); + + b.Property("InitiativeId") + .HasColumnType("uuid") + .HasColumnName("initiative_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("mode"); + + b.Property("Phase") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phase"); + + b.Property("RequestedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("requested_at"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("requested_by"); + + b.Property("RequiresCoreVerdict") + .HasColumnType("boolean") + .HasColumnName("requires_core_verdict"); + + b.Property("State") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("state"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("InitiativeId"); + + b.HasIndex("TenantId"); + + b.HasIndex("InitiativeId", "Phase"); + + b.ToTable("gate_submissions", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Governance.Entities.PhaseGateStateRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("EvaluatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("evaluated_at"); + + b.Property("EvaluatedBy") + .HasColumnType("uuid") + .HasColumnName("evaluated_by"); + + b.Property("GateStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gate_status"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Phase") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phase"); + + b.Property("SdlcExecutionId") + .HasColumnType("uuid") + .HasColumnName("sdlc_execution_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Phase"); + + b.HasIndex("TenantId", "SdlcExecutionId"); + + b.ToTable("phase_gate_states", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Governance.Entities.SDLCExecutionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BlockedReason") + .HasColumnType("text") + .HasColumnName("blocked_reason"); + + b.Property("CompletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("current_phase"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ProductCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("product_code"); + + b.Property("StartedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("ProductCode"); + + b.HasIndex("TenantId"); + + b.ToTable("sdlc_executions", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Intake.Entities.IntakeDeadLetterRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AggregateId") + .HasColumnType("uuid") + .HasColumnName("aggregate_id"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeadLetteredAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("dead_lettered_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("event_type"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("external_id"); + + b.Property("LastError") + .HasColumnType("text") + .HasColumnName("last_error"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("intake_dead_letter", "tracker_intake"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Intake.Entities.IntakeOutboxRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AggregateId") + .HasColumnType("uuid") + .HasColumnName("aggregate_id"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DispatchedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("dispatched_at"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("event_type"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("external_id"); + + b.Property("NextAttemptAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("next_attempt_at"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("Status", "NextAttemptAtUtc"); + + b.ToTable("intake_outbox", "tracker_intake"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Intake.Entities.PpmEndpointRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AuthType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("none") + .HasColumnName("auth_type"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("FieldBindingsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("field_bindings"); + + b.Property("HealthState") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("health_state"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastSyncUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sync_at"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("provider"); + + b.Property("SecretRef") + .HasColumnType("text") + .HasColumnName("secret_ref"); + + b.Property("SourceSystem") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_system"); + + b.Property("TelemetryUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("telemetry_url"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ppm_endpoints", "tracker_intake"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Intake.Entities.StrategicBaselineRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AnchoredAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("anchored_at"); + + b.Property("Budget") + .IsRequired() + .HasColumnType("text") + .HasColumnName("budget"); + + b.Property("ExpectedRoi") + .IsRequired() + .HasColumnType("text") + .HasColumnName("expected_roi"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("external_id"); + + b.Property("RiskLevel") + .IsRequired() + .HasColumnType("text") + .HasColumnName("risk_level"); + + b.Property("StrategicInitiativeId") + .HasColumnType("uuid") + .HasColumnName("strategic_initiative_id"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("StrategicInitiativeId"); + + b.HasIndex("StrategicInitiativeId", "Version") + .IsUnique(); + + b.ToTable("strategic_baselines", "tracker_intake"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Intake.Entities.StrategicInitiativeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BusinessCaseJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("business_case"); + + b.Property("ConsolidatedInitiativeId") + .HasColumnType("uuid") + .HasColumnName("consolidated_initiative_id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("external_id"); + + b.Property("FeasibilityJson") + .HasColumnType("jsonb") + .HasColumnName("feasibility"); + + b.Property("FunctionalBriefJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("functional_brief"); + + b.Property("IntakeState") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("intake_state"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("metadata"); + + b.Property("ProductCode") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("product_code"); + + b.Property("ProductId") + .HasColumnType("uuid") + .HasColumnName("product_id"); + + b.Property("ProductName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("product_name"); + + b.Property("PromotedInitiativeId") + .HasColumnType("uuid") + .HasColumnName("promoted_initiative_id"); + + b.Property("ReceivedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("received_at"); + + b.Property("SourceSystem") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("source_system"); + + b.Property("StakeholdersJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("stakeholders"); + + b.Property("TechnicalBriefJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("technical_brief"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("title"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("IntakeState"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "ExternalId") + .IsUnique(); + + b.ToTable("strategic_initiatives", "tracker_intake"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Integration.Entities.CoreEvaluationTransactionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CompletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("ErrorCode") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("error_code"); + + b.Property("ErrorMessage") + .HasColumnType("text") + .HasColumnName("error_message"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("OperationId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("operation_id"); + + b.Property("RepositoryRevision") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("repository_revision"); + + b.Property("RepositoryUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("repository_url"); + + b.Property("RequestedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("requested_at"); + + b.Property("ResponseDataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("response_data"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("OperationId") + .IsUnique(); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("core_evaluation_transactions", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Products.Entities.ProductRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("code"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MetadataJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("metadata"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("OwnerIdentity") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("owner_identity"); + + b.Property("RepositoryAccessJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("{}") + .HasColumnName("repository_access"); + + b.Property("RepositoryUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("repository_url"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Topology") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("topology"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("products", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Sdlc.Entities.PhaseArtifactRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("InitiativeId") + .HasColumnType("uuid") + .HasColumnName("initiative_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ItemsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("items"); + + b.Property("Phase") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("phase"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("InitiativeId", "Phase") + .IsUnique(); + + b.ToTable("phase_artifacts", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Sdlc.Entities.PhaseProgressionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CurrentPhase") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("current_phase"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("HistoryJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("history"); + + b.Property("InitiativeId") + .HasColumnType("uuid") + .HasColumnName("initiative_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("InitiativeId") + .IsUnique(); + + b.HasIndex("TenantId"); + + b.ToTable("phase_progressions", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Tenancy.Entities.TenantIntelligenceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AgentRegistrationsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("agent_registrations"); + + b.Property("CorpusRefsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("corpus_refs"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ModelRoutingJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("model_routing"); + + b.Property("SettingsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("settings"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("TenantId") + .IsUnique(); + + b.ToTable("tenant_intelligence", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Tenancy.Entities.TenantRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("code"); + + b.Property("ContactEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("contact_email"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("text") + .HasColumnName("deleted_by"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("display_name"); + + b.Property("GeoCatalogRelease") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("geo_catalog_release"); + + b.Property("GeoCountryId") + .HasColumnType("uuid") + .HasColumnName("geo_country_id"); + + b.Property("GeoDisplayPath") + .HasColumnType("text") + .HasColumnName("geo_display_path"); + + b.Property("GeoUbigeo") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("geo_ubigeo"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LocalizationJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("{}") + .HasColumnName("localization"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("SettingsJson") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("settings"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("Tier") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("tier"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("tenants", "tracker_governance"); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationCodeRecord", b => + { + b.HasOne("Tracker.Infrastructure.Persistence.Geo.Entities.LocationRecord", null) + .WithMany("Codes") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationI18nRecord", b => + { + b.HasOne("Tracker.Infrastructure.Persistence.Geo.Entities.LocationRecord", null) + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Tracker.Infrastructure.Persistence.Geo.Entities.LocationRecord", b => + { + b.Navigation("Codes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.cs b/src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.cs new file mode 100644 index 00000000..8ba281fa --- /dev/null +++ b/src/apps/tracker-api/Tracker.Infrastructure/Migrations/20260708044013_AddEvidenceGraphFields.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tracker.Infrastructure.Migrations +{ + /// + public partial class AddEvidenceGraphFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "classification", + schema: "tracker_governance", + table: "evidence_records", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "content_hash", + schema: "tracker_governance", + table: "evidence_records", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "immutable", + schema: "tracker_governance", + table: "evidence_records", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "producer", + schema: "tracker_governance", + table: "evidence_records", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "references", + schema: "tracker_governance", + table: "evidence_records", + type: "jsonb", + nullable: false, + defaultValue: "[]"); + + migrationBuilder.AddColumn( + name: "retention_policy_ref", + schema: "tracker_governance", + table: "evidence_records", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "source_provider", + schema: "tracker_governance", + table: "evidence_records", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_evidence_records_classification", + schema: "tracker_governance", + table: "evidence_records", + column: "classification"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_evidence_records_classification", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "classification", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "content_hash", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "immutable", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "producer", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "references", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "retention_policy_ref", + schema: "tracker_governance", + table: "evidence_records"); + + migrationBuilder.DropColumn( + name: "source_provider", + schema: "tracker_governance", + table: "evidence_records"); + } + } +} diff --git a/src/apps/tracker-api/Tracker.Infrastructure/Migrations/TrackerDbContextModelSnapshot.cs b/src/apps/tracker-api/Tracker.Infrastructure/Migrations/TrackerDbContextModelSnapshot.cs index 905e09d8..5d544d92 100644 --- a/src/apps/tracker-api/Tracker.Infrastructure/Migrations/TrackerDbContextModelSnapshot.cs +++ b/src/apps/tracker-api/Tracker.Infrastructure/Migrations/TrackerDbContextModelSnapshot.cs @@ -43,11 +43,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(128)") .HasColumnName("artifact_type"); + b.Property("Classification") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("classification"); + b.Property("Content") .IsRequired() .HasColumnType("text") .HasColumnName("content"); + b.Property("ContentHash") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("content_hash"); + b.Property("CreatedAtUtc") .HasColumnType("timestamp with time zone") .HasColumnName("created_at"); @@ -71,6 +85,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(128)") .HasColumnName("evidence_type"); + b.Property("Immutable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("immutable"); + b.Property("IsDeleted") .HasColumnType("boolean") .HasColumnName("is_deleted"); @@ -80,6 +100,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("jsonb") .HasColumnName("metadata"); + b.Property("Producer") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("producer"); + + b.Property("ReferencesJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValue("[]") + .HasColumnName("references"); + + b.Property("RetentionPolicyRef") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("retention_policy_ref"); + + b.Property("SourceProvider") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("") + .HasColumnName("source_provider"); + b.Property("Status") .IsRequired() .HasMaxLength(20) @@ -109,6 +157,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("Classification"); + b.HasIndex("TenantId"); b.HasIndex("ArtifactType", "ArtifactId"); diff --git a/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Configurations/EvidenceRecordRecordConfiguration.cs b/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Configurations/EvidenceRecordRecordConfiguration.cs index 51ac4b78..d3733548 100644 --- a/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Configurations/EvidenceRecordRecordConfiguration.cs +++ b/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Configurations/EvidenceRecordRecordConfiguration.cs @@ -24,6 +24,14 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.SubmittedAtUtc).HasColumnName("submitted_at").IsRequired(); builder.Property(x => x.MetadataJson).HasColumnName("metadata").HasColumnType("jsonb"); + builder.Property(x => x.SourceProvider).HasColumnName("source_provider").HasColumnType("text").HasDefaultValue(""); + builder.Property(x => x.Producer).HasColumnName("producer").HasColumnType("text").HasDefaultValue(""); + builder.Property(x => x.ContentHash).HasColumnName("content_hash").HasColumnType("text").HasDefaultValue(""); + builder.Property(x => x.Classification).HasColumnName("classification").HasColumnType("text").HasDefaultValue(""); + builder.Property(x => x.RetentionPolicyRef).HasColumnName("retention_policy_ref").HasColumnType("text").HasDefaultValue(""); + builder.Property(x => x.ReferencesJson).HasColumnName("references").HasColumnType("jsonb").HasDefaultValue("[]"); + builder.Property(x => x.Immutable).HasColumnName("immutable").HasDefaultValue(false); + builder.Property(x => x.CreatedBy).HasColumnName("created_by").IsRequired(); builder.Property(x => x.CreatedAtUtc).HasColumnName("created_at").IsRequired(); builder.Property(x => x.UpdatedBy).HasColumnName("updated_by").IsRequired(); @@ -36,5 +44,6 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.ArtifactType, x.ArtifactId }); + builder.HasIndex(x => x.Classification); } } diff --git a/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Entities/EvidenceRecordRecord.cs b/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Entities/EvidenceRecordRecord.cs index 88b561dc..24b2b99c 100644 --- a/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Entities/EvidenceRecordRecord.cs +++ b/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/Entities/EvidenceRecordRecord.cs @@ -17,6 +17,15 @@ public sealed class EvidenceRecordRecord : IAuditableRecord public DateTime SubmittedAtUtc { get; set; } public string MetadataJson { get; set; } = "{}"; + // Evidence Graph: provenance / integrity / lineage node fields. + public string SourceProvider { get; set; } = string.Empty; + public string Producer { get; set; } = string.Empty; + public string ContentHash { get; set; } = string.Empty; + public string Classification { get; set; } = string.Empty; + public string RetentionPolicyRef { get; set; } = string.Empty; + public string ReferencesJson { get; set; } = "[]"; + public bool Immutable { get; set; } + public string CreatedBy { get; set; } = string.Empty; public DateTime CreatedAtUtc { get; set; } public string UpdatedBy { get; set; } = string.Empty; diff --git a/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/PostgreSqlEvidenceRecordRepository.cs b/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/PostgreSqlEvidenceRecordRepository.cs index 13bd4200..98c12470 100644 --- a/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/PostgreSqlEvidenceRecordRepository.cs +++ b/src/apps/tracker-api/Tracker.Infrastructure/Persistence/Artifacts/PostgreSqlEvidenceRecordRepository.cs @@ -63,6 +63,13 @@ public async Task UpdateAsync(EvidenceRecord aggregate, CancellationToken cancel record.Content = aggregate.Content; record.Status = aggregate.Status; record.MetadataJson = JsonSerializer.Serialize(aggregate.Metadata, JsonOptions); + record.SourceProvider = aggregate.SourceProvider; + record.Producer = aggregate.Producer; + record.ContentHash = aggregate.ContentHash; + record.Classification = aggregate.Classification; + record.RetentionPolicyRef = aggregate.RetentionPolicyRef; + record.ReferencesJson = JsonSerializer.Serialize(aggregate.References, JsonOptions); + record.Immutable = aggregate.Immutable; record.UpdatedBy = aggregate.Props.Audit.UpdatedBy; record.UpdatedAtUtc = aggregate.Props.Audit.UpdatedAtUtc; } @@ -92,7 +99,14 @@ private static EvidenceRecord MapToDomain(EvidenceRecordRecord record) Status = record.Status, SubmittedBy = record.SubmittedBy, SubmittedAtUtc = record.SubmittedAtUtc, - Metadata = DeserializeMetadata(record.MetadataJson) + Metadata = DeserializeMetadata(record.MetadataJson), + SourceProvider = record.SourceProvider, + Producer = record.Producer, + ContentHash = record.ContentHash, + Classification = record.Classification, + RetentionPolicyRef = record.RetentionPolicyRef, + References = DeserializeStringList(record.ReferencesJson), + Immutable = record.Immutable }; var result = EvidenceRecord.Create( @@ -122,6 +136,13 @@ private static EvidenceRecordRecord MapToRecord(EvidenceRecord aggregate) SubmittedBy = aggregate.SubmittedBy, SubmittedAtUtc = aggregate.SubmittedAtUtc, MetadataJson = JsonSerializer.Serialize(aggregate.Metadata, JsonOptions), + SourceProvider = aggregate.SourceProvider, + Producer = aggregate.Producer, + ContentHash = aggregate.ContentHash, + Classification = aggregate.Classification, + RetentionPolicyRef = aggregate.RetentionPolicyRef, + ReferencesJson = JsonSerializer.Serialize(aggregate.References, JsonOptions), + Immutable = aggregate.Immutable, CreatedBy = aggregate.Props.Audit.CreatedBy, CreatedAtUtc = aggregate.Props.Audit.CreatedAtUtc, UpdatedBy = aggregate.Props.Audit.UpdatedBy, @@ -129,6 +150,24 @@ private static EvidenceRecordRecord MapToRecord(EvidenceRecord aggregate) }; } + private static List DeserializeStringList(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + + try + { + return JsonSerializer.Deserialize>(json, JsonOptions) + ?? new List(); + } + catch (JsonException) + { + return new List(); + } + } + private static Dictionary DeserializeMetadata(string json) { if (string.IsNullOrWhiteSpace(json)) diff --git a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Artifacts/EvidenceRecordEndpoints.cs b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Artifacts/EvidenceRecordEndpoints.cs index 139d630a..be9577d2 100644 --- a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Artifacts/EvidenceRecordEndpoints.cs +++ b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Artifacts/EvidenceRecordEndpoints.cs @@ -1,4 +1,5 @@ using MediatR; +using Tracker.Application.Artifacts.EvidenceRecord.Commands.AddEvidenceReference; using Tracker.Application.Artifacts.EvidenceRecord.Commands.CreateEvidenceRecord; using Tracker.Application.Artifacts.EvidenceRecord.Commands.VerifyEvidenceRecord; using Tracker.Application.Artifacts.EvidenceRecord.DTOs; @@ -50,13 +51,34 @@ public static void MapEvidenceRecordEndpoints(this IEndpointRouteBuilder app) request.ArtifactId, request.EvidenceType, request.Content, - user.ActorId), ct); + user.ActorId, + request.SourceProvider, + request.Producer, + string.IsNullOrWhiteSpace(request.Classification) + ? EvidenceClassification.Internal + : request.Classification, + request.RetentionPolicyRef, + request.References), ct); return result.ToCreated(id => $"/api/evidence-records/{id}"); }) .RequireTrackerPermission(TrackerPermissions.EvidenceCreate) .WithName("CreateEvidenceRecord"); + group.MapPost("/{id:guid}/references", async ( + Guid id, + AddEvidenceReferenceRequest request, + ITrackerUserContext user, + IMediator mediator, + CancellationToken ct) => + { + var result = await mediator.Send( + new AddEvidenceReferenceCommand(id, user.TenantId, request.Reference), ct); + return result.ToNoContent(); + }) + .RequireTrackerPermission(TrackerPermissions.EvidenceCreate) + .WithName("AddEvidenceReference"); + group.MapPost("/{id:guid}/verify", async ( Guid id, ITrackerUserContext user, @@ -81,7 +103,14 @@ private static EvidenceRecordDto ToDto(EvidenceRecord record) Content = record.Content, Status = record.Status, SubmittedBy = record.SubmittedBy, - SubmittedAtUtc = record.SubmittedAtUtc + SubmittedAtUtc = record.SubmittedAtUtc, + SourceProvider = record.SourceProvider, + Producer = record.Producer, + ContentHash = record.ContentHash, + Classification = record.Classification, + RetentionPolicyRef = record.RetentionPolicyRef, + References = record.References, + Immutable = record.Immutable }; } } @@ -92,4 +121,16 @@ public sealed class CreateEvidenceRecordRequest public string ArtifactId { get; init; } = string.Empty; public string EvidenceType { get; init; } = string.Empty; public string Content { get; init; } = string.Empty; + + // Evidence Graph: optional provenance / classification / lineage inputs. + public string SourceProvider { get; init; } = string.Empty; + public string Producer { get; init; } = string.Empty; + public string Classification { get; init; } = string.Empty; + public string RetentionPolicyRef { get; init; } = string.Empty; + public IReadOnlyList? References { get; init; } +} + +public sealed class AddEvidenceReferenceRequest +{ + public string Reference { get; init; } = string.Empty; } diff --git a/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/AddEvidenceReferenceCommandHandlerTests.cs b/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/AddEvidenceReferenceCommandHandlerTests.cs new file mode 100644 index 00000000..76b2f90e --- /dev/null +++ b/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/AddEvidenceReferenceCommandHandlerTests.cs @@ -0,0 +1,86 @@ +using FluentAssertions; +using Moq; +using Tracker.Application.Artifacts.EvidenceRecord.Commands.AddEvidenceReference; +using Tracker.Domain.Artifacts.EvidenceRecord; +using Tracker.Domain.Kernel; +using Xunit; + +namespace Tracker.Tests.Application.Artifacts; + +public class AddEvidenceReferenceCommandHandlerTests +{ + private readonly Mock _repositoryMock = new(); + private readonly Mock _unitOfWorkMock = new(); + private readonly AddEvidenceReferenceCommandHandler _handler; + private readonly Guid _tenantId = Guid.NewGuid(); + + public AddEvidenceReferenceCommandHandlerTests() + { + _handler = new AddEvidenceReferenceCommandHandler( + _repositoryMock.Object, _unitOfWorkMock.Object); + } + + private EvidenceRecord NewRecord() => EvidenceRecord.Create( + _tenantId, "TechnicalBlueprint", "BP-001", "CodeReview", "Review completed", Guid.NewGuid()).Value; + + [Fact] + public async Task Handle_WithValidReference_ShouldAppendAndSave() + { + var record = NewRecord(); + _repositoryMock + .Setup(r => r.GetByIdAsync(record.Id, It.IsAny())) + .ReturnsAsync(record); + + var result = await _handler.Handle( + new AddEvidenceReferenceCommand(record.Id, _tenantId, "gate-1"), CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + record.References.Should().ContainSingle().Which.Should().Be("gate-1"); + _repositoryMock.Verify(r => r.UpdateAsync(record, It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WhenNotFound_ShouldReturnFailure() + { + _repositoryMock + .Setup(r => r.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((EvidenceRecord?)null); + + var result = await _handler.Handle( + new AddEvidenceReferenceCommand(Guid.NewGuid(), _tenantId, "gate-1"), CancellationToken.None); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("EvidenceRecord.NotFound"); + } + + [Fact] + public async Task Handle_WhenTenantMismatch_ShouldReturnAccessDenied() + { + var record = NewRecord(); + _repositoryMock + .Setup(r => r.GetByIdAsync(record.Id, It.IsAny())) + .ReturnsAsync(record); + + var result = await _handler.Handle( + new AddEvidenceReferenceCommand(record.Id, Guid.NewGuid(), "gate-1"), CancellationToken.None); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("EvidenceRecord.AccessDenied"); + } + + [Fact] + public async Task Handle_WhenReferenceEmpty_ShouldReturnFailure() + { + var record = NewRecord(); + _repositoryMock + .Setup(r => r.GetByIdAsync(record.Id, It.IsAny())) + .ReturnsAsync(record); + + var result = await _handler.Handle( + new AddEvidenceReferenceCommand(record.Id, _tenantId, " "), CancellationToken.None); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("EvidenceRecord.EmptyReference"); + } +} diff --git a/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/EvidenceHashCommandHandlerTests.cs b/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/EvidenceHashCommandHandlerTests.cs index af63f10b..e6a71948 100644 --- a/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/EvidenceHashCommandHandlerTests.cs +++ b/src/apps/tracker-api/Tracker.Tests/Application/Artifacts/EvidenceHashCommandHandlerTests.cs @@ -15,7 +15,7 @@ public class EvidenceHashCommandHandlerTests private readonly Mock _unitOfWorkMock = new(); [Fact] - public async Task Handle_ShouldStoreCorrectContentSha256InMetadata() + public async Task Handle_ShouldPromoteContentSha256ToFirstClassField() { const string content = "Review completed and verified."; EvidenceRecord? added = null; @@ -42,7 +42,10 @@ public async Task Handle_ShouldStoreCorrectContentSha256InMetadata() result.IsSuccess.Should().BeTrue(); added.Should().NotBeNull(); - added!.Metadata.Should().ContainKey("contentSha256"); + // Promoted to the first-class Evidence Graph node field... + added!.ContentHash.Should().Be(expected); + // ...and kept in metadata for backward compatibility. + added.Metadata.Should().ContainKey("contentSha256"); added.Metadata["contentSha256"].Should().Be(expected); } } diff --git a/src/apps/tracker-api/Tracker.Tests/Domain/Artifacts/EvidenceRecordTests.cs b/src/apps/tracker-api/Tracker.Tests/Domain/Artifacts/EvidenceRecordTests.cs index eed457e4..8767248e 100644 --- a/src/apps/tracker-api/Tracker.Tests/Domain/Artifacts/EvidenceRecordTests.cs +++ b/src/apps/tracker-api/Tracker.Tests/Domain/Artifacts/EvidenceRecordTests.cs @@ -80,4 +80,114 @@ public void Reject_WhenSubmitted_ShouldReturnSuccess() result.IsSuccess.Should().BeTrue(); record.Status.Should().Be("rejected"); } + + [Fact] + public void Create_ShouldDefaultProvenanceFields() + { + var record = NewRecord(); + + record.Classification.Should().Be(EvidenceClassification.Internal); + record.Immutable.Should().BeFalse(); + record.References.Should().BeEmpty(); + record.SourceProvider.Should().BeEmpty(); + record.Producer.Should().BeEmpty(); + record.ContentHash.Should().BeEmpty(); + record.RetentionPolicyRef.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldCarryProvenanceFields() + { + var result = EvidenceRecord.Create( + _tenantId, "TechnicalBlueprint", "BP-001", "CodeReview", "Review completed", _userId, + sourceProvider: "github", + producer: "ci-bot", + contentHash: "abc123", + classification: EvidenceClassification.Confidential, + retentionPolicyRef: "retain-7y", + references: new List { "gate-1" }); + + result.IsSuccess.Should().BeTrue(); + var record = result.Value; + record.SourceProvider.Should().Be("github"); + record.Producer.Should().Be("ci-bot"); + record.ContentHash.Should().Be("abc123"); + record.Classification.Should().Be(EvidenceClassification.Confidential); + record.RetentionPolicyRef.Should().Be("retain-7y"); + record.References.Should().ContainSingle().Which.Should().Be("gate-1"); + } + + [Fact] + public void Create_WithInvalidClassification_ShouldReturnFailure() + { + var result = EvidenceRecord.Create( + _tenantId, "TechnicalBlueprint", "BP-001", "CodeReview", "Review completed", _userId, + classification: "top-secret"); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("EvidenceRecord.InvalidClassification"); + } + + [Fact] + public void AddReference_ShouldAppendEdge() + { + var record = NewRecord(); + + var result = record.AddReference("evidence-42"); + + result.IsSuccess.Should().BeTrue(); + record.References.Should().ContainSingle().Which.Should().Be("evidence-42"); + } + + [Fact] + public void AddReference_WhenDuplicate_ShouldBeIdempotent() + { + var record = NewRecord(); + record.AddReference("evidence-42"); + + var result = record.AddReference("evidence-42"); + + result.IsSuccess.Should().BeTrue(); + record.References.Should().ContainSingle().Which.Should().Be("evidence-42"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void AddReference_WhenEmpty_ShouldReturnFailure(string reference) + { + var record = NewRecord(); + + var result = record.AddReference(reference); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("EvidenceRecord.EmptyReference"); + record.References.Should().BeEmpty(); + } + + [Fact] + public void Seal_ShouldSetImmutable() + { + var record = NewRecord(); + + var result = record.Seal(); + + result.IsSuccess.Should().BeTrue(); + record.Immutable.Should().BeTrue(); + } + + [Fact] + public void Seal_WhenAlreadySealed_ShouldBeIdempotent() + { + var record = NewRecord(); + record.Seal(); + + var result = record.Seal(); + + result.IsSuccess.Should().BeTrue(); + record.Immutable.Should().BeTrue(); + } + + private EvidenceRecord NewRecord() => EvidenceRecord.Create( + _tenantId, "TechnicalBlueprint", "BP-001", "CodeReview", "Review completed", _userId).Value; } diff --git a/src/apps/tracker-api/Tracker.Tests/Infrastructure/Persistence/EvidenceGraphRoundTripTests.cs b/src/apps/tracker-api/Tracker.Tests/Infrastructure/Persistence/EvidenceGraphRoundTripTests.cs new file mode 100644 index 00000000..ae1b7a26 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Tests/Infrastructure/Persistence/EvidenceGraphRoundTripTests.cs @@ -0,0 +1,68 @@ +using System.Reflection; +using FluentAssertions; +using Tracker.Domain.Artifacts.EvidenceRecord; +using Tracker.Infrastructure.Persistence.Artifacts.Entities; +using Xunit; + +namespace Tracker.Tests.Infrastructure.Persistence; + +/// +/// Verifies that the CR-14 Evidence Graph provenance/integrity/lineage fields survive a full +/// round trip through PostgreSqlEvidenceRecordRepository's private MapToRecord / MapToDomain +/// mappers (including the jsonb list (de)serialization of References). The mappers are private +/// statics so we reach them via reflection, exercising the exact persistence contract. +/// +public class EvidenceGraphRoundTripTests +{ + private static readonly Type RepoType = + typeof(EvidenceRecordRecord).Assembly.GetType( + "Tracker.Infrastructure.Persistence.Artifacts.PostgreSqlEvidenceRecordRepository")!; + + private static EvidenceRecordRecord MapToRecord(EvidenceRecord aggregate) + => (EvidenceRecordRecord)RepoType + .GetMethod("MapToRecord", BindingFlags.NonPublic | BindingFlags.Static)! + .Invoke(null, new object[] { aggregate })!; + + private static EvidenceRecord MapToDomain(EvidenceRecordRecord record) + => (EvidenceRecord)RepoType + .GetMethod("MapToDomain", BindingFlags.NonPublic | BindingFlags.Static)! + .Invoke(null, new object[] { record })!; + + [Fact] + public void ProvenanceFields_ShouldRoundTrip_ThroughRepositoryMappers() + { + var aggregate = EvidenceRecord.Create( + Guid.NewGuid(), "TechnicalBlueprint", "BP-001", "CodeReview", "Review completed", Guid.NewGuid(), + sourceProvider: "github", + producer: "ci-bot", + contentHash: "abc123", + classification: EvidenceClassification.Confidential, + retentionPolicyRef: "retain-7y", + references: new List { "gate-1", "evidence-2" }).Value; + aggregate.Seal(); + + var rehydrated = MapToDomain(MapToRecord(aggregate)); + + rehydrated.SourceProvider.Should().Be("github"); + rehydrated.Producer.Should().Be("ci-bot"); + rehydrated.ContentHash.Should().Be("abc123"); + rehydrated.Classification.Should().Be(EvidenceClassification.Confidential); + rehydrated.RetentionPolicyRef.Should().Be("retain-7y"); + rehydrated.References.Should().BeEquivalentTo(new[] { "gate-1", "evidence-2" }); + rehydrated.Immutable.Should().BeTrue(); + } + + [Fact] + public void EmptyReferences_ShouldRoundTripToEmptyList() + { + var aggregate = EvidenceRecord.Create( + Guid.NewGuid(), "TechnicalBlueprint", "BP-001", "CodeReview", "Review completed", Guid.NewGuid()).Value; + + var record = MapToRecord(aggregate); + record.ReferencesJson.Should().Be("[]"); + + var rehydrated = MapToDomain(record); + rehydrated.References.Should().BeEmpty(); + rehydrated.Immutable.Should().BeFalse(); + } +}