Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/audit/tracker-gap-reference-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an
- **Acceptance criteria:**
- [ ] Integrations are expressed as capability ports with ACLs; provider replacement preserves history.
- **Dependencies:** [CR-14](#cr-14).
- **Status:** `IN-PROGRESS` — solo display estático (catálogo + panel UI); el agregado `ProviderConnection`, los ports por capacidad, el ACL y los adaptadores siguen sin construir.
- **Status:** `DONE` — dominio `ProviderConnection` (taxonomía capacidad→Port, VO `ProviderAcl`, ciclo de vida), persistencia `provider_connections` + migración `AddProviderConnections`, comandos/queries/endpoints; 493 tests. Diferido: adaptadores externos concretos tras el seam `IProviderPort`.

#### CR-16

Expand Down
2 changes: 1 addition & 1 deletion docs/audit/tracker-gap-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor
| [`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) | **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` |
| [`CR-15`](./tracker-gap-reference-catalog.md#cr-15) | **Hecho:** dominio `ProviderConnection` construido — taxonomía capacidad→Port (`ProviderCapabilityCatalog`), VO `ProviderAcl` (dirección validada + scopes), agregado con ciclo de vida `Connect`/`Disconnect`/`MarkError`/`UpdateAcl`, persistencia (tabla `provider_connections` + índices) + migración aditiva `AddProviderConnections`, comandos/queries + grupo de endpoints `provider-connections`. 493 tests (+20). **Diferido:** adaptadores externos concretos (GitHub/Jira/Datadog) tras el seam `IProviderPort` — integración viva. | `Backend/Integration` | Cross | P1 | M | `DONE` |
| [`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` |
| [`COH-001`](./tracker-gap-reference-catalog.md#coh-001) | Status naming: `CANVAS_SUBMITTED` vs DB `submitted`. InitiativeStatus enum undefined in DDD. `under_review` in DB but zero specs. | `Backend` | Discovery | P2 | M | `PENDING` |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Tracker.Application.Abstractions.Messaging;

namespace Tracker.Application.Integration.ProviderConnection.Commands.RegisterProviderConnection;

public sealed record RegisterProviderConnectionCommand(
Guid TenantId,
string Code,
string Capability,
string Provider,
string AclDirection,
string AclFieldOwnership,
IReadOnlyList<string>? AclScopes,
Guid CreatedBy
) : ICommand<Guid>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Domain.Integration.ProviderConnection;
using Tracker.Domain.Kernel;

namespace Tracker.Application.Integration.ProviderConnection.Commands.RegisterProviderConnection;

internal sealed class RegisterProviderConnectionCommandHandler
: ICommandHandler<RegisterProviderConnectionCommand, Guid>
{
private readonly IProviderConnectionRepository _repository;
private readonly IUnitOfWork _unitOfWork;

public RegisterProviderConnectionCommandHandler(
IProviderConnectionRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}

public async Task<Result<Guid>> Handle(
RegisterProviderConnectionCommand request,
CancellationToken cancellationToken)
{
var aclResult = ProviderAcl.Create(
request.AclDirection,
request.AclFieldOwnership,
request.AclScopes);

if (aclResult.IsFailure)
return Result<Guid>.Failure(aclResult.Error);

var result = Domain.Integration.ProviderConnection.ProviderConnection.Create(
request.TenantId,
request.Code,
request.Capability,
request.Provider,
aclResult.Value,
request.CreatedBy);

if (result.IsFailure)
return Result<Guid>.Failure(result.Error);

await _repository.AddAsync(result.Value, cancellationToken);
await _unitOfWork.SaveEntitiesAsync(cancellationToken);

return Result<Guid>.Success(result.Value.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Tracker.Application.Abstractions.Messaging;

namespace Tracker.Application.Integration.ProviderConnection.Commands.SetProviderConnectionStatus;

public sealed record SetProviderConnectionStatusCommand(
Guid TenantId,
Guid ConnectionId,
string Action,
string? Reason = null
) : ICommand;

/// <summary>Canonical lifecycle actions accepted by <see cref="SetProviderConnectionStatusCommand"/>.</summary>
public static class ProviderConnectionAction
{
public const string Connect = "connect";
public const string Disconnect = "disconnect";
public const string Error = "error";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Domain.Integration.ProviderConnection;
using Tracker.Domain.Kernel;

namespace Tracker.Application.Integration.ProviderConnection.Commands.SetProviderConnectionStatus;

internal sealed class SetProviderConnectionStatusCommandHandler
: ICommandHandler<SetProviderConnectionStatusCommand>
{
private readonly IProviderConnectionRepository _repository;
private readonly IUnitOfWork _unitOfWork;

public SetProviderConnectionStatusCommandHandler(
IProviderConnectionRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(
SetProviderConnectionStatusCommand request,
CancellationToken cancellationToken)
{
var connection = await _repository.GetByIdAsync(request.ConnectionId, cancellationToken);

if (connection is null)
return Result.Failure("ProviderConnection.NotFound");

if (connection.TenantId != request.TenantId)
return Result.Failure("ProviderConnection.AccessDenied");

var result = request.Action switch
{
ProviderConnectionAction.Connect => connection.Connect(),
ProviderConnectionAction.Disconnect => connection.Disconnect(),
ProviderConnectionAction.Error => connection.MarkError(request.Reason ?? string.Empty),
_ => Result.Failure("ProviderConnection.UnknownAction")
};

if (result.IsFailure)
return Result.Failure(result.Error);

await _repository.UpdateAsync(connection, cancellationToken);
await _unitOfWork.SaveEntitiesAsync(cancellationToken);

return Result.Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Tracker.Application.Abstractions.Messaging;

namespace Tracker.Application.Integration.ProviderConnection.Commands.UpdateProviderConnectionAcl;

public sealed record UpdateProviderConnectionAclCommand(
Guid TenantId,
Guid ConnectionId,
string AclDirection,
string AclFieldOwnership,
IReadOnlyList<string>? AclScopes
) : ICommand;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Domain.Integration.ProviderConnection;
using Tracker.Domain.Kernel;

namespace Tracker.Application.Integration.ProviderConnection.Commands.UpdateProviderConnectionAcl;

internal sealed class UpdateProviderConnectionAclCommandHandler
: ICommandHandler<UpdateProviderConnectionAclCommand>
{
private readonly IProviderConnectionRepository _repository;
private readonly IUnitOfWork _unitOfWork;

public UpdateProviderConnectionAclCommandHandler(
IProviderConnectionRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(
UpdateProviderConnectionAclCommand request,
CancellationToken cancellationToken)
{
var connection = await _repository.GetByIdAsync(request.ConnectionId, cancellationToken);

if (connection is null)
return Result.Failure("ProviderConnection.NotFound");

if (connection.TenantId != request.TenantId)
return Result.Failure("ProviderConnection.AccessDenied");

var aclResult = ProviderAcl.Create(
request.AclDirection,
request.AclFieldOwnership,
request.AclScopes);

if (aclResult.IsFailure)
return Result.Failure(aclResult.Error);

var result = connection.UpdateAcl(aclResult.Value);
if (result.IsFailure)
return Result.Failure(result.Error);

await _repository.UpdateAsync(connection, cancellationToken);
await _unitOfWork.SaveEntitiesAsync(cancellationToken);

return Result.Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Tracker.Application.Integration.ProviderConnection.DTOs;

public sealed class ProviderConnectionDto
{
public Guid Id { get; init; }
public string Code { get; init; } = string.Empty;
public string Capability { get; init; } = string.Empty;
public string Provider { get; init; } = string.Empty;
public string Port { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public ProviderAclDto Acl { get; init; } = new();
}

public sealed class ProviderAclDto
{
public string Direction { get; init; } = string.Empty;
public string FieldOwnership { get; init; } = string.Empty;
public IReadOnlyList<string> Scopes { get; init; } = new List<string>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Tracker.Application.Integration.ProviderConnection.DTOs;

/// <summary>Maps the ProviderConnection aggregate to its read DTO.</summary>
internal static class ProviderConnectionMapping
{
public static ProviderConnectionDto ToDto(Domain.Integration.ProviderConnection.ProviderConnection connection)
{
return new ProviderConnectionDto
{
Id = connection.Id,
Code = connection.Code,
Capability = connection.Capability,
Provider = connection.Provider,
Port = connection.Port,
Status = connection.Status,
Acl = new ProviderAclDto
{
Direction = connection.Acl.Direction,
FieldOwnership = connection.Acl.FieldOwnership,
Scopes = connection.Acl.Scopes
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Tracker.Application.Integration.ProviderConnection.Ports;

/// <summary>
/// Driven-side seam for an integration capability adapter. A concrete adapter binds one
/// <see cref="Capability"/> (e.g. "work-management") to a live external provider, translating
/// between the provider's API and the Tracker's canonical Port.
///
/// TODO CR-15: concrete provider adapters (GitHub/Jira/Datadog) are deferred — live integration.
/// This is intentionally a marker interface; the capability-specific method surface
/// (WorkItemPort, SourceControlPort, ObservabilityPort) is introduced when the first live
/// adapter lands. No concrete implementations exist yet.
/// </summary>
public interface IProviderPort
{
/// <summary>The canonical capability this adapter serves (see <c>ProviderCapability</c>).</summary>
string Capability { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Application.Integration.ProviderConnection.DTOs;

namespace Tracker.Application.Integration.ProviderConnection.Queries.GetProviderConnection;

public sealed record GetProviderConnectionQuery(
Guid TenantId,
Guid ConnectionId
) : IQuery<ProviderConnectionDto?>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Application.Integration.ProviderConnection.DTOs;
using Tracker.Domain.Integration.ProviderConnection;

namespace Tracker.Application.Integration.ProviderConnection.Queries.GetProviderConnection;

internal sealed class GetProviderConnectionQueryHandler
: IQueryHandler<GetProviderConnectionQuery, ProviderConnectionDto?>
{
private readonly IProviderConnectionRepository _repository;

public GetProviderConnectionQueryHandler(IProviderConnectionRepository repository)
{
_repository = repository;
}

public async Task<ProviderConnectionDto?> Handle(
GetProviderConnectionQuery request,
CancellationToken cancellationToken)
{
var connection = await _repository.GetByIdAsync(request.ConnectionId, cancellationToken);

if (connection is null || connection.TenantId != request.TenantId)
return null;

return ProviderConnectionMapping.ToDto(connection);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Application.Integration.ProviderConnection.DTOs;

namespace Tracker.Application.Integration.ProviderConnection.Queries.ListProviderConnections;

public sealed record ListProviderConnectionsQuery(
Guid TenantId,
string? Capability = null
) : IQuery<IReadOnlyList<ProviderConnectionDto>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Tracker.Application.Abstractions.Messaging;
using Tracker.Application.Integration.ProviderConnection.DTOs;
using Tracker.Domain.Integration.ProviderConnection;

namespace Tracker.Application.Integration.ProviderConnection.Queries.ListProviderConnections;

internal sealed class ListProviderConnectionsQueryHandler
: IQueryHandler<ListProviderConnectionsQuery, IReadOnlyList<ProviderConnectionDto>>
{
private readonly IProviderConnectionRepository _repository;

public ListProviderConnectionsQueryHandler(IProviderConnectionRepository repository)
{
_repository = repository;
}

public async Task<IReadOnlyList<ProviderConnectionDto>> Handle(
ListProviderConnectionsQuery request,
CancellationToken cancellationToken)
{
var items = string.IsNullOrWhiteSpace(request.Capability)
? await _repository.GetByTenantAsync(request.TenantId, cancellationToken)
: await _repository.GetByTenantAndCapabilityAsync(request.TenantId, request.Capability, cancellationToken);

return items.Select(ProviderConnectionMapping.ToDto).ToList();
}
}
Loading
Loading