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
10 changes: 0 additions & 10 deletions .codex/config.toml

This file was deleted.

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,8 @@ vite.config.ts.timestamp-*
# Informe generado por .harness/scripts/coverage-dashboard.mjs; el workflow lo sube
# como artifact. No se versiona ni se valida como documentacion redactada.
COVERAGE_REPORT.md

# Local editor/tool config: machine-specific absolute paths, never shared.
# Committed by accident in a5f719c (a `git add -A`) and only caught when a PR
# targeted main, where the root-cleanliness guard rejects unknown root folders.
.codex/
10 changes: 7 additions & 3 deletions docs/audit/tracker-gap-reference-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an
- **Criticality:** P0 · **Complexity:** M
- **Proposed fix:** Crear un `CoreArtifactFormatCatalog` que sincronice el catálogo por fase desde el Core, materialice una vista tenant-aware y permita overrides locales declarados, auditables y no destructivos.
- **Acceptance criteria:**
- [ ] Discovery, design, construction, QA y release usan un catálogo de artefactos derivado del Core.
- [ ] Cada tenant puede marcar artefactos como requeridos, opcionales o no aplicables sin modificar el estándar base.
- [ ] El scorecard y las compuertas leen la misma fuente de catálogo.
- [x] **Parcial (construction/QA/release):** el expediente operativo (`GetPhaseArtifact`) ya se contrasta contra el catálogo derivado del Core, resolviendo por `IPhaseArtifactProfileSource` — la misma costura que lee `/phase-artifact-profiles`. Discovery y Design conservan sus verticales a medida y NO están cubiertas; el camino genérico solo cubre esas tres fases (`SdlcPhases.GenericGatePhases`).
- [ ] Cada tenant puede marcar artefactos como requeridos, opcionales o no aplicables sin modificar el estándar base. **Sin empezar.** No existe ningún override por tenant sobre este catálogo: el handler no recibe tenant y no hay entidad de aplicabilidad. Es la otra mitad del título de esta ficha.
- [ ] El scorecard y las compuertas leen la misma fuente de catálogo. **Parcial:** el expediente y el endpoint de solo lectura ya comparten fuente; `ScorecardEvaluator` y el camino de `GateSubmission` siguen sin consultarla.
- [ ] `GAP-020` queda re-evaluado a la luz del catálogo vivo.

**Hallazgo que motivó el cambio (2026-08-03):** el Tracker tenía DOS vocabularios de artefactos que nunca se encontraban. `/phase-artifact-profiles` derivaba del Core pero nada operativo lo consultaba; el expediente que la gente llena aceptaba cualquier cadena en `type` y arrancaba con `Items` vacío. Una fase podía completarse entera con artefactos que el Core no declara sin que ninguna superficie lo dijera — que es el **Risk** de esta ficha al pie de la letra.

**Reporta, no impone (T-056).** El bloque `coreCatalog` dice las dos direcciones (lo declarado que falta, y lo presente que el Core no declara) y ahí se detiene. Nada rechaza un ítem ni cambia un estado: qué exige una compuerta lo configura el tenant, no lo cablea el motor. Se distingue además «no había catálogo para la fase» de «el catálogo no pide nada»: la primera omite el bloque en vez de devolver una lista vacía.
- **Dependencies:** CP-01.

#### CP-03
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 @@ -17,7 +17,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor

| ID | Gap | Qué significa | Ejemplo | Component | Phase | Criticality | Complexity | Status |
|---|---|---|---|:---:|:---:|:---:|:---:|:---:|
| [`CP-02`](./tracker-gap-reference-catalog.md#cp-02) | El Tracker no tiene un catálogo vivo de formatos de artefacto por fase sincronizado con el Core y parametrizable por tenant | La iniciativa puede avanzar con una lista local de entregables que ya no coincide con el estándar que el Core evalúa | Core publica gates y schemas para `gate-f1..f5`; Tracker conserva catálogos derivados y un gap histórico de URLs de esquema | `Artifacts/Config` | Cross | P0 | M | `PENDING` |
| [`CP-02`](./tracker-gap-reference-catalog.md#cp-02) | El Tracker no tiene un catálogo vivo de formatos de artefacto por fase sincronizado con el Core y parametrizable por tenant | La iniciativa puede avanzar con una lista local de entregables que ya no coincide con el estándar que el Core evalúa | El expediente operativo de construction/QA/release ya se contrasta contra el catálogo derivado del Core por la misma costura que el endpoint de solo lectura; falta la parametrización por tenant, y el scorecard y las compuertas siguen sin leer esa fuente | `Artifacts/Config` | Cross | P0 | M | `PENDING` |
| [`CP-06`](./tracker-gap-reference-catalog.md#cp-06) | Falta una matriz por tenant que convierta resultados del Core en decisión de compuerta distinguiendo bloqueante, requisito local y advisory | Un hallazgo técnico opcional puede terminar bloqueando una fase, o un requisito contractual puede quedar como simple recomendación | El gateway lee `overallVerdict` y `results.gate`, pero no aplica una política tenant-aware sobre `qualitySignals`, `phaseArtifacts`, `design`, `architecture` o recomendaciones | `Governance/Tenancy` | Cross | P0 | M | `PENDING` |
| [`CP-12`](./tracker-gap-reference-catalog.md#cp-12) | No existe una mesa de ejecución documental por gate que permita operar cada artefacto tenant-aware de inicio a aprobación | El usuario sabe qué artefactos pide el gate, pero no tiene un lugar único para crear la copia del template, editarla, ver su estado y llevarla a aprobación | Los catálogos y wizards cubren piezas; falta el expediente operativo por iniciativa/fase/gate/artefacto | `WEB/Artifacts` | Cross | P0 | L | `PENDING` |
| [`CP-15`](./tracker-gap-reference-catalog.md#cp-15) | Los artefactos y gates no tienen flujos de aprobación configurables por tenant con personas y agentes autorizados | Una fase puede depender de revisores humanos o agentes especializados, pero el Tracker no modela secuencias, quórum, delegaciones ni HITL por artefacto | Las aprobaciones existentes no resuelven miembros/equipos/agentes del tenant para cada artefacto SDLC | `Governance/Tenancy` | Cross | P0 | L | `PENDING` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,51 @@ public sealed class ArtifactItemDto
public string Status { get; init; } = string.Empty;
}

/// <summary>
/// CP-02 — one artifact of the phase AS THE CORE DECLARES IT, next to whether this
/// record actually carries it.
/// </summary>
public sealed class CoreCatalogArtifactDto
{
public string ArtifactKind { get; init; } = string.Empty;
public string Label { get; init; } = string.Empty;

/// <summary>`binding` in the Core registry. Says what the STANDARD expects, not what this Tracker enforces.</summary>
public bool Required { get; init; }

/// <summary>Whether an item of this kind exists on the record. Present, not valid — nothing here reads content.</summary>
public bool Present { get; init; }
}

/// <summary>
/// CP-02 — the phase's artifact catalogue as the Core declares it, resolved through the
/// SAME seam the read-only `/phase-artifact-profiles` endpoint reads.
///
/// <para><b>Why this is REPORTED and never enforced.</b> T-056: what a gate demands is
/// tenant configuration, not engine code. Comparing the record against the standard is
/// a fact; deciding that a missing artifact blocks a phase is an opinion, and it is not
/// ours to wire. So this block states both directions and stops there.</para>
///
/// <para><b>Both directions, deliberately.</b> `Artifacts` says what the Core declares
/// and whether the record carries it; <see cref="UnknownItemKinds"/> says what the record
/// carries that the Core does not declare. Reporting only the first would make a record
/// full of off-standard items look complete.</para>
/// </summary>
public sealed class PhaseCoreCatalogDto
{
/// <summary>
/// `core-sync` once a sync has succeeded, `core-standin` while the mirror is being
/// served. A reader must be able to tell a synced answer from a mirrored one by
/// reading the payload rather than by assuming a sync happened.
/// </summary>
public string Source { get; init; } = string.Empty;

public IReadOnlyList<CoreCatalogArtifactDto> Artifacts { get; init; } = new List<CoreCatalogArtifactDto>();

/// <summary>Item kinds on the record that the Core catalogue does not declare.</summary>
public IReadOnlyList<string> UnknownItemKinds { get; init; } = new List<string>();
}

public sealed class PhaseArtifactDto
{
public Guid Id { get; init; }
Expand All @@ -20,6 +65,13 @@ public sealed class PhaseArtifactDto
public string Phase { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public IReadOnlyList<ArtifactItemDto> Items { get; init; } = new List<ArtifactItemDto>();

/// <summary>
/// Null when no catalogue was resolved for the phase — which is NOT the same as an
/// empty catalogue, and the difference matters: one means "the standard was not
/// available", the other means "the standard asks for nothing here".
/// </summary>
public PhaseCoreCatalogDto? CoreCatalog { get; init; }
}

public static class PhaseArtifactDtoMapper
Expand All @@ -34,6 +86,54 @@ public static class PhaseArtifactDtoMapper
Items = artifact.Items.Select(ToDto).ToList()
};

/// <summary>
/// CP-02 — the same record, contrasted against the Core's catalogue for its phase.
///
/// <para>Item kinds are matched case-insensitively: the catalogue writes slugs and the
/// record accepts whatever a caller sent, so a case difference would report a present
/// artifact as missing — a false gap on a governance surface, which is worse than no
/// comparison at all.</para>
/// </summary>
public static PhaseArtifactDto ToDto(
DomainPhaseArtifact artifact,
string source,
IReadOnlyList<(string Kind, string Label, bool Required)> catalog)
{
var present = artifact.Items
.Select(i => i.Type)
.Where(t => !string.IsNullOrWhiteSpace(t))
.ToHashSet(StringComparer.OrdinalIgnoreCase);

var declared = catalog.Select(c => c.Kind).ToHashSet(StringComparer.OrdinalIgnoreCase);

return new PhaseArtifactDto
{
Id = artifact.Id,
TenantId = artifact.TenantId,
InitiativeId = artifact.InitiativeId,
Phase = artifact.Phase,
Status = artifact.Status,
Items = artifact.Items.Select(ToDto).ToList(),
CoreCatalog = new PhaseCoreCatalogDto
{
Source = source,
Artifacts = catalog
.Select(c => new CoreCatalogArtifactDto
{
ArtifactKind = c.Kind,
Label = c.Label,
Required = c.Required,
Present = present.Contains(c.Kind),
})
.ToList(),
UnknownItemKinds = present
.Where(t => !declared.Contains(t))
.OrderBy(t => t, StringComparer.OrdinalIgnoreCase)
.ToList(),
},
};
}

private static ArtifactItemDto ToDto(ArtifactItem i) => new()
{
Type = i.Type,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
using Tracker.Application.Sdlc.PhaseArtifact.DTOs;
using Tracker.Domain.Sdlc.PhaseArtifact;

// `PhaseArtifactDto` is a live name in BOTH the governance catalogue and this module. The
// namespace is aliased rather than imported so the two never silently resolve to each other.
using IPhaseArtifactProfileSource = Tracker.Application.Governance.ArtifactFieldSchema.IPhaseArtifactProfileSource;

namespace Tracker.Application.Sdlc.PhaseArtifact.Queries.GetPhaseArtifact;

/// <summary>
/// CP-02 — the operative phase record, contrasted against the Core's catalogue.
///
/// <para><b>What this closes.</b> The Tracker had TWO artifact vocabularies that never met:
/// `/phase-artifact-profiles`, read-only and derived from the Core, which nothing operational
/// consulted; and this record, which the user actually fills and which accepted any `type`
/// string whatsoever. A phase could therefore be completed entirely with items the Core does
/// not declare, and no surface would say so. That is CP-02's risk verbatim: the Tracker
/// looks like it governs the SDLC while governing a copy of the standard.</para>
///
/// <para><b>It reports, it does not enforce.</b> T-056: what a gate demands is tenant
/// configuration, not engine code. Nothing here rejects an item, changes a status, or blocks
/// a phase — the comparison is a fact the caller can act on under its own policy.</para>
///
/// <para><b>Same seam, deliberately.</b> It resolves through <see cref="IPhaseArtifactProfileSource"/>,
/// the very source the read-only endpoint reads. Two surfaces answering the same question from
/// two catalogues is the defect; a second copy here would have rebuilt it.</para>
/// </summary>
internal sealed class GetPhaseArtifactQueryHandler
: IQueryHandler<GetPhaseArtifactQuery, PhaseArtifactDto?>
{
private readonly IPhaseArtifactRepository _artifacts;
private readonly IPhaseArtifactProfileSource _profiles;

public GetPhaseArtifactQueryHandler(IPhaseArtifactRepository artifacts)
public GetPhaseArtifactQueryHandler(
IPhaseArtifactRepository artifacts,
IPhaseArtifactProfileSource profiles)
{
_artifacts = artifacts;
_profiles = profiles;
}

public async Task<PhaseArtifactDto?> Handle(
Expand All @@ -21,6 +47,18 @@ public GetPhaseArtifactQueryHandler(IPhaseArtifactRepository artifacts)
if (artifact is null || artifact.TenantId != request.TenantId)
return null;

return PhaseArtifactDtoMapper.ToDto(artifact);
var profile = _profiles.GetProfiles()
.FirstOrDefault(p => string.Equals(p.Phase, artifact.Phase, StringComparison.OrdinalIgnoreCase));

// No profile for the phase is NOT an empty profile. Reporting "the standard asks for
// nothing here" when the truth is "the standard was not available" is the kind of
// confident emptiness a governance surface must never produce, so the block is omitted.
if (profile is null)
return PhaseArtifactDtoMapper.ToDto(artifact);

return PhaseArtifactDtoMapper.ToDto(
artifact,
profile.Source,
profile.Artifacts.Select(a => (a.ArtifactKind, a.Label, a.Required)).ToList());
}
}
Loading
Loading