diff --git a/docs/artifacts/CORE_ARTIFACT_SCHEMAS.es.md b/docs/artifacts/CORE_ARTIFACT_SCHEMAS.es.md index 212eb420..112c86fd 100644 --- a/docs/artifacts/CORE_ARTIFACT_SCHEMAS.es.md +++ b/docs/artifacts/CORE_ARTIFACT_SCHEMAS.es.md @@ -63,6 +63,43 @@ Tres filas merecen atención en vez de un mapeo silencioso: catálogo del Tracker**. Se listan igualmente, porque quien los busque debe enterarse de que existen en vez de concluir que no. +## Cómo lo consume el Tracker + +Hasta el 2026-08-02 este documento *era* todo GAP-020: una persona podía encontrar la forma +canónica y ninguna máquina podía. La ficha se reabrió por dirección de producto exactamente por eso +— un satélite que sólo documenta el enlace no lo ha consumido. + +La correspondencia vive ahora como **dato** en +`Tracker.Application/Governance/ArtifactFieldSchema/CoreArtifactSchemaCatalog.cs`, y cada +`PhaseArtifactDto` que sirve `GET /api/phase-artifact-profiles` lleva su `canonicalSchema`: + +```json +{ "artifactKind": "prd", "label": "PRD", "required": true, + "canonicalSchema": { "schemaId": "https://evolith.dev/schema/prd.schema.json", + "provenance": "core-schema" } } +``` + +`provenance` tiene tres valores y no un id anulable, porque dos de ellos son *ausencias distintas*: + +| valor | qué significa | qué hacer | +|---|---|---| +| `core-schema` | el Core publica una forma canónica | seguir el `$id` | +| `tool-output` | el Core no publica ninguna, a propósito | leer el formato propio de la herramienta | +| `none` | nada upstream lo cubre | el `ArtifactFieldSchema` del tenant es la única autoridad | + +Juntar `tool-output` y `none` en un `null` borraría la diferencia entre *un hueco que alguien +debería cerrar* y *una decisión ya tomada*. + +La tabla de este documento y la de esa clase se mantienen idénticas mediante +`CoreArtifactSchemaCatalogTests`, que parsea el markdown de arriba y compara en los dos sentidos: +falla una fila de aquí que nadie cableó, y falla también una entrada de allí que nadie documentó. +Se comprobó rompiéndolo en ambas direcciones, no se supuso. + +**Sigue siendo un puntero y no una restricción.** Nada en el Tracker valida el contenido de un +artefacto contra estos esquemas: según [`T-056`](../adrs/T-056-three-layer-separation.md) eso es +configuración del tenant y no código del motor. La referencia se ofrece a quien rellena el +artefacto. + ## Los artefactos que no tienen esquema, y por qué Diez de los veinticuatro no están cubiertos, y se dividen en dos grupos que piden respuestas diff --git a/docs/artifacts/CORE_ARTIFACT_SCHEMAS.md b/docs/artifacts/CORE_ARTIFACT_SCHEMAS.md index bd18c873..1b747e18 100644 --- a/docs/artifacts/CORE_ARTIFACT_SCHEMAS.md +++ b/docs/artifacts/CORE_ARTIFACT_SCHEMAS.md @@ -61,6 +61,42 @@ Three rows deserve attention rather than a silent mapping: Tracker's catalog**. They are listed anyway, because a reader looking for them should find out they exist rather than conclude they do not. +## How the Tracker consumes this + +Until 2026-08-02 this document *was* the whole of GAP-020: a reader could find the canonical shape, +and no machine could. The row was reopened by product direction for exactly that reason — a +satellite that only documents the link has not consumed it. + +The correspondence now lives as **data** in +`Tracker.Application/Governance/ArtifactFieldSchema/CoreArtifactSchemaCatalog.cs`, and every +`PhaseArtifactDto` served by `GET /api/phase-artifact-profiles` carries a `canonicalSchema`: + +```json +{ "artifactKind": "prd", "label": "PRD", "required": true, + "canonicalSchema": { "schemaId": "https://evolith.dev/schema/prd.schema.json", + "provenance": "core-schema" } } +``` + +`provenance` has three values and not a nullable id, because two of them are *different absences*: + +| value | meaning | what a reader should do | +|---|---|---| +| `core-schema` | the Core publishes a canonical shape | follow the `$id` | +| `tool-output` | the Core deliberately publishes none | read the tool's own format | +| `none` | nothing upstream covers it | the tenant's `ArtifactFieldSchema` is the only authority | + +Collapsing `tool-output` and `none` into `null` would erase the distinction between *a gap someone +should close* and *a decision already made*. + +The table in this document and the table in that class are kept identical by +`CoreArtifactSchemaCatalogTests`, which parses the markdown above and compares both ways — a row +here that nobody wired fails, and an entry there that nobody documented fails too. This was +verified by breaking it in both directions, not assumed. + +**It remains a pointer and not a constraint.** Nothing in the Tracker validates artifact content +against these schemas: per [`T-056`](../adrs/T-056-three-layer-separation.md) that is the tenant's +configuration, not engine code. The reference is offered to whoever fills the artifact in. + ## The artifacts that have no schema, and why Ten of the twenty-four are not covered, and they split into two groups that call for different diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index 23fab29b..611ee0fb 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -2947,6 +2947,7 @@ Corregidas las **29 menciones** distinguiendo dos clases que no son lo mismo: la - **Resolution / Next step:** Consumir desde el Core las URLs o referencias canónicas de schema de cada artefacto, registrarlas en el catálogo tenant-aware del Tracker y usarlas como base para validar los formatos expuestos y llenados. - **Status:** `PENDING` (2026-08-01) — reabierta por dirección de producto: aunque el Core define la autoridad de los schemas, el Tracker debe materializar su consumo para operar el flujo SDLC completo. - **Triaje 2026-08-01 — PENDIENTE.** Las URLs de los esquemas de artefacto siguen siendo autoridad del Core; el Tracker no las inventa ni las redefine. El trabajo pendiente en este repositorio es consumirlas, persistir o cachear sus referencias de forma tenant-aware, exponerlas en los formatos de fase y usarlas para validar los artefactos que personas o agentes llenan antes de la evaluación del Core. +- **Avance 2026-08-02 (la ficha sigue PENDIENTE).** Consumo hecho: mapa como dato en `CoreArtifactSchemaCatalog`, `canonicalSchema` en cada artefacto de `/api/phase-artifact-profiles`, enlace en la pantalla de campos y prueba anti-deriva en los dos sentidos contra `docs/artifacts/CORE_ARTIFACT_SCHEMAS.md`. Queda la persistencia/caché tenant-aware de las referencias y —sujeto a decisión de producto por T-056— usarlas para validar lo que se rellena. #### GAP-021 diff --git a/docs/audit/tracker-gaps-opportunities-tracking.md b/docs/audit/tracker-gaps-opportunities-tracking.md index 6ddaba52..c27a1f15 100644 --- a/docs/audit/tracker-gaps-opportunities-tracking.md +++ b/docs/audit/tracker-gaps-opportunities-tracking.md @@ -25,7 +25,7 @@ This document is the only operational gap register in this repository. The maste | # | Status | ID | Type | Category | Component | Module | Story(ies) | Description | Resolution / Next Step | Criticality | Complexity | |---:|---|---|---|---|---|---|---|---|---|:---:|:---:| -| 1 | 🟡 OPEN | [GAP-020](#detail-gap-020) | Docs | Documentation gap | Docs | Docs | N/A | Artifact Schema URLs from Core Missing | REOPENED — this is a correction of my own closure earlier today. I closed it on the strength of `docs/artifacts/CORE_ARTIFACT_SCHEMAS.md`, which links each artifact to its canonical `$id`. The catalog then reopened it the same day by product direction, and the reason is not satisfied by that document: the Tracker must CONSUME the Core schema references — persist or cache them tenant-aware, expose them in the phase forms, and validate what people and agents fill in before the Core evaluates. Documenting the links is a prerequisite, not the deliverable. Aligned to the catalog rather than argued with. | 🟡 MEDIUM | 🟡 MEDIUM | +| 1 | 🟡 OPEN | [GAP-020](#detail-gap-020) | Docs | Documentation gap | Docs | Docs | N/A | Artifact Schema URLs from Core Missing | NARROWED 2026-08-02, not closed. The reopen asked for FOUR things and two are done. **Done:** the references are consumed as data — `CoreArtifactSchemaCatalog` holds the correspondence, every artifact in `GET /api/phase-artifact-profiles` carries a `canonicalSchema` with the published `$id`, and the artifact-fields screen renders it as a link. `provenance` separates `core-schema` from `tool-output` (the Core publishes none on purpose) and from `none` (nothing upstream), because those are different absences. The mapping is a written decision — a slug matcher would miss `security-scan-result` ⇄ Security Scan Report and two others — so a test compares the code against `CORE_ARTIFACT_SCHEMAS.md` in both directions, verified by breaking it both ways. **Still open:** (1) tenant-aware persistence or caching of the references — today the map is static in code, which is arguably right since the references are Core-authoritative and tenant-agnostic, but that is a judgement the row has not ratified; (2) using the schemas to validate what people and agents fill in. Point 2 collides with T-056 — content validation is the tenant's configuration, not engine code — so it needs a product decision before anyone builds it, and this row must not read as done while it is unresolved. | 🟡 MEDIUM | 🟡 MEDIUM | | 2 | 🟡⏳ DEFERRED | [OPP-002](#detail-opp-002) | OPP | Improvement opportunity | Backend | N/A | N/A | Extraer AuditTrail como Shared Kernel — 5+ contextos implementan historiales inmutables | Pending owner/action definition in this register. | 🟠 HIGH | 🔴 HIGH | | 3 | 🟢 RESOLVED | [GAP-023](#detail-gap-023) | Docs | Documentation gap | Docs | Docs | N/A | Re-Do Flow Not Fully Designed | RESOLVED — stale row; the evidence was in the catalog and never reached this register. `tracker-gap-reference-catalog.md` closed it on 2026-07-20 with «YA HECHA» and named the artifact. Re-verified here rather than taken on trust: `reference/specs/design/tracker-redo-flow-design.md` is 254 lines with trigger conditions, the state model, the domain model inside the Release context, the recalculation algorithm, impact propagation, the human-authorization gate, domain events and a traceability table. Its §10 lists five scheduling decisions (RD-D1…RD-D5) explicitly registered as proposals awaiting PO ratification — that is a decision pending an owner, not a design that is missing. | 🟡 MEDIUM | 🟡 MEDIUM | | 4 | 🟢 RESOLVED | [GAP-025](#detail-gap-025) | Docs | Documentation gap | Docs | Docs | N/A | Portal DDD incomplete — NARROWED 2026-08-01: the strategic-map half is DONE (`reference/specs/architecture/bounded-context-map.md` calls itself «the single, authoritative strategic map» of the 9 contexts, with integration patterns and cross-context events). What remains is the four support contexts. | RESOLVED — stale row, same shape. The catalog closed it on 2026-07-19; this register kept the narrowed half open. Verified directly: `bounded-context-map.md` §2 classifies all nine contexts and names the four supporting ones — Governance, Artifacts, Metrics, Integration — each linked to its own tactical model, and all ten `ddd-model.md` files exist on disk. Both halves of the row are satisfied. | 🟡 MEDIUM | 🟡 MEDIUM | diff --git a/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/CoreArtifactSchemaCatalog.cs b/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/CoreArtifactSchemaCatalog.cs new file mode 100644 index 00000000..426a4616 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/CoreArtifactSchemaCatalog.cs @@ -0,0 +1,128 @@ +namespace Tracker.Application.Governance.ArtifactFieldSchema; + +/// +/// Where an artifact's canonical shape comes from — or why it has none. Three states, not a +/// nullable string: «no schema because the Core publishes none» and «no schema because it is a +/// tool's own output» call for opposite answers from a consumer, and collapsing them into `null` +/// makes the difference unrecoverable at the point where it matters. +/// +public static class ArtifactSchemaProvenance +{ + /// Evolith Core publishes a canonical JSON Schema for this artifact. + public const string CoreSchema = "core-schema"; + + /// + /// The artifact IS a tool's native output (CI run record, coverage report). The Core declares + /// producedBy rather than a schema on purpose: a bespoke shape would restate what the + /// tool already publishes and rot the day the tool changes. + /// + public const string ToolOutput = "tool-output"; + + /// + /// Nothing upstream. The shape is whatever the tenant's ArtifactFieldSchema says, and + /// per T-056 nothing here contradicts it. + /// + public const string None = "none"; +} + +/// The canonical shape of an artifact, as far as the Core publishes one. +public sealed record ArtifactCanonicalSchemaDto +{ + /// + /// The schema's published $id, or null when there is none. It is an `$id` and not + /// a repository path deliberately: a path is a fact about where a file sits in one repository + /// at one moment, while the `$id` is the schema's own identity and survives the Core + /// reorganising its tree. Those paths were in fact broken until `evolith_arch32#378`. + /// + public string? SchemaId { get; init; } + + /// See . + public string Provenance { get; init; } = ArtifactSchemaProvenance.None; +} + +/// +/// The correspondence between a Tracker artifact kind and the canonical schema Evolith Core +/// publishes for it (GAP-020). +/// +/// THIS IS A WRITTEN DECISION, NOT A TRANSFORMATION. The two vocabularies agree for some +/// artifacts (prd, release-notes, test-summary-report) and diverge for others: +/// security-scan-result is the Core's *Security Scan Report*, rollback-plan is its +/// *Rollback Procedure*, observability-readiness is its *Observability Validation*. A +/// mapper that matched by slug would silently miss all three. Each entry below is a judgement +/// someone made and can be argued with; none of it is derived. +/// +/// The prose version, with the reasoning for every row, is +/// docs/artifacts/CORE_ARTIFACT_SCHEMAS.md. That document and this table are kept identical +/// by CoreArtifactSchemaCatalogTests, which parses the markdown and compares — because a +/// decision recorded in two places drifts, and this repository has spent the week finding out how +/// quietly. +/// +/// WHAT THIS DOES NOT DO. It does not make the Core the authority over what a tenant may +/// record. Per T-056 content validation is the tenant's configuration and not engine code: +/// the custom fields a tenant adds through ArtifactFieldSchema stay theirs, and this +/// reference is offered to whoever fills an artifact in — never enforced against them here. +/// +public static class CoreArtifactSchemaCatalog +{ + private const string Base = "https://evolith.dev/schema/"; + + /// + /// Tracker kind → published $id. Ten entries: the Core publishes fourteen schemas, but + /// four of them — Discovery Canvas, Ballpark Estimation, Integration Evidence and On-Call + /// Handoff — have no kind in and therefore nothing here to + /// hang off. They are listed in the markdown so a reader looking for them finds out they exist. + /// + private static readonly IReadOnlyDictionary SchemaIdByKind = + new Dictionary + { + ["prd"] = Base + "prd.schema.json", + ["technical-feasibility-canvas"] = Base + "technical-feasibility.schema.json", + ["build-versus-compose-analysis"] = Base + "build-vs-compose.schema.json", + ["bounded-context-map"] = Base + "bounded-context-map.schema.json", + ["definition-of-done-checklist"] = Base + "definition-of-done-checklist.schema.json", + ["test-summary-report"] = Base + "test-summary-report.schema.json", + ["security-scan-result"] = Base + "security-scan-report.schema.json", + ["release-notes"] = Base + "release-notes.schema.json", + ["observability-readiness"] = Base + "observability-validation.schema.json", + ["rollback-plan"] = Base + "rollback-rehearsal.schema.json", + }; + + /// + /// Kinds whose content IS a tool's own output. The Core's third such artifact — Pyramid + /// Distribution — has no Tracker kind. + /// + private static readonly IReadOnlySet ToolOutputKinds = + new HashSet { "ci-pipeline-result", "coverage-report" }; + + /// + /// Note on adr-registry: it is NOT wired to adr.schema.json even though the names + /// invite it. A registry is a *list* of ADRs and not an ADR, so the mapping would produce a + /// validation that fails against the real artifact — coverage on paper, a false negative in + /// practice. + /// + public static ArtifactCanonicalSchemaDto For(string artifactKind) + { + if (SchemaIdByKind.TryGetValue(artifactKind, out var id)) + { + return new ArtifactCanonicalSchemaDto + { + SchemaId = id, + Provenance = ArtifactSchemaProvenance.CoreSchema, + }; + } + + return new ArtifactCanonicalSchemaDto + { + SchemaId = null, + Provenance = ToolOutputKinds.Contains(artifactKind) + ? ArtifactSchemaProvenance.ToolOutput + : ArtifactSchemaProvenance.None, + }; + } + + /// The mapped kinds, for guards that need to compare the table against its source. + public static IReadOnlyDictionary Mappings => SchemaIdByKind; + + /// The tool-output kinds, same purpose. + public static IReadOnlySet ToolOutputs => ToolOutputKinds; +} diff --git a/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/IPhaseArtifactProfileSource.cs b/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/IPhaseArtifactProfileSource.cs index 878c3830..cbd72f85 100644 --- a/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/IPhaseArtifactProfileSource.cs +++ b/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/IPhaseArtifactProfileSource.cs @@ -51,6 +51,9 @@ public IReadOnlyList GetProfiles() => ArtifactKind = a.ArtifactKind, Label = a.Label, Required = a.Required, + // GAP-020: el perfil ya no dice sólo QUE hace falta un artefacto, sino + // dónde está su forma canónica — o por qué no la tiene. + CanonicalSchema = CoreArtifactSchemaCatalog.For(a.ArtifactKind), }) .ToList(), }) diff --git a/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/PhaseArtifactCatalog.cs b/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/PhaseArtifactCatalog.cs index 88d3fd43..92a92999 100644 --- a/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/PhaseArtifactCatalog.cs +++ b/src/apps/tracker-api/Tracker.Application/Governance/ArtifactFieldSchema/PhaseArtifactCatalog.cs @@ -7,13 +7,14 @@ namespace Tracker.Application.Governance.ArtifactFieldSchema; /// the tenant configures custom fields per artifact (via ArtifactFieldSchema keyed by the /// artifact kind) and — for optional artifacts — their applicability. /// -/// GAP-020 — este catálogo dice que un `prd` es obligatorio y NO dice qué debe contener. La -/// forma canónica, para los 10 de 24 artefactos que la tienen aguas arriba, está enlazada en -/// docs/artifacts/CORE_ARTIFACT_SCHEMAS.md. No se enlaza desde aquí como dato porque los -/// nombres no coinciden mecánicamente (`security-scan-result` ⇄ Security Scan Report, -/// `rollback-plan` ⇄ Rollback Procedure) y la correspondencia es una decisión escrita, no una -/// transformación de cadena — y porque el `schemaRef` que publican los gates del Core es una ruta -/// relativa ROTA: resuelve a un directorio que no existe. +/// GAP-020 — este catálogo dice que un `prd` es obligatorio; QUÉ debe contener lo dice +/// ahora , que cuelga de cada artefacto el `$id` del esquema +/// canónico publicado por el Core, o la razón de que no lo tenga. La correspondencia sigue siendo una +/// DECISIÓN ESCRITA y no una transformación de cadena — `security-scan-result` ⇄ Security Scan Report, +/// `rollback-plan` ⇄ Rollback Procedure — y por eso vive en una tabla explícita contrastada contra +/// `docs/artifacts/CORE_ARTIFACT_SCHEMAS.md`. Se enlaza por `$id` y no por ruta: las rutas relativas +/// que publicaban las compuertas del Core estaban rotas hasta `evolith_arch32#378`, y una ruta es un +/// hecho sobre dónde está un fichero hoy, mientras que el `$id` es la identidad del esquema. /// public static class PhaseArtifactCatalog { @@ -78,6 +79,15 @@ public sealed class PhaseArtifactDto public string ArtifactKind { get; init; } = string.Empty; public string Label { get; init; } = string.Empty; public bool Required { get; init; } + + /// + /// The canonical shape the Core publishes for this artifact, or the reason there is none + /// (GAP-020). Until this field existed a consumer learned that a `prd` is required in discovery + /// and had no way to find out what a PRD is supposed to contain. See + /// — and note that it is a POINTER, not a constraint: + /// per T-056 the tenant's ArtifactFieldSchema stays the authority over content. + /// + public ArtifactCanonicalSchemaDto CanonicalSchema { get; init; } = new(); } public sealed class PhaseArtifactProfileDto diff --git a/src/apps/tracker-api/Tracker.Tests/Application/Governance/CoreArtifactSchemaCatalogTests.cs b/src/apps/tracker-api/Tracker.Tests/Application/Governance/CoreArtifactSchemaCatalogTests.cs new file mode 100644 index 00000000..3a150466 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Tests/Application/Governance/CoreArtifactSchemaCatalogTests.cs @@ -0,0 +1,145 @@ +using System.Text.RegularExpressions; +using Tracker.Application.Governance.ArtifactFieldSchema; + +namespace Tracker.Tests.Application.Governance; + +/// +/// Contrasta la tabla de con su fuente en prosa, +/// docs/artifacts/CORE_ARTIFACT_SCHEMAS.md. +/// +/// POR QUÉ. La correspondencia entre los tipos del Tracker y los artefactos del Core es una +/// decisión escrita, y las decisiones escritas en dos sitios se separan. Esta semana el repositorio +/// encontró la misma avería cuatro veces: dos tableros de gaps que se contradecían en 5 de 34 +/// identificadores, y dos ficheros marcados «traducidos» que eran copias del español. En todos los +/// casos había una comprobación verde al lado que miraba otra cosa. +/// +/// Estas pruebas fallan en las DOS direcciones: una fila en el markdown que nadie cableó, y +/// una entrada en el código que nadie documentó. Una guarda que sólo mira un sentido deja el otro +/// como sitio cómodo donde esconder cosas. +/// +public class CoreArtifactSchemaCatalogTests +{ + private const string Doc = "docs/artifacts/CORE_ARTIFACT_SCHEMAS.md"; + + /// + /// Filas de la tabla con tipo del Tracker. Las que dicen (no catalog kind) se saltan a + /// propósito: son artefactos con esquema en el Core y sin tipo aquí, y están en el documento + /// para que quien los busque descubra que existen. + /// + private static Dictionary FilasDelDocumento() + { + var texto = File.ReadAllText(Path.Combine(RaizDelRepositorio(), Doc)); + var filas = new Dictionary(); + + // | fase | `tipo` | Nombre en el Core | [`fichero`](https://evolith.dev/schema/fichero) | + var patron = new Regex( + @"^\|\s*\w+\s*\|\s*`([a-z0-9-]+)`\s*\|[^|]*\|\s*\[[^\]]*\]\((https://evolith\.dev/schema/[^)]+)\)\s*\|", + RegexOptions.Multiline); + + foreach (System.Text.RegularExpressions.Match m in patron.Matches(texto)) + { + filas[m.Groups[1].Value] = m.Groups[2].Value; + } + + return filas; + } + + [Fact] + public void LaTablaDelCodigoYLaDelDocumentoDicenLoMismo() + { + var doc = FilasDelDocumento(); + + // Una guarda que recorre cero filas pasa siempre. Si el formato del documento cambia y el + // patrón deja de encajar, esto lo dice en vez de dar un verde vacío. + doc.Should().HaveCount(10, + "el documento publica catorce esquemas y diez de ellos tienen tipo en el Tracker; " + + "si este número cambia hay que decidir el mapeo, no ajustar la prueba"); + + CoreArtifactSchemaCatalog.Mappings.Should().BeEquivalentTo(doc, + "la correspondencia es una decisión escrita y vive en dos sitios: el documento explica " + + "el porqué de cada fila y el código la aplica. En cuanto discrepan, uno de los dos " + + "miente y nadie sabe cuál"); + } + + [Fact] + public void LosTiposMapeadosExistenEnElCatalogoDeFases() + { + var conocidos = PhaseArtifactCatalog.ByPhase.Values + .SelectMany(a => a) + .Select(a => a.ArtifactKind) + .ToHashSet(); + + foreach (var tipo in CoreArtifactSchemaCatalog.Mappings.Keys.Concat(CoreArtifactSchemaCatalog.ToolOutputs)) + { + conocidos.Should().Contain(tipo, + $"`{tipo}` tiene esquema o procedencia declarada pero ningún artefacto lo pide: " + + "es un mapeo a la nada, y se lee como cobertura"); + } + } + + [Fact] + public void ElRegistroDeAdrNoSeMapeaAlEsquemaDeUnAdr() + { + // Los nombres invitan al atajo. Un registro es una LISTA de ADRs y no un ADR, así que el + // mapeo daría una validación que falla contra el artefacto real: cobertura sobre el papel + // y un falso negativo en la práctica. + CoreArtifactSchemaCatalog.For("adr-registry").Provenance + .Should().Be(ArtifactSchemaProvenance.None); + } + + [Fact] + public void SinEsquemaYSalidaDeHerramientaNoSeConfunden() + { + // Son dos ausencias distintas y piden respuestas opuestas: a una le falta un esquema que + // habría que escribir; la otra NO debe tenerlo nunca. Colapsarlas en `null` perdería eso. + CoreArtifactSchemaCatalog.For("ci-pipeline-result").Provenance + .Should().Be(ArtifactSchemaProvenance.ToolOutput); + CoreArtifactSchemaCatalog.For("coverage-report").Provenance + .Should().Be(ArtifactSchemaProvenance.ToolOutput); + CoreArtifactSchemaCatalog.For("business-case").Provenance + .Should().Be(ArtifactSchemaProvenance.None); + + CoreArtifactSchemaCatalog.For("ci-pipeline-result").SchemaId.Should().BeNull(); + CoreArtifactSchemaCatalog.For("business-case").SchemaId.Should().BeNull(); + } + + [Fact] + public void LosTresNombresQueNoCoincidenEstanCableadosAMano() + { + // Un mapeador por slug fallaría estos tres, y fallaría en silencio. + CoreArtifactSchemaCatalog.For("security-scan-result").SchemaId + .Should().EndWith("security-scan-report.schema.json"); + CoreArtifactSchemaCatalog.For("rollback-plan").SchemaId + .Should().EndWith("rollback-rehearsal.schema.json"); + CoreArtifactSchemaCatalog.For("observability-readiness").SchemaId + .Should().EndWith("observability-validation.schema.json"); + } + + [Fact] + public void ElPerfilQueSeSirveLlevaLaReferencia() + { + var perfiles = new StandInPhaseArtifactProfileSource().GetProfiles(); + + var prd = perfiles.Single(p => p.Phase == "discovery") + .Artifacts.Single(a => a.ArtifactKind == "prd"); + + prd.CanonicalSchema.Provenance.Should().Be(ArtifactSchemaProvenance.CoreSchema); + prd.CanonicalSchema.SchemaId.Should().Be("https://evolith.dev/schema/prd.schema.json"); + + // Y el que no tiene esquema lo dice, en vez de venir vacío y parecer un fallo de carga. + var caso = perfiles.Single(p => p.Phase == "discovery") + .Artifacts.Single(a => a.ArtifactKind == "business-case"); + caso.CanonicalSchema.Provenance.Should().Be(ArtifactSchemaProvenance.None); + } + + private static string RaizDelRepositorio() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, ".git"))) + { + dir = dir.Parent; + } + dir.Should().NotBeNull("la prueba necesita ubicar la raiz del repositorio"); + return dir!.FullName; + } +} diff --git a/src/apps/tracker-web/src/api/types.ts b/src/apps/tracker-web/src/api/types.ts index 0cfba37e..5613a901 100644 --- a/src/apps/tracker-web/src/api/types.ts +++ b/src/apps/tracker-web/src/api/types.ts @@ -255,11 +255,34 @@ export interface UpsertArtifactFieldSchemaRequest { customFields: CustomFieldDto[]; } +/** + * Where an artifact's canonical shape comes from — or why it has none (GAP-020). + * + * Three states rather than a nullable id, because two of them are different absences and call for + * opposite answers: `tool-output` means the Core deliberately publishes NO schema (the artifact is + * a CI run record or a coverage report, and a bespoke shape would restate what the tool already + * emits), while `none` means nothing upstream covers it and the tenant's own field schema is the + * only authority. Collapsing both into `null` would make the difference unrecoverable in the UI. + */ +export type ArtifactSchemaProvenance = 'core-schema' | 'tool-output' | 'none'; + +export interface ArtifactCanonicalSchemaDto { + /** The schema's published `$id` — not a repository path, which would break when Core moves it. */ + schemaId?: string | null; + provenance: ArtifactSchemaProvenance | string; +} + /** One Core-authoritative base artifact within a phase (read-only for the tenant). */ export interface PhaseArtifactDto { artifactKind: string; label: string; required: boolean; + /** + * The canonical shape Core publishes for this artifact (GAP-020). A pointer, never a constraint: + * per T-056 the tenant's custom fields stay the authority over content. Absent on legacy + * responses. + */ + canonicalSchema?: ArtifactCanonicalSchemaDto | null; } /** GET /api/phase-artifact-profiles — Core base artifacts per phase (PRD required in Discovery, …). */ diff --git a/src/apps/tracker-web/src/components/screens/md3/FieldSchemaManager.tsx b/src/apps/tracker-web/src/components/screens/md3/FieldSchemaManager.tsx index be4a9f15..1f9fb668 100644 --- a/src/apps/tracker-web/src/components/screens/md3/FieldSchemaManager.tsx +++ b/src/apps/tracker-web/src/components/screens/md3/FieldSchemaManager.tsx @@ -12,7 +12,7 @@ import { usePhaseArtifactProfiles, useUpsertArtifactFieldSchema, } from '../../../api'; -import type { CustomFieldDto } from '../../../api'; +import type { ArtifactCanonicalSchemaDto, CustomFieldDto } from '../../../api'; import { Button, Card, @@ -37,10 +37,55 @@ const PHASE_LABEL: Record = { }; /** Per-artifact custom-field editor. The artifact itself is Core-defined; only fields are editable. */ -const ArtifactFieldCard: React.FC<{ artifactKind: string; label: string; required: boolean }> = ({ +/** + * El puntero a la forma canonica de un artefacto (GAP-020). Las tres procedencias se muestran + * distinto A PROPOSITO: un esquema publicado se puede abrir, una salida de herramienta NO tiene + * esquema por decision del Core, y `none` es un hueco de verdad. Un guion para los tres casos + * ocultaria justo la informacion que hace falta para saber si falta trabajo o no. + */ +const CanonicalSchemaHint: React.FC<{ schema?: ArtifactCanonicalSchemaDto | null }> = ({ schema }) => { + const { t } = useTranslation(); + if (!schema) return null; + + if (schema.provenance === 'core-schema' && schema.schemaId) { + return ( + + ); + } + + const texto = + schema.provenance === 'tool-output' + ? t('shape defined by the emitting tool', 'forma definida por la herramienta que lo emite') + : t('no upstream schema — your fields define it', 'sin esquema upstream — lo definen tus campos'); + + return ( +
+ {texto} +
+ ); +}; + +const ArtifactFieldCard: React.FC<{ + artifactKind: string; + label: string; + required: boolean; + canonicalSchema?: ArtifactCanonicalSchemaDto | null; +}> = ({ artifactKind, label, required, + canonicalSchema, }) => { const { t } = useTranslation(); const query = useArtifactFieldSchema(artifactKind); @@ -115,6 +160,11 @@ const ArtifactFieldCard: React.FC<{ artifactKind: string; label: string; require {artifactKind} {count > 0 ? ` · ${count} ${count === 1 ? t('custom field', 'campo personalizado') : t('custom fields', 'campos personalizados')}` : ''} + {/* GAP-020 — hasta aqui la tarjeta decia que un artefacto hace falta y no QUE debe + contener. El enlace apunta a la forma canonica publicada por el Core; es una + referencia y no una restriccion (T-056), por eso se ofrece y no se impone. Las dos + ausencias se distinguen: `tool-output` es deliberada, `none` es un hueco. */} +