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
37 changes: 37 additions & 0 deletions docs/artifacts/CORE_ARTIFACT_SCHEMAS.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions docs/artifacts/CORE_ARTIFACT_SCHEMAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/audit/tracker-gap-reference-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/audit/tracker-gaps-opportunities-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
namespace Tracker.Application.Governance.ArtifactFieldSchema;

/// <summary>
/// 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.
/// </summary>
public static class ArtifactSchemaProvenance
{
/// <summary>Evolith Core publishes a canonical JSON Schema for this artifact.</summary>
public const string CoreSchema = "core-schema";

/// <summary>
/// The artifact IS a tool's native output (CI run record, coverage report). The Core declares
/// <c>producedBy</c> rather than a schema on purpose: a bespoke shape would restate what the
/// tool already publishes and rot the day the tool changes.
/// </summary>
public const string ToolOutput = "tool-output";

/// <summary>
/// Nothing upstream. The shape is whatever the tenant's <c>ArtifactFieldSchema</c> says, and
/// per <c>T-056</c> nothing here contradicts it.
/// </summary>
public const string None = "none";
}

/// <summary>The canonical shape of an artifact, as far as the Core publishes one.</summary>
public sealed record ArtifactCanonicalSchemaDto
{
/// <summary>
/// The schema's published <c>$id</c>, or <c>null</c> 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`.
/// </summary>
public string? SchemaId { get; init; }

/// <summary>See <see cref="ArtifactSchemaProvenance"/>.</summary>
public string Provenance { get; init; } = ArtifactSchemaProvenance.None;
}

/// <summary>
/// The correspondence between a Tracker artifact kind and the canonical schema Evolith Core
/// publishes for it (<c>GAP-020</c>).
///
/// <para>THIS IS A WRITTEN DECISION, NOT A TRANSFORMATION. The two vocabularies agree for some
/// artifacts (<c>prd</c>, <c>release-notes</c>, <c>test-summary-report</c>) and diverge for others:
/// <c>security-scan-result</c> is the Core's *Security Scan Report*, <c>rollback-plan</c> is its
/// *Rollback Procedure*, <c>observability-readiness</c> 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.</para>
///
/// <para>The prose version, with the reasoning for every row, is
/// <c>docs/artifacts/CORE_ARTIFACT_SCHEMAS.md</c>. That document and this table are kept identical
/// by <c>CoreArtifactSchemaCatalogTests</c>, 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.</para>
///
/// <para>WHAT THIS DOES NOT DO. It does not make the Core the authority over what a tenant may
/// record. Per <c>T-056</c> content validation is the tenant's configuration and not engine code:
/// the custom fields a tenant adds through <c>ArtifactFieldSchema</c> stay theirs, and this
/// reference is offered to whoever fills an artifact in — never enforced against them here.</para>
/// </summary>
public static class CoreArtifactSchemaCatalog
{
private const string Base = "https://evolith.dev/schema/";

/// <summary>
/// Tracker kind → published <c>$id</c>. 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 <see cref="PhaseArtifactCatalog"/> and therefore nothing here to
/// hang off. They are listed in the markdown so a reader looking for them finds out they exist.
/// </summary>
private static readonly IReadOnlyDictionary<string, string> SchemaIdByKind =
new Dictionary<string, string>
{
["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",
};

/// <summary>
/// Kinds whose content IS a tool's own output. The Core's third such artifact — Pyramid
/// Distribution — has no Tracker kind.
/// </summary>
private static readonly IReadOnlySet<string> ToolOutputKinds =
new HashSet<string> { "ci-pipeline-result", "coverage-report" };

/// <summary>
/// Note on <c>adr-registry</c>: it is NOT wired to <c>adr.schema.json</c> 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.
/// </summary>
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,
};
}

/// <summary>The mapped kinds, for guards that need to compare the table against its source.</summary>
public static IReadOnlyDictionary<string, string> Mappings => SchemaIdByKind;

/// <summary>The tool-output kinds, same purpose.</summary>
public static IReadOnlySet<string> ToolOutputs => ToolOutputKinds;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public IReadOnlyList<PhaseArtifactProfileDto> 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(),
})
Expand Down
Loading
Loading