diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 67e0b94f..9dae8df1 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -33,6 +33,14 @@ jobs:
- name: Validate root cleanliness
run: node .harness/scripts/validate-root-cleanliness.mjs
+ # GT-617 — los inventarios documentales se DERIVAN del código, no se mantienen a mano.
+ # Falla el build a proposito: la insignia de ADRs, el mapa de schemas y el listado de
+ # robots llevaban meses desfasados (30 vs 54 decisiones, 10 vs 8 schemas, 3 vs 12
+ # robots) y nadie lo noto, porque nada los contrastaba con la fuente. Corre en segundos
+ # y no necesita ni base de datos ni SDK.
+ - name: Documentation inventory must match the code
+ run: node .harness/scripts/doc-inventory.mjs --check
+
- name: Check orphan bilingual files
# El escalar plano de `run:` no puede contener ": " — rompia el parseo del workflow y
# por eso este fichero nunca llego a ejecutarse (374 ejecuciones, 0 exitosas). Bloque
diff --git a/.harness/scripts/doc-inventory.mjs b/.harness/scripts/doc-inventory.mjs
new file mode 100644
index 00000000..047a6571
--- /dev/null
+++ b/.harness/scripts/doc-inventory.mjs
@@ -0,0 +1,409 @@
+#!/usr/bin/env node
+// -----------------------------------------------------------------------------
+// GT-617 — inventarios documentales DERIVADOS del código, no mantenidos a mano.
+//
+// El defecto que cierra no es que unos números estuvieran mal. Es que eran números
+// A MANO: la insignia del README decía «30 decisions» contra 54 reales, el diseño
+// de datos declaraba 10 schemas y 32 tablas contra 8 y 49 reales —y nombraba cinco
+// schemas que no existen—, y el README de RoboSoft listaba 3 robots contra 12. Cada
+// uno se escribió correcto el día que se escribió y se quedó atrás en silencio.
+// Corregirlos a mano reproduce exactamente el mismo fallo dentro de un mes.
+//
+// Aquí hay UNA derivación y tres consumidores. Las fuentes de verdad son artefactos
+// que el código ya produce y que no se pueden falsear escribiendo prosa:
+//
+// decisiones → DECISIONS.md, filas `| T-NNN | ... |` del registro canónico
+// schemas → los ModelSnapshot de EF Core (`b.ToTable("tabla", "schema")`),
+// que EF regenera en cada migración: si alguien añade una tabla, el
+// snapshot cambia, y esta guarda se pone roja al instante
+// robots → robosoft/robots/*.robot.mjs, IMPORTADOS con el mismo mecanismo con
+// el que los carga `robosoft/run.mjs` (no una regex sobre la fuente:
+// dos formas de descubrir el mismo conjunto acaban divergiendo)
+//
+// Uso:
+// node .harness/scripts/doc-inventory.mjs imprime el inventario derivado
+// node .harness/scripts/doc-inventory.mjs --write reescribe los bloques generados
+// node .harness/scripts/doc-inventory.mjs --check falla (exit 1) si hay deriva
+//
+// `--check` es lo que corre CI. Falla CERRADO por diseño: si una fuente rinde cero
+// elementos, o un documento destino ha perdido sus marcas, sale con 2 en vez de pasar.
+// Una guarda que sólo puede pasar es el patrón de falso verde que este tablero lleva
+// meses cazando; ésta se escribió viéndola primero en ROJO contra los documentos que
+// el diagnóstico denunció.
+// -----------------------------------------------------------------------------
+
+import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { dirname, join, relative } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import process from 'node:process';
+
+const AQUI = dirname(fileURLToPath(import.meta.url));
+const RAIZ = join(AQUI, '..', '..');
+
+const rutaDe = (...p) => join(RAIZ, ...p);
+const leer = (...p) => readFileSync(rutaDe(...p), 'utf8');
+
+// ── Fuentes de verdad ────────────────────────────────────────────────────────
+
+/**
+ * Los ModelSnapshot de EF Core. Se leen los DOS contextos: `TrackerDbContext` (el
+ * dominio) y `TenantProjectionDbContext` (la réplica de MMS que `T-047` declaró como
+ * excepción CON MOTIVO). Contar sólo el primero daría 7 schemas y 45 tablas y dejaría
+ * fuera un schema que existe de verdad en la base — que es justo la clase de recorte
+ * silencioso que produjo el número original.
+ */
+const CONTEXTOS = [
+ {
+ contexto: 'TrackerDbContext',
+ ruta: 'src/apps/tracker-api/Tracker.Infrastructure/Migrations/TrackerDbContextModelSnapshot.cs',
+ },
+ {
+ contexto: 'TenantProjectionDbContext',
+ ruta: 'src/apps/tracker-api/Tracker.Infrastructure/MasterData/Migrations/TenantProjectionDbContextModelSnapshot.cs',
+ },
+];
+
+function derivarSchemas() {
+ const patron = /\.ToTable\(\s*"([^"]+)"\s*,\s*"([^"]+)"/g;
+ const porSchema = new Map();
+
+ for (const { contexto, ruta } of CONTEXTOS) {
+ const fuente = leer(ruta);
+ let m;
+ let encontradas = 0;
+ while ((m = patron.exec(fuente)) !== null) {
+ const [, tabla, schema] = m;
+ encontradas += 1;
+ if (!porSchema.has(schema)) porSchema.set(schema, { schema, contexto, tablas: [] });
+ porSchema.get(schema).tablas.push(tabla);
+ }
+ if (encontradas === 0) {
+ abortar(`${ruta} no produjo ninguna tabla — o el snapshot cambió de forma o la ruta es incorrecta`);
+ }
+ }
+
+ const schemas = [...porSchema.values()]
+ .map((s) => ({ ...s, tablas: s.tablas.sort() }))
+ .sort((a, b) => a.schema.localeCompare(b.schema));
+
+ return {
+ schemas,
+ totalSchemas: schemas.length,
+ totalTablas: schemas.reduce((n, s) => n + s.tablas.length, 0),
+ };
+}
+
+/**
+ * Decisiones del satélite. `T-NNN` es el namespace local (distinto de los `ADR-NNNN`
+ * upstream del Core, que este satélite REFERENCIA pero no posee), y `DECISIONS.md` es
+ * su registro canónico. Se cuentan los IDS DISTINTOS de la primera columna de la tabla,
+ * no las menciones: un ADR citado en la columna de notas de otro no es otra decisión.
+ */
+function derivarDecisiones() {
+ const fuente = leer('DECISIONS.md');
+ const ids = new Set();
+ for (const linea of fuente.split('\n')) {
+ const m = /^\|\s*(T-\d{3})\s*\|/.exec(linea);
+ if (m) ids.add(m[1]);
+ }
+ if (ids.size === 0) abortar('DECISIONS.md no produjo ninguna fila `| T-NNN |`');
+
+ const ordenados = [...ids].sort();
+ return {
+ total: ids.size,
+ primera: ordenados[0],
+ ultima: ordenados[ordenados.length - 1],
+ };
+}
+
+/**
+ * Robots de RoboSoft. Se IMPORTAN, igual que hace `robosoft/run.mjs`, en vez de
+ * raspar la fuente: `governance-journey.robot.mjs` contiene un `name:` de attrezzo
+ * en un objeto interior que cualquier regex confundiría con el nombre del robot.
+ */
+async function derivarRobots() {
+ const dir = rutaDe('robosoft', 'robots');
+ const ficheros = readdirSync(dir).filter((f) => f.endsWith('.robot.mjs')).sort();
+ if (ficheros.length === 0) abortar('robosoft/robots/ no contiene ningún *.robot.mjs');
+
+ const robots = [];
+ for (const f of ficheros) {
+ const mod = await import(pathToFileURL(join(dir, f)).href);
+ const robot = mod.default;
+ if (!robot?.name || typeof robot.run !== 'function') {
+ abortar(`robosoft/robots/${f} no exporta un robot válido ({ name, run })`);
+ }
+ robots.push({ fichero: f, name: robot.name, description: robot.description ?? '' });
+ }
+ return robots.sort((a, b) => a.name.localeCompare(b.name));
+}
+
+// ── Bloques generados ────────────────────────────────────────────────────────
+
+const marcaInicio = (id) =>
+ ``;
+const marcaFin = (id) => ``;
+
+function insigniaAdrs(inv, idioma) {
+ const etiqueta = idioma === 'es' ? 'decisiones' : 'decisions';
+ const alt = idioma === 'es' ? 'ADRs' : 'ADRs';
+ return `[](./DECISIONS.md)`;
+}
+
+function mapaDeSchemas(inv, idioma) {
+ const es = idioma === 'es';
+ const cabecera = es
+ ? '| Schema | Contexto EF | Tablas | Tablas (derivadas del snapshot de EF) |'
+ : '| Schema | EF context | Tables | Tables (derived from the EF snapshot) |';
+ const separador = '|--------|------------|--------|---------------------------------------|';
+
+ const filas = inv.esquemas.schemas.map(
+ (s) => `| \`${s.schema}\` | \`${s.contexto}\` | ${s.tablas.length} | ${s.tablas.map((t) => `\`${t}\``).join(', ')} |`,
+ );
+
+ const resumen = es
+ ? `**${inv.esquemas.totalSchemas} schemas · ${inv.esquemas.totalTablas} tablas.** Derivado de los \`ModelSnapshot\` de EF Core, que son lo que la base tiene de verdad.`
+ : `**${inv.esquemas.totalSchemas} schemas · ${inv.esquemas.totalTablas} tables.** Derived from the EF Core \`ModelSnapshot\` files, which are what the database actually has.`;
+
+ return [resumen, '', cabecera, separador, ...filas].join('\n');
+}
+
+function tablaDeRobots(inv) {
+ const filas = inv.robots.map((r) => `| \`${r.name}\` | ${r.description} |`);
+ return [
+ `**${inv.robots.length} robots.** Derived from \`robosoft/robots/*.robot.mjs\` — the same discovery \`robosoft/run.mjs\` performs, so this table cannot disagree with what a run executes.`,
+ '',
+ '| Robot | Proves |',
+ '|-------|--------|',
+ ...filas,
+ ].join('\n');
+}
+
+/**
+ * Los destinos. Cada uno declara el documento, el id del bloque y cómo se renderiza.
+ * Añadir un consumidor es añadir una fila aquí, no otra copia de los números.
+ */
+function destinos(inv) {
+ return [
+ { fichero: 'README.md', bloque: 'adr-count', cuerpo: insigniaAdrs(inv, 'en') },
+ { fichero: 'README.es.md', bloque: 'adr-count', cuerpo: insigniaAdrs(inv, 'es') },
+ {
+ fichero: 'reference/specs/design/tracker-postgresql-data-design.md',
+ bloque: 'schema-map',
+ cuerpo: mapaDeSchemas(inv, 'en'),
+ },
+ {
+ fichero: 'reference/specs/design/tracker-postgresql-data-design.es.md',
+ bloque: 'schema-map',
+ cuerpo: mapaDeSchemas(inv, 'es'),
+ },
+ { fichero: 'robosoft/README.md', bloque: 'robot-roster', cuerpo: tablaDeRobots(inv) },
+ ];
+}
+
+function reemplazarBloque(texto, bloque, cuerpo, fichero) {
+ const inicio = marcaInicio(bloque);
+ const fin = marcaFin(bloque);
+ const i = texto.indexOf(inicio);
+ const j = texto.indexOf(fin);
+
+ // Fallo CERRADO: un destino sin marcas no se «salta», aborta. Si se saltara, borrar
+ // las marcas de un documento sería la forma más fácil de poner la guarda en verde —
+ // exactamente al revés de lo que tiene que premiar.
+ if (i === -1 || j === -1 || j < i) {
+ abortar(`${fichero} no contiene el bloque generado '${bloque}' (marcas BEGIN/END ausentes o desordenadas)`);
+ }
+
+ return texto.slice(0, i) + inicio + '\n' + cuerpo + '\n' + texto.slice(j);
+}
+
+// ── Cifras a mano sueltas por el corpus ──────────────────────────────────────
+//
+// Reescribir cinco bloques no basta. La cifra falsa del diseño de datos («10 schemas»)
+// se había copiado a NUEVE documentos más: los índices de área, el C4, la hoja de ruta,
+// el spec de CI/CD. Ese es el mecanismo real del defecto — un número escrito a mano se
+// propaga por copia y cada copia envejece por su cuenta. Sin esta pasada, arreglar los
+// bloques generados dejaría el tablero exactamente igual de mentiroso dentro de un mes.
+//
+// Se rastrea sólo el corpus VIVO. Quedan fuera a propósito:
+// DECISIONS*.md registro de decisiones: `T-028` DEBE poder decir «declaraba 10
+// schemas», porque narra lo que se superó
+// docs/adrs/** misma razón: un ADR superado cita su propia cifra
+// docs/audit/** el tablero de gaps y su catálogo — narran el hallazgo histórico
+// docs/architecture-review/** informe fechado, no documentación viva
+//
+// Escape para prosa histórica legítima dentro del corpus vivo: ``
+// en la misma línea.
+
+const CORPUS_VIVO = ['README.md', 'README.es.md', 'MASTER_INDEX.md', 'MASTER_INDEX.es.md', 'reference', 'robosoft'];
+const ESCAPE = '';
+
+function ficherosMarkdown(rutaRelativa) {
+ const absoluta = rutaDe(rutaRelativa);
+ let entradas;
+ try {
+ entradas = readdirSync(absoluta, { withFileTypes: true });
+ } catch {
+ return absoluta.endsWith('.md') ? [rutaRelativa] : [];
+ }
+
+ const salida = [];
+ for (const e of entradas) {
+ const hijo = `${rutaRelativa}/${e.name}`;
+ if (e.isDirectory()) {
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
+ salida.push(...ficherosMarkdown(hijo));
+ } else if (e.name.endsWith('.md')) {
+ salida.push(hijo);
+ }
+ }
+ return salida;
+}
+
+function cifrasAMano(inv) {
+ // Sólo se vigilan afirmaciones GLOBALES e inequívocas. La primera versión de esta pasada
+ // rastreaba además «N tablas» y «N decisiones» en prosa libre, y se puso roja 29 veces
+ // contra cosas legítimas: los números de epígrafe (`### 1.1 Schema Map` → «1 Schema»), los
+ // ids de ADR (`ADR-0043 decision`) y los recuentos POR SCHEMA de una tabla de migraciones
+ // («5 tables» en la fila de un fichero concreto), que son ciertos y no tienen por qué
+ // sumar el total. Una guarda con ese ruido se desactiva en una semana, y entonces no
+ // vigila nada — es el mismo final que una guarda que sólo puede pasar.
+ //
+ // El `(? ficherosMarkdown(r));
+
+ if (ficheros.length === 0) abortar('el corpus vivo no rindió ningún .md — revisa CORPUS_VIVO');
+
+ for (const fichero of ficheros) {
+ const lineas = leer(fichero).split('\n');
+ lineas.forEach((linea, i) => {
+ if (linea.includes(ESCAPE)) return;
+ for (const { patron, esperado, que } of afirmaciones) {
+ patron.lastIndex = 0;
+ let m;
+ while ((m = patron.exec(linea)) !== null) {
+ const declarado = Number(m[1]);
+ if (declarado !== esperado) {
+ hallazgos.push({ fichero, linea: i + 1, texto: m[0].trim(), que, declarado, esperado });
+ }
+ }
+ }
+ });
+ }
+
+ return hallazgos;
+}
+
+// ── Ejecución ────────────────────────────────────────────────────────────────
+
+function abortar(mensaje) {
+ console.error(`FATAL: ${mensaje}`);
+ process.exit(2);
+}
+
+async function inventario() {
+ return {
+ decisiones: derivarDecisiones(),
+ esquemas: derivarSchemas(),
+ robots: await derivarRobots(),
+ };
+}
+
+async function main() {
+ const modo = process.argv[2] ?? '--print';
+ const inv = await inventario();
+
+ if (modo === '--print') {
+ console.log(JSON.stringify(
+ {
+ decisiones: inv.decisiones,
+ schemas: inv.esquemas.totalSchemas,
+ tablas: inv.esquemas.totalTablas,
+ porSchema: Object.fromEntries(inv.esquemas.schemas.map((s) => [s.schema, s.tablas.length])),
+ robots: inv.robots.map((r) => r.name),
+ },
+ null,
+ 2,
+ ));
+ return;
+ }
+
+ if (modo !== '--write' && modo !== '--check') {
+ abortar(`modo desconocido '${modo}' (usa --print, --write o --check)`);
+ }
+
+ const derivas = [];
+
+ for (const destino of destinos(inv)) {
+ const ruta = rutaDe(destino.fichero);
+ const actual = readFileSync(ruta, 'utf8');
+ const esperado = reemplazarBloque(actual, destino.bloque, destino.cuerpo, destino.fichero);
+
+ if (actual === esperado) continue;
+
+ if (modo === '--write') {
+ writeFileSync(ruta, esperado, 'utf8');
+ console.log(` actualizado ${relative(RAIZ, ruta)} [${destino.bloque}]`);
+ } else {
+ derivas.push({ fichero: destino.fichero, bloque: destino.bloque });
+ }
+ }
+
+ if (modo === '--write') {
+ console.log(
+ `\n ${inv.decisiones.total} decisiones (${inv.decisiones.primera}..${inv.decisiones.ultima}) · `
+ + `${inv.esquemas.totalSchemas} schemas · ${inv.esquemas.totalTablas} tablas · ${inv.robots.length} robots`,
+ );
+ return;
+ }
+
+ const sueltas = cifrasAMano(inv);
+
+ if (derivas.length > 0 || sueltas.length > 0) {
+ if (derivas.length > 0) {
+ console.error('\nDERIVA DOCUMENTAL — los bloques generados no coinciden con el código:\n');
+ for (const d of derivas) {
+ console.error(` ✗ ${d.fichero} [${d.bloque}]`);
+ }
+ console.error('\n Arréglalo con: node .harness/scripts/doc-inventory.mjs --write');
+ }
+
+ if (sueltas.length > 0) {
+ console.error('\nCIFRAS A MANO — números escritos a mano que contradicen el código:\n');
+ for (const s of sueltas) {
+ console.error(
+ ` ✗ ${s.fichero}:${s.linea} "${s.texto}" → ${s.que} reales: ${s.esperado}`,
+ );
+ }
+ console.error(
+ '\n Reescribe la frase sin la cifra (o apunta al bloque generado). Si la línea narra\n'
+ + ` historia a propósito, márcala con ${ESCAPE}`,
+ );
+ }
+
+ console.error(
+ '\nValores reales derivados ahora mismo:\n'
+ + ` decisiones : ${inv.decisiones.total} (${inv.decisiones.primera}..${inv.decisiones.ultima})\n`
+ + ` schemas : ${inv.esquemas.totalSchemas}\n`
+ + ` tablas : ${inv.esquemas.totalTablas}\n`
+ + ` robots : ${inv.robots.length}\n`,
+ );
+ process.exit(1);
+ }
+
+ console.log(
+ `OK — inventario documental al día: ${inv.decisiones.total} decisiones · `
+ + `${inv.esquemas.totalSchemas} schemas · ${inv.esquemas.totalTablas} tablas · ${inv.robots.length} robots`,
+ );
+}
+
+await main();
diff --git a/MASTER_INDEX.es.md b/MASTER_INDEX.es.md
index 2be5dd16..b89cc7ae 100644
--- a/MASTER_INDEX.es.md
+++ b/MASTER_INDEX.es.md
@@ -36,7 +36,7 @@ Bienvenido a la documentación del **Evolith Tracker**, repositorio satélite de
| [Tracker Target Architecture](./reference/specs/design/tracker-target-architecture.md) | — | TAD maestro: bounded contexts, domain model, layer structure, integrations |
| [NestJS Backend Design](./reference/specs/design/tracker-nestjs-backend-design.md) | — | Arquitectura NestJS: módulos, CQRS, repositories, guards, middleware |
| [React Frontend Design](./reference/specs/design/tracker-react-frontend-design.md) | — | Arquitectura React: state management, routing, permission-driven UI |
-| [PostgreSQL Data Design](./reference/specs/design/tracker-postgresql-data-design.md) | — | Diseño de datos: 10 schemas, DDL, RLS, indexes, concurrency, migrations |
+| [PostgreSQL Data Design](./reference/specs/design/tracker-postgresql-data-design.md) | — | Diseño de datos: mapa de schemas (derivado del código), DDL, RLS, indexes, concurrency, migrations |
| [UMS Authentication Integration](./reference/specs/design/tracker-ums-authentication-integration.md) | — | Integración AuthN/AuthZ con UMS SaaS: JWT, JWKS, permission guards |
| [UMS Authorization Graph Design](./reference/specs/design/tracker-ums-authorization-graph-design.md) | — | ACL transformation: UMS graph → Tracker permissions, role hierarchy |
| [Core Integration Design](./reference/specs/design/tracker-core-integration-design.md) | — | Integración con Evolith Core: rulesets, artifact schemas, taxonomy, caching |
diff --git a/MASTER_INDEX.md b/MASTER_INDEX.md
index 8e24410c..67bc3c84 100644
--- a/MASTER_INDEX.md
+++ b/MASTER_INDEX.md
@@ -37,7 +37,7 @@ Bienvenido a la documentación del **Evolith Tracker**, repositorio satélite de
| [Platform Implementation](./reference/specs/design/tracker-platform-implementation.md) | Reporte de implementación: Ondas 1-4 (Fundación, PostgreSQL, Gobernanza, Integración Core) |
| [NestJS Backend Design](./reference/specs/design/tracker-nestjs-backend-design.md) | Arquitectura NestJS: módulos, CQRS, repositories, guards, middleware |
| [React Frontend Design](./reference/specs/design/tracker-react-frontend-design.md) | Arquitectura React: state management, routing, permission-driven UI |
-| [PostgreSQL Data Design](./reference/specs/design/tracker-postgresql-data-design.md) | Diseño de datos: 10 schemas, DDL, RLS, indexes, concurrency, migrations |
+| [PostgreSQL Data Design](./reference/specs/design/tracker-postgresql-data-design.md) | Diseño de datos: mapa de schemas (derivado del código), DDL, RLS, indexes, concurrency, migrations |
| [UMS Authentication Integration](./reference/specs/design/tracker-ums-authentication-integration.md) | Integración AuthN/AuthZ con UMS SaaS: JWT, JWKS, permission guards |
| [UMS Authorization Graph Design](./reference/specs/design/tracker-ums-authorization-graph-design.md) | ACL transformation: UMS graph → Tracker permissions, role hierarchy |
| [Core Integration Design](./reference/specs/design/tracker-core-integration-design.md) | Integración con Evolith Core: rulesets, artifact schemas, taxonomy, caching |
@@ -73,7 +73,7 @@ Bienvenido a la documentación del **Evolith Tracker**, repositorio satélite de
| Documento | Descripción |
| :--- | :--- |
| [Bounded Context Map (oficial)](./reference/specs/architecture/bounded-context-map.md) | **Mapa estratégico** de los 9 contexts: relaciones, patrones DDD, flujo de domain events |
-| [Domain Model & E/R Overview](./reference/specs/architecture/tracker-domain-model-overview.md) | **Síntesis consolidada**: modelo conceptual, agregados, value objects y E/R de los 10 schemas |
+| [Domain Model & E/R Overview](./reference/specs/architecture/tracker-domain-model-overview.md) | **Síntesis consolidada**: modelo conceptual, agregados, value objects y E/R de los schemas reales |
| **Fase 0 — Strategic Intake** | |
| [Intake Specifications](./reference/specs/intake/README.md) | Intake aggregate structures for Gate 0 |
| [Phase 0 Intake Formats](./docs/artifacts/PHASE_0_INTAKE_FORMATS.md) | Manual UI templates and API JSON Schema contracts for Funnel 0 |
diff --git a/README.es.md b/README.es.md
index f53084e9..95e07662 100644
--- a/README.es.md
+++ b/README.es.md
@@ -7,7 +7,9 @@
[]()
[]()
[](https://github.com/beyondnetcode/evolith_arch32)
-[](./DECISIONS.md)
+
+[](./DECISIONS.md)
+
[](./LICENSE)
diff --git a/README.md b/README.md
index 70ccc77b..8ec7169b 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,9 @@
[]()
[]()
[](https://github.com/beyondnetcode/evolith_arch32)
-[](./DECISIONS.md)
+
+[](./DECISIONS.md)
+
[](./LICENSE)
diff --git a/product/infra/helm/evolith-tracker-api/templates/configmap.yaml b/product/infra/helm/evolith-tracker-api/templates/configmap.yaml
index 3c40bb4d..f097465a 100644
--- a/product/infra/helm/evolith-tracker-api/templates/configmap.yaml
+++ b/product/infra/helm/evolith-tracker-api/templates/configmap.yaml
@@ -20,3 +20,10 @@ data:
Authentication__Ums__Issuer: {{ .Values.auth.ums.issuer | quote }}
Authentication__Ums__RequireHttpsMetadata: {{ .Values.auth.ums.requireHttpsMetadata | quote }}
Cors__Origins__0: {{ .Values.cors.origins | quote }}
+ {{- if .Values.otlp.endpoint }}
+ # T-049 / GT-616 — solo se emite cuando hay colector. Emitirlo vacio seria equivalente a no
+ # emitirlo (la app hace retorno temprano), pero dejaria una clave en el configmap que sugiere
+ # que las trazas estan configuradas cuando no lo estan.
+ Otlp__Endpoint: {{ .Values.otlp.endpoint | quote }}
+ Otlp__ServiceName: {{ .Values.otlp.serviceName | quote }}
+ {{- end }}
diff --git a/product/infra/helm/evolith-tracker-api/values.yaml b/product/infra/helm/evolith-tracker-api/values.yaml
index 01e6badc..5138b637 100644
--- a/product/infra/helm/evolith-tracker-api/values.yaml
+++ b/product/infra/helm/evolith-tracker-api/values.yaml
@@ -74,6 +74,23 @@ auth:
cors:
origins: "http://localhost:4200"
+# T-049 / GT-616 — trazas distribuidas por push OTLP.
+#
+# `endpoint` VACIO = trazas apagadas, y ese sigue siendo el valor por defecto: `T-049` decidio no
+# entregarlas encendidas porque un exportador que no alcanza a nadie reintenta para siempre y ata
+# la disponibilidad del producto a la de un sumidero de telemetria.
+#
+# Lo que GT-616 arregla no es el valor por defecto sino que NO HABIA INTERRUPTOR: el configmap no
+# emitia `Otlp__Endpoint` en absoluto, asi que un operador CON colector no tenia forma soportada de
+# encenderlas — solo inyectarla a mano por `extraEnv`, que es exactamente el tipo de configuracion
+# no declarada que este chart evita en todo lo demas.
+#
+# Ponlo al colector del cluster para encenderlas, p. ej.:
+# otlp.endpoint=http://opentelemetry-collector.observability:4317
+otlp:
+ endpoint: ""
+ serviceName: evolith-tracker-api
+
# EF Core migrations: run as a pre-install/pre-upgrade Job using the embedded
# `dotnet ef migrations bundle` shipped in the image at /app/efbundle/efbundle.
migrations:
diff --git a/reference/specs/architecture/README.es.md b/reference/specs/architecture/README.es.md
index c3419a6a..ac0b2f01 100644
--- a/reference/specs/architecture/README.es.md
+++ b/reference/specs/architecture/README.es.md
@@ -21,7 +21,7 @@
| Documento | Descripción | Propósito | Tipo |
|---|---|---|---|
| [Bounded Context Map](./bounded-context-map.es.md) | Mapa estratégico oficial de los 9 contextos: cadena Customer-Supplier, Partnership, Conformist, ACLs y el flujo de eventos de dominio | Saber dónde termina la responsabilidad de cada contexto y quién depende de quién | DDD estratégico |
-| [Visión del Modelo de Dominio y E/R](./tracker-domain-model-overview.es.md) | Síntesis consolidada: modelo conceptual, agregados, value objects y diseño E/R de los 10 schemas | Punto de entrada único a todo el diseño de dominio sin leer 9 modelos separados | Síntesis |
+| [Visión del Modelo de Dominio y E/R](./tracker-domain-model-overview.es.md) | Síntesis consolidada: modelo conceptual, agregados, value objects y diseño E/R de los schemas reales | Punto de entrada único a todo el diseño de dominio sin leer 9 modelos separados | Síntesis |
diff --git a/reference/specs/architecture/README.md b/reference/specs/architecture/README.md
index bef74ac4..80adc86e 100644
--- a/reference/specs/architecture/README.md
+++ b/reference/specs/architecture/README.md
@@ -21,7 +21,7 @@
| Document | Description | Purpose | Type |
|---|---|---|---|
| [Bounded Context Map](./bounded-context-map.md) | Official strategic map of the 9 contexts: Customer-Supplier chain, Partnership, Conformist, ACLs, and the domain-event flow | Know where each context's responsibility ends and who depends on whom | Strategic DDD |
-| [Domain Model & E/R Overview](./tracker-domain-model-overview.md) | Consolidated synthesis: conceptual model, aggregates, value objects, and E/R design across the 10 schemas | Single entry point to the whole domain design without reading 9 separate models | Synthesis |
+| [Domain Model & E/R Overview](./tracker-domain-model-overview.md) | Consolidated synthesis: conceptual model, aggregates, value objects, and E/R design across the real schemas | Single entry point to the whole domain design without reading 9 separate models | Synthesis |
diff --git a/reference/specs/architecture/c4-macro-topology-phase1.es.md b/reference/specs/architecture/c4-macro-topology-phase1.es.md
index 909b2f7b..d7ea4a1f 100644
--- a/reference/specs/architecture/c4-macro-topology-phase1.es.md
+++ b/reference/specs/architecture/c4-macro-topology-phase1.es.md
@@ -113,7 +113,7 @@ C4Component
}
Container(api_gateway, "Governance API", "Nginx", "Llamadas entrantes (REST)")
- ContainerDb(single_db, "Relational Database", "PostgreSQL", "10 schemas aislados")
+ ContainerDb(single_db, "Relational Database", "PostgreSQL", "schema-per-context (T-047)")
Rel(api_gateway, discovery_module, "Ruta tráfico", "REST")
Rel(api_gateway, integration_module, "Ruta tráfico", "REST")
diff --git a/reference/specs/architecture/c4-macro-topology-phase1.md b/reference/specs/architecture/c4-macro-topology-phase1.md
index cf51a233..6bfdf481 100644
--- a/reference/specs/architecture/c4-macro-topology-phase1.md
+++ b/reference/specs/architecture/c4-macro-topology-phase1.md
@@ -113,7 +113,7 @@ C4Component
}
Container(api_gateway, "Governance API", "Nginx", "Llamadas entrantes (REST)")
- ContainerDb(single_db, "Relational Database", "PostgreSQL", "10 schemas aislados")
+ ContainerDb(single_db, "Relational Database", "PostgreSQL", "schema-per-context (T-047)")
Rel(api_gateway, discovery_module, "Ruta tráfico", "REST")
Rel(api_gateway, integration_module, "Ruta tráfico", "REST")
diff --git a/reference/specs/architecture/scale-out-strategy.es.md b/reference/specs/architecture/scale-out-strategy.es.md
index cecced3f..fb6324d6 100644
--- a/reference/specs/architecture/scale-out-strategy.es.md
+++ b/reference/specs/architecture/scale-out-strategy.es.md
@@ -6,7 +6,7 @@ Este documento detalla la ruta de migración desde el **Monolito de Despliegue
## 1. El Estado Inicial (Fase 1)
- **Topología:** Un solo servicio Backend corriendo en un solo pod/servidor.
-- **Base de Datos:** Un solo cluster físico segregado en 5 esquemas (`discovery_schema`, `qa_schema`, etc.).
+- **Base de Datos:** Un solo cluster físico segregado por schema-per-context (el mapa vigente lo deriva `.harness/scripts/doc-inventory.mjs` en `reference/specs/design/tracker-postgresql-data-design.md` §1.1).
- **Comunicación Interna:** Llamadas a interfaces a través del *In-Memory Event Bus* (CQRS nativo en memoria).
- **Frontend:** Microfrontends ya segregados por dominio (aprobado en T-002).
diff --git a/reference/specs/architecture/scale-out-strategy.md b/reference/specs/architecture/scale-out-strategy.md
index 0fd83026..c900cc7b 100644
--- a/reference/specs/architecture/scale-out-strategy.md
+++ b/reference/specs/architecture/scale-out-strategy.md
@@ -6,7 +6,7 @@ Este documento detalla la ruta de migración desde el **Monolito de Despliegue
## 1. El Estado Inicial (Fase 1)
- **Topología:** Un solo servicio Backend corriendo en un solo pod/servidor.
-- **Base de Datos:** Un solo cluster físico segregado en 5 esquemas (`discovery_schema`, `qa_schema`, etc.).
+- **Base de Datos:** Un solo cluster físico segregado por schema-per-context (the live map is derived by `.harness/scripts/doc-inventory.mjs` into `reference/specs/design/tracker-postgresql-data-design.md` §1.1).
- **Comunicación Interna:** Llamadas a interfaces a través del *In-Memory Event Bus* (CQRS nativo en memoria).
- **Frontend:** Microfrontends ya segregados por dominio (aprobado en T-002).
diff --git a/reference/specs/architecture/tracker-domain-model-overview.es.md b/reference/specs/architecture/tracker-domain-model-overview.es.md
index 439b20ef..0736e8b8 100644
--- a/reference/specs/architecture/tracker-domain-model-overview.es.md
+++ b/reference/specs/architecture/tracker-domain-model-overview.es.md
@@ -13,7 +13,7 @@
## 1. Propósito
-Este documento es la **vista única consolidada** del diseño de dominio de Evolith Tracker: el modelo conceptual, los 9 bounded contexts, sus agregados y value objects, y el diseño entidad-relación a través de los 10 schemas PostgreSQL (T-008).
+Este documento es la **vista única consolidada** del diseño de dominio de Evolith Tracker: el modelo conceptual, los 9 bounded contexts, sus agregados y value objects, y el diseño entidad-relación a través de los schemas PostgreSQL reales (`T-047`; el mapa vigente lo deriva del código `.harness/scripts/doc-inventory.mjs` y vive en `reference/specs/design/tracker-postgresql-data-design.es.md` §1.1).
Sintetiza — nunca reemplaza — las fuentes canónicas:
@@ -139,7 +139,7 @@ Toda tabla (según [PostgreSQL Data Design §2](../design/tracker-postgresql-dat
- Concurrencia optimista vía `xmin` (columna `version` explícita donde se requiera)
- **Sin foreign keys entre schemas.** Las referencias entre contextos son UUIDs validados en la capa de aplicación — preservando la autonomía de cada contexto y habilitando la extracción a servicios en Fase 2.
-### 6.2 Cadena Core (5 schemas de compuertas)
+### 6.2 Cadena Core (5 schemas de compuertas)
Líneas sólidas = FK intra-schema. Líneas punteadas = referencia UUID cross-schema (sin FK).
diff --git a/reference/specs/design/README.es.md b/reference/specs/design/README.es.md
index cbea41f9..9310dbee 100644
--- a/reference/specs/design/README.es.md
+++ b/reference/specs/design/README.es.md
@@ -32,7 +32,7 @@
| Documento | Descripción | Propósito | Tipo |
|---|---|---|---|
-| [Modelo de Dominio Consolidado y E/R](../architecture/tracker-domain-model-overview.es.md) | El diseño DDD único y consolidado: modelo conceptual propuesto, los 9 bounded contexts, agregados, value objects y el diseño E/R de los 10 schemas | Fuente única del diseño de dominio propuesto — el detalle táctico por contexto se navega desde aquí | DDD |
+| [Modelo de Dominio Consolidado y E/R](../architecture/tracker-domain-model-overview.es.md) | El diseño DDD único y consolidado: modelo conceptual propuesto, los 9 bounded contexts, agregados, value objects y el diseño E/R de los schemas | Fuente única del diseño de dominio propuesto — el detalle táctico por contexto se navega desde aquí | DDD |
@@ -46,7 +46,7 @@
| [Arquitectura Objetivo (TAD)](./tracker-target-architecture.es.md) | Arquitectura técnica maestra: bounded contexts, modelo de dominio, CQRS, capas, despliegue | Referencia única para entender cómo se construye toda la aplicación Tracker | Arquitectura |
| [Diseño Backend NestJS](./tracker-nestjs-backend-design.es.md) | Blueprint del backend: módulos, buses CQRS, repositorios, guards, middleware | Guiar a los desarrolladores backend al implementar cualquier bounded context | Backend |
| [Diseño Frontend React](./tracker-react-frontend-design.es.md) | Blueprint del frontend: shell Module Federation + 7 remotes, estado Zustand/TanStack, routing | Guiar a los desarrolladores frontend al construir o extender un microfrontend | Frontend |
-| [Diseño de Datos PostgreSQL](./tracker-postgresql-data-design.es.md) | Diseño físico de datos: 10 schemas, DDL completo, políticas RLS, índices, control de concurrencia | Implementar o migrar la base de datos exactamente como fue aprobada | Datos |
+| [Diseño de Datos PostgreSQL](./tracker-postgresql-data-design.es.md) | Diseño físico de datos: mapa de schemas derivado, DDL completo, políticas RLS, índices, control de concurrencia | Implementar o migrar la base de datos exactamente como fue aprobada | Datos |
diff --git a/reference/specs/design/README.md b/reference/specs/design/README.md
index 0323af35..c3e978ab 100644
--- a/reference/specs/design/README.md
+++ b/reference/specs/design/README.md
@@ -32,7 +32,7 @@
| Document | Description | Purpose | Type |
|---|---|---|---|
-| [Consolidated Domain Model & E/R](../architecture/tracker-domain-model-overview.md) | The single consolidated DDD design: proposed conceptual model, the 9 bounded contexts, aggregates, value objects, and the E/R design across the 10 schemas | Single source for the proposed domain design — per-context tactical detail is navigated from here | DDD |
+| [Consolidated Domain Model & E/R](../architecture/tracker-domain-model-overview.md) | The single consolidated DDD design: proposed conceptual model, the 9 bounded contexts, aggregates, value objects, and the E/R design across the schemas | Single source for the proposed domain design — per-context tactical detail is navigated from here | DDD |
@@ -46,7 +46,7 @@
| [Target Architecture (TAD)](./tracker-target-architecture.md) | Master technical architecture: bounded contexts, domain model, CQRS, layers, deployment | Single reference to understand how the whole Tracker application is built | Architecture |
| [NestJS Backend Design](./tracker-nestjs-backend-design.md) | Backend blueprint: module layout, CQRS buses, repositories, guards, middleware | Guide backend developers implementing any bounded context | Backend |
| [React Frontend Design](./tracker-react-frontend-design.md) | Frontend blueprint: Module Federation shell + 7 remotes, Zustand/TanStack state, routing | Guide frontend developers building or extending a microfrontend | Frontend |
-| [PostgreSQL Data Design](./tracker-postgresql-data-design.md) | Physical data design: 10 schemas, full DDL, RLS policies, indexes, concurrency control | Implement or migrate the database exactly as approved | Data |
+| [PostgreSQL Data Design](./tracker-postgresql-data-design.md) | Physical data design: derived schema map, full DDL, RLS policies, indexes, concurrency control | Implement or migrate the database exactly as approved | Data |
diff --git a/reference/specs/design/tracker-implementation-roadmap.es.md b/reference/specs/design/tracker-implementation-roadmap.es.md
index ee5570cf..494930c8 100644
--- a/reference/specs/design/tracker-implementation-roadmap.es.md
+++ b/reference/specs/design/tracker-implementation-roadmap.es.md
@@ -69,7 +69,7 @@ Establecer el monorepo, pipeline CI/CD, primitivas de dominio compartidas e infr
- [ ] Bootstrap de app API NestJS con TypeScript strict mode
- [ ] App Web React con Vite + TanStack Query + Zustand
- [ ] `libs/domain` compartido con clases base (`AggregateRoot`, `BaseDomainEvent`, `Result`)
-- [ ] 10 esquemas PostgreSQL creados con migraciones DDL
+- [ ] Esquemas PostgreSQL creados con migraciones DDL (el mapa vigente lo deriva `.harness/scripts/doc-inventory.mjs` en `reference/specs/design/tracker-postgresql-data-design.md` §1.1)
- [ ] Políticas RLS configuradas para multi-tenancy
- [ ] Pipeline CI GitHub Actions (lint, typecheck, test)
- [ ] Docker Compose para desarrollo local
diff --git a/reference/specs/design/tracker-platform-implementation.es.md b/reference/specs/design/tracker-platform-implementation.es.md
index 8a2bd426..7dce9a36 100644
--- a/reference/specs/design/tracker-platform-implementation.es.md
+++ b/reference/specs/design/tracker-platform-implementation.es.md
@@ -109,7 +109,7 @@ Se crearon 7 archivos de migración en `src/apps/tracker-api/src/migrations/`:
| Migración | Schemas/Tablas | Características Clave |
|-----------|---------------|----------------------|
-| `0001-create-schemas.sql` | 10 schemas | `tracker_discovery`, `tracker_design`, `tracker_construction`, `tracker_qa`, `tracker_release`, `tracker_governance`, `tracker_artifacts`, `tracker_metrics`, `tracker_integration`, `tracker_audit` |
+| `0001-create-schemas.sql` | schemas | `tracker_discovery`, `tracker_design`, `tracker_construction`, `tracker_qa`, `tracker_release`, `tracker_governance`, `tracker_artifacts`, `tracker_metrics`, `tracker_integration`, `tracker_audit` |
| `0002-create-governance-tables.sql` | 5 tablas | `satellite_products`, `sdlc_executions`, `phase_gate_states`, `exception_requests`, `outbox_messages` |
| `0003-create-artifacts-tables.sql` | 3 tablas | `artifact_definitions`, `artifact_instances`, `evidence_records` |
| `0004-create-integration-tables.sql` | 4 tablas | `core_evaluation_transactions`, `integration_endpoints`, `sync_records`, `acl_adapters` |
diff --git a/reference/specs/design/tracker-platform-implementation.md b/reference/specs/design/tracker-platform-implementation.md
index 4bc8bfbe..b9c38e0f 100644
--- a/reference/specs/design/tracker-platform-implementation.md
+++ b/reference/specs/design/tracker-platform-implementation.md
@@ -109,7 +109,7 @@ Created 7 migration files in `src/apps/tracker-api/src/migrations/`:
| Migration | Schemas/Tables | Key Features |
|-----------|---------------|--------------|
-| `0001-create-schemas.sql` | 10 schemas | `tracker_discovery`, `tracker_design`, `tracker_construction`, `tracker_qa`, `tracker_release`, `tracker_governance`, `tracker_artifacts`, `tracker_metrics`, `tracker_integration`, `tracker_audit` |
+| `0001-create-schemas.sql` | schemas | `tracker_discovery`, `tracker_design`, `tracker_construction`, `tracker_qa`, `tracker_release`, `tracker_governance`, `tracker_artifacts`, `tracker_metrics`, `tracker_integration`, `tracker_audit` |
| `0002-create-governance-tables.sql` | 5 tables | `satellite_products`, `sdlc_executions`, `phase_gate_states`, `exception_requests`, `outbox_messages` |
| `0003-create-artifacts-tables.sql` | 3 tables | `artifact_definitions`, `artifact_instances`, `evidence_records` |
| `0004-create-integration-tables.sql` | 4 tables | `core_evaluation_transactions`, `integration_endpoints`, `sync_records`, `acl_adapters` |
diff --git a/reference/specs/design/tracker-postgresql-data-design.es.md b/reference/specs/design/tracker-postgresql-data-design.es.md
index 9ddb390c..afcf316f 100644
--- a/reference/specs/design/tracker-postgresql-data-design.es.md
+++ b/reference/specs/design/tracker-postgresql-data-design.es.md
@@ -16,18 +16,31 @@
### 1.1 Mapa de Schemas
-| Schema | Bounded Context | Módulo Propietario | Propósito |
-|--------|----------------|--------------|---------|
-| `tracker_discovery` | Discovery | `@evolith/tracker-discovery` | Aggregates Initiative, Backlog, Epic, UserStory |
-| `tracker_design` | Design | `@evolith/tracker-design` | TechnicalBlueprint, Contract, ADR, DataSchema |
-| `tracker_construction` | Construction | `@evolith/tracker-construction` | ImplementationCycle, TechnicalStory, PeerReview |
-| `tracker_qa` | QA | `@evolith/tracker-qa` | TestCycle, TestExecution, Defect |
-| `tracker_release` | Release | `@evolith/tracker-release` | ReleasePackage, DeploymentRecord, Environment |
-| `tracker_governance` | Governance | `@evolith/tracker-governance` | SatelliteProduct, SDLCExecution, PhaseGateState, ExceptionRequest |
-| `tracker_artifacts` | Artifacts | `@evolith/tracker-artifacts` | ArtifactDefinition, ArtifactInstance, EvidenceRecord |
-| `tracker_metrics` | Metrics | `@evolith/tracker-metrics` | ScorecardDefinition, MetricSnapshot, DriftAlert |
-| `tracker_integration` | Integration | `@evolith/tracker-integration` | IntegrationEndpoint, SyncRecord, ACLAdapter |
-| `tracker_audit` | Audit | `@evolith/tracker-audit` | AuditEntry (append-only) |
+
+**8 schemas · 49 tablas.** Derivado de los `ModelSnapshot` de EF Core, que son lo que la base tiene de verdad.
+
+| Schema | Contexto EF | Tablas | Tablas (derivadas del snapshot de EF) |
+|--------|------------|--------|---------------------------------------|
+| `masterdata` | `TenantProjectionDbContext` | 4 | `InboxState`, `OutboxMessage`, `OutboxState`, `tenant_projection` |
+| `tracker_construction` | `TrackerDbContext` | 1 | `peer_reviews` |
+| `tracker_geo` | `TrackerDbContext` | 13 | `catalog_release`, `country`, `currency`, `exchange_rate`, `language`, `locale_profile`, `location`, `location_code`, `location_i18n`, `location_level`, `tax_rate`, `tax_scheme`, `time_zone` |
+| `tracker_governance` | `TrackerDbContext` | 21 | `artifact_field_schemas`, `audit_entries`, `core_evaluation_transactions`, `evidence_records`, `exception_requests`, `gate_decisions`, `gate_policies`, `gate_submissions`, `initiatives`, `opportunities`, `phase_artifacts`, `phase_gate_states`, `phase_progressions`, `products`, `provider_connections`, `publication_outbox`, `runtime_approvals`, `sdlc_executions`, `technical_blueprints`, `tenant_intelligence`, `tenants` |
+| `tracker_intake` | `TrackerDbContext` | 5 | `intake_dead_letter`, `intake_outbox`, `ppm_endpoints`, `strategic_baselines`, `strategic_initiatives` |
+| `tracker_metrics` | `TrackerDbContext` | 1 | `phase_sla_policies` |
+| `tracker_qa` | `TrackerDbContext` | 3 | `defects`, `test_cycles`, `test_executions` |
+| `tracker_release` | `TrackerDbContext` | 1 | `deployment_records` |
+
+
+> **Qué es el resto de este documento (GT-617).** El mapa de arriba lo DERIVA de los
+> `ModelSnapshot` de EF Core el script `.harness/scripts/doc-inventory.mjs` en cada corrida;
+> es la topología embarcada, ratificada por
+> [`T-047`](../../../docs/adrs/T-047-consolidated-schema-topology.es.md). Las **secciones 3–12
+> son el diseño OBJETIVO original del 2026-06-07** y se conservan sólo por trazabilidad: varios
+> de los schemas que detallan (`tracker_discovery`, `tracker_design`, `tracker_artifacts`,
+> `tracker_integration`, `tracker_audit`) nunca se crearon, y sus agregados viven en
+> `tracker_governance`. Léelas como intención, jamás como descripción de la base. El bloque
+> generado es la única parte de este fichero que describe la realidad, y la única que mantiene
+> un script.
### 1.2 Estrategia de Referencia Cross-Schema
diff --git a/reference/specs/design/tracker-postgresql-data-design.md b/reference/specs/design/tracker-postgresql-data-design.md
index 0b6f8209..6164e00c 100644
--- a/reference/specs/design/tracker-postgresql-data-design.md
+++ b/reference/specs/design/tracker-postgresql-data-design.md
@@ -16,18 +16,30 @@
### 1.1 Schema Map
-| Schema | Bounded Context | Owner Module | Purpose |
-|--------|----------------|--------------|---------|
-| `tracker_discovery` | Discovery | `@evolith/tracker-discovery` | Initiative, Backlog, Epic, UserStory aggregates |
-| `tracker_design` | Design | `@evolith/tracker-design` | TechnicalBlueprint, Contract, ADR, DataSchema |
-| `tracker_construction` | Construction | `@evolith/tracker-construction` | ImplementationCycle, TechnicalStory, PeerReview |
-| `tracker_qa` | QA | `@evolith/tracker-qa` | TestCycle, TestExecution, Defect |
-| `tracker_release` | Release | `@evolith/tracker-release` | ReleasePackage, DeploymentRecord, Environment |
-| `tracker_governance` | Governance | `@evolith/tracker-governance` | SatelliteProduct, SDLCExecution, PhaseGateState, ExceptionRequest |
-| `tracker_artifacts` | Artifacts | `@evolith/tracker-artifacts` | ArtifactDefinition, ArtifactInstance, EvidenceRecord |
-| `tracker_metrics` | Metrics | `@evolith/tracker-metrics` | ScorecardDefinition, MetricSnapshot, DriftAlert |
-| `tracker_integration` | Integration | `@evolith/tracker-integration` | IntegrationEndpoint, SyncRecord, ACLAdapter |
-| `tracker_audit` | Audit | `@evolith/tracker-audit` | AuditEntry (append-only) |
+
+**8 schemas · 49 tables.** Derived from the EF Core `ModelSnapshot` files, which are what the database actually has.
+
+| Schema | EF context | Tables | Tables (derived from the EF snapshot) |
+|--------|------------|--------|---------------------------------------|
+| `masterdata` | `TenantProjectionDbContext` | 4 | `InboxState`, `OutboxMessage`, `OutboxState`, `tenant_projection` |
+| `tracker_construction` | `TrackerDbContext` | 1 | `peer_reviews` |
+| `tracker_geo` | `TrackerDbContext` | 13 | `catalog_release`, `country`, `currency`, `exchange_rate`, `language`, `locale_profile`, `location`, `location_code`, `location_i18n`, `location_level`, `tax_rate`, `tax_scheme`, `time_zone` |
+| `tracker_governance` | `TrackerDbContext` | 21 | `artifact_field_schemas`, `audit_entries`, `core_evaluation_transactions`, `evidence_records`, `exception_requests`, `gate_decisions`, `gate_policies`, `gate_submissions`, `initiatives`, `opportunities`, `phase_artifacts`, `phase_gate_states`, `phase_progressions`, `products`, `provider_connections`, `publication_outbox`, `runtime_approvals`, `sdlc_executions`, `technical_blueprints`, `tenant_intelligence`, `tenants` |
+| `tracker_intake` | `TrackerDbContext` | 5 | `intake_dead_letter`, `intake_outbox`, `ppm_endpoints`, `strategic_baselines`, `strategic_initiatives` |
+| `tracker_metrics` | `TrackerDbContext` | 1 | `phase_sla_policies` |
+| `tracker_qa` | `TrackerDbContext` | 3 | `defects`, `test_cycles`, `test_executions` |
+| `tracker_release` | `TrackerDbContext` | 1 | `deployment_records` |
+
+
+> **What the rest of this document is (GT-617).** The map above is derived from the EF Core
+> model snapshots on every run of `.harness/scripts/doc-inventory.mjs`; it is the shipped
+> topology, ratified by [`T-047`](../../../docs/adrs/T-047-consolidated-schema-topology.md).
+> Sections **3–12 below are the original 2026-06-07 TARGET design** and are kept for
+> traceability only — several of the schemas they detail (`tracker_discovery`,
+> `tracker_design`, `tracker_artifacts`, `tracker_integration`, `tracker_audit`) were never
+> created, and their aggregates live in `tracker_governance` instead. Read them as intent,
+> never as a description of the database. The generated block is the only part of this file
+> that describes reality, and it is the only part a script maintains.
### 1.2 Cross-Schema Reference Strategy
diff --git a/reference/specs/design/tracker-target-architecture.es.md b/reference/specs/design/tracker-target-architecture.es.md
index 20d2ccf2..af0b5820 100644
--- a/reference/specs/design/tracker-target-architecture.es.md
+++ b/reference/specs/design/tracker-target-architecture.es.md
@@ -951,7 +951,7 @@ ingress:
| Re-Do Flow | PRD UC-005, Release DDD | Regla de contingencia | `ReDoFlowEngine` |
| Generación de evidencia | PRD Estándar de artefactos | Spec de artefacto de evidencia | Aggregate `EvidenceRecord` |
| Sincronización con Core | PRD Governance | Versionado de Core | `CoreIntegrationService` |
-| PostgreSQL multi-schema | C4 Topology, este documento | Schema-por-dominio | 10 schemas PostgreSQL |
+| PostgreSQL multi-schema | C4 Topology, este documento | Schema-por-dominio | Schemas PostgreSQL (el mapa vigente lo deriva `.harness/scripts/doc-inventory.mjs` en `reference/specs/design/tracker-postgresql-data-design.md` §1.1) |
| OpenAPI contract-first | PRD EPIC-001 | Estándar de API | Specs OpenAPI 3.0 por módulo |
---
diff --git a/reference/specs/infrastructure/tracker-cicd-spec.es.md b/reference/specs/infrastructure/tracker-cicd-spec.es.md
index dc0f033e..a86d8bbf 100644
--- a/reference/specs/infrastructure/tracker-cicd-spec.es.md
+++ b/reference/specs/infrastructure/tracker-cicd-spec.es.md
@@ -127,7 +127,7 @@ Per [TAD §27](../design/tracker-target-architecture.md), el deployment apunta a
|------------|------------|
| `tracker-api` | Monolito NestJS; ≥2 réplicas; HPA en CPU/latencia ⊕ |
| `tracker-web` | Shell Host + remotes (estático, servido vía CDN/ingress) |
-| PostgreSQL | Instancia gestionada (cloud) o Helm subchart (non-prod); 10 schemas `tracker_*` |
+| PostgreSQL | Instancia gestionada (cloud) o Helm subchart (non-prod); schemas `tracker_*` |
| Redis ⊕ | Solo si se confirma el requisito de caching de Core-ruleset (GAP-021) |
| OTel Collector | Sidecar/daemonset para traces (TAD §24) |
| Ingress | TLS (cert-manager), security headers ⊕ (SEC-D6) |
diff --git a/robosoft/README.md b/robosoft/README.md
index b49b474b..deaf5b7f 100644
--- a/robosoft/README.md
+++ b/robosoft/README.md
@@ -59,11 +59,24 @@ fails the run; a failed **soft** check is a warning only.
## Robots
+
+**12 robots.** Derived from `robosoft/robots/*.robot.mjs` — the same discovery `robosoft/run.mjs` performs, so this table cannot disagree with what a run executes.
+
| Robot | Proves |
|-------|--------|
-| `core-integration` | Every Tracker↔Core interface: capabilities manifest, rulesets, MCP tools list + call, a real advisory `evaluate` round-trip, and `/assistant/converse`. |
-| _(next)_ `governance-journey` | A tenant drives the full SDLC funnel (opportunity → discovery → design → construction → qa → release), each gate asserted, with a real Core verdict at the Design gate. Will reuse the flow proven in `seed/e2e-governance.mjs`. |
-| _(next)_ `provider-connections` | Register / connect / publish a ProviderConnection and assert the outbound custom-check delivery. |
+| `audit-trail` | Assert governed actions are recorded, attributed to the authenticated actor, and tenant-scoped (BR-009). |
+| `auth-perimeter` | Prove the CLOSED (ums) perimeter enforces: 401 anonymous, 2xx valid, 403 under-privileged, 401 branch/forged. |
+| `core-integration` | Assert every Tracker↔Core interface against the live stack (REST + MCP + /converse). |
+| `exception-governance` | Drive the exception-request lifecycle (submit→pending→decide→approved) and assert authority + audit (CD-20/21). |
+| `gate-enforcement` | Prove the discovery gate REFUSES to approve until evidence AND approval are present. |
+| `governance-journey` | Drive one tenant initiative through the full SDLC funnel, asserting every gate. |
+| `intake` | Drive the pre-discovery intake funnel (handshake → feasibility → promote) and assert each transition. |
+| `provider-connections` | Drive the ProviderConnection lifecycle (register→secret→acl→connect→disconnect) and assert its state machine. |
+| `qa-quality-gate` | Prove the QA gate stops authorizing while a blocking defect is open, and clears once it is closed. |
+| `runtime-approvals` | Drive the CD-23 HITL flow: machine submits, only an authorized human may approve, rejection is recorded. |
+| `scorecard` | Validate the executive scorecard surface: phase-SLA round-trip + a well-formed per-initiative lamina (GT-469). |
+| `tenant-isolation` | Two tenant personas: assert tenant B never sees or mutates tenant A's data (BR-006). |
+
## Running against the live stack
diff --git a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Governance/GateDecisionEndpoints.cs b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Governance/GateDecisionEndpoints.cs
index 76e39ea7..ac58f688 100644
--- a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Governance/GateDecisionEndpoints.cs
+++ b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Governance/GateDecisionEndpoints.cs
@@ -3,6 +3,7 @@
using Tracker.Application.Governance.GateDecision.DTOs;
using Tracker.Application.Governance.GateDecision.Queries.GetGateDecision;
using Tracker.Domain.Governance.GateDecision;
+using Tracker.Presentation.Observability;
namespace Tracker.Presentation.Endpoints.Governance;
@@ -74,6 +75,14 @@ public static void MapGateDecisionEndpoints(this IEndpointRouteBuilder app)
statusCode: StatusCodes.Status403Forbidden);
}
+ // GT-616 — el span de dominio. Decidir una compuerta es EL acto de gobierno de este
+ // producto: quien lo investiga despues quiere filtrar por inquilino, actor y desenlace,
+ // no por latencia de ruta. Sin listener esto es `null` y no cuesta nada.
+ using var traza = TrackerTracing.StartGovernanceActivity(
+ "governance.gate_decision.decide",
+ user.TenantId,
+ user.ActorId);
+
var result = await mediator.Send(new DecideGateDecisionCommand(
user.TenantId,
id,
@@ -88,6 +97,8 @@ public static void MapGateDecisionEndpoints(this IEndpointRouteBuilder app)
// CD-21: las autoridades del actor son lo que la `ExceptionPolicy` del gate
// contrasta. Sin esto la politica no tenia contra que decidir.
user.Authorities.ToList()), ct);
+
+ traza.SetGovernanceOutcome(result.IsSuccess ? request.Status : "rejected_by_policy");
return result.ToNoContent();
})
.RequireTrackerPermission(TrackerPermissions.GateDecisionDecide)
diff --git a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/AssistantEndpoints.cs b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/AssistantEndpoints.cs
index bf5f8b33..d9719a15 100644
--- a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/AssistantEndpoints.cs
+++ b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/AssistantEndpoints.cs
@@ -1,3 +1,5 @@
+using Tracker.Presentation.Observability;
+
namespace Tracker.Presentation.Endpoints.Integration;
///
@@ -18,12 +20,31 @@ public static void MapAssistantEndpoints(this IEndpointRouteBuilder app)
IAgentRuntimeGateway gateway,
CancellationToken ct) =>
{
+ // GT-616 — el turno de agente es el tercer punto que una traza tiene que poder
+ // responder: QUE agente actuo, sobre que iniciativa y en que fase. `agent-runtime` es
+ // el nombre del componente que ejecuta el turno; el motor concreto es deliberadamente
+ // opaco para el Tracker (la superficie es engine-agnostica, ver el resumen de arriba).
+ using var traza = TrackerTracing.StartGovernanceActivity(
+ "governance.assistant.converse",
+ user.TenantId,
+ user.ActorId,
+ phase: request.Context?.Phase,
+ agent: "agent-runtime");
+
+ if (!string.IsNullOrWhiteSpace(request.Context?.InitiativeId))
+ {
+ traza?.SetTag(GovernanceTraceTags.InitiativeId, request.Context.InitiativeId);
+ }
+
try
{
- return Results.Ok(await gateway.ConverseAsync(request, user, ct));
+ var respuesta = await gateway.ConverseAsync(request, user, ct);
+ traza.SetGovernanceOutcome("ok");
+ return Results.Ok(respuesta);
}
catch (AgentRuntimeGatewayException ex)
{
+ traza.SetGovernanceOutcome(ex.Code);
return Results.Json(
new { code = ex.Code, message = ex.Message },
statusCode: ex.StatusCode);
diff --git a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/CoreEvaluationEndpoints.cs b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/CoreEvaluationEndpoints.cs
index ec2d084e..aa67a6fe 100644
--- a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/CoreEvaluationEndpoints.cs
+++ b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Integration/CoreEvaluationEndpoints.cs
@@ -1,4 +1,5 @@
using Tracker.Domain.Integration.CoreEvaluationTransaction;
+using Tracker.Presentation.Observability;
namespace Tracker.Presentation.Endpoints.Integration;
@@ -17,8 +18,27 @@ public static void MapCoreEvaluationEndpoints(this IEndpointRouteBuilder app)
IUnitOfWork unitOfWork,
CancellationToken ct) =>
{
+ // GT-616 — span de dominio sobre la evaluacion del Core. Es la unica operacion del
+ // Tracker que cruza la frontera hacia otro producto, asi que es la que mas se investiga
+ // cuando algo no cuadra; el span de `HttpClient` que ya generaba la instrumentacion
+ // dice la URL y el codigo, y no dice ni el inquilino ni la fase ni el veredicto.
+ using var traza = TrackerTracing.StartGovernanceActivity(
+ "governance.core.evaluate",
+ user.TenantId,
+ user.ActorId,
+ phase: request.Phase);
+
+ // `InitiativeId` viaja como cadena en este contrato (no es un Guid del Tracker sino la
+ // referencia que el Core recibe), asi que se etiqueta aparte del ayudante tipado.
+ if (!string.IsNullOrWhiteSpace(request.InitiativeId))
+ {
+ traza?.SetTag(GovernanceTraceTags.InitiativeId, request.InitiativeId);
+ }
+
var result = await gateway.EvaluateAsync(request, user, ct);
+ traza.SetGovernanceOutcome(result.Status == "FAILED" ? "failed" : result.ResultDecision);
+
// Persist the transaction as the Tracker's own audit trail (the Core is
// stateless; the Tracker owns state).
var transaction = CoreEvaluationTransaction.Create(
diff --git a/src/apps/tracker-api/Tracker.Presentation/Observability/TrackerTracing.cs b/src/apps/tracker-api/Tracker.Presentation/Observability/TrackerTracing.cs
index 69c2525a..f7671f20 100644
--- a/src/apps/tracker-api/Tracker.Presentation/Observability/TrackerTracing.cs
+++ b/src/apps/tracker-api/Tracker.Presentation/Observability/TrackerTracing.cs
@@ -25,9 +25,14 @@ public sealed class TracingOptions
/// Endpoint del colector OTLP. **Vacío = trazas desactivadas**, y ése es el valor por defecto.
///
/// Entregarlo encendido haría que cada despliegue intentara alcanzar un colector que en
- /// la mayoría de entornos no existe — fabricando justo el acoplamiento que `T-049` rechaza.
- /// `.env.example` ya declaraba `OTEL_EXPORTER_OTLP_ENDPOINT` sin ningún consumidor; ésta es la
- /// variable que por fin lo tiene.
+ /// la mayoría de entornos no existe — fabricando justo el acoplamiento que `T-049` rechaza.
+ ///
+ /// GT-616. El comentario anterior afirmaba que `OTEL_EXPORTER_OTLP_ENDPOINT` —la
+ /// variable que `.env.example:40` lleva declarando desde siempre— era «la variable que por fin
+ /// tiene consumidor». No lo era: el enlace de configuración de ASP.NET traduce `Otlp__Endpoint`,
+ /// no `OTEL_EXPORTER_OTLP_ENDPOINT`, así que ponerla no encendía nada y el retorno temprano
+ /// seguía disparando. La resuelve ahora , que la
+ /// acepta como respaldo — es la convención de OpenTelemetry y ya estaba escrita en el `.env`.
///
public string? Endpoint { get; set; }
@@ -35,6 +40,31 @@ public sealed class TracingOptions
public string ServiceName { get; set; } = "evolith-tracker-api";
}
+///
+/// GT-616 — los atributos que hacen que una traza responda una pregunta de GOBIERNO.
+///
+/// Antes de esta ficha el Tracker registraba una
+/// propia y no la usaba NUNCA: no había una sola llamada a StartActivity en todo el
+/// repositorio (verificado por búsqueda). Con endpoint configurado el backend habría recibido sólo
+/// spans de ASP.NET y de HttpClient — «`POST /api/v1/gate-decisions/{id}/decide` tardó
+/// 180 ms». Eso es fontanería. La pregunta que este producto necesita poder hacerle a una traza es
+/// «¿qué inquilino decidió qué compuerta, de qué iniciativa, con qué desenlace, y quién —humano o
+/// agente— la empujó?».
+///
+/// Los nombres viven en constantes y no en literales sueltos porque un atributo mal escrito
+/// no falla: se ingiere igual y crea una dimensión huérfana que nadie consulta. La misma razón por
+/// la que el prefijo `tracker_` de las métricas tiene guarda.
+///
+public static class GovernanceTraceTags
+{
+ public const string TenantId = "evolith.tenant.id";
+ public const string ActorId = "evolith.actor.id";
+ public const string InitiativeId = "evolith.initiative.id";
+ public const string Phase = "evolith.sdlc.phase";
+ public const string Agent = "evolith.agent.name";
+ public const string Outcome = "evolith.governance.outcome";
+}
+
public static class TrackerTracing
{
///
@@ -43,16 +73,53 @@ public static class TrackerTracing
///
public static readonly System.Diagnostics.ActivitySource Source = new("Evolith.Tracker");
+ ///
+ /// GT-616 — nombre de la variable de entorno estandar de OpenTelemetry, aceptada como respaldo.
+ ///
+ public const string OtelEndpointEnvVar = "OTEL_EXPORTER_OTLP_ENDPOINT";
+
+ ///
+ /// Resuelve el endpoint del colector a partir de las DOS fuentes que un operador puede razonar:
+ /// la seccion `Otlp:Endpoint` (que el chart rellena) y la variable estandar
+ /// `OTEL_EXPORTER_OTLP_ENDPOINT` (que `.env.example` ya declaraba). La seccion gana: es la
+ /// especifica del despliegue.
+ ///
+ /// Antes solo existia la primera, de modo que un operador que ponia la variable estandar
+ /// —la unica documentada en el `.env`— obtenia silencio. Un interruptor que existe, esta
+ /// documentado y no hace nada es peor que no tenerlo: consume el presupuesto de diagnostico de
+ /// quien lo prueba.
+ ///
+ internal static string? ResolveEndpoint(TracingOptions options, IConfiguration configuration)
+ {
+ if (!string.IsNullOrWhiteSpace(options.Endpoint))
+ {
+ return options.Endpoint.Trim();
+ }
+
+ // Se lee de la CONFIGURACION, no de `Environment.GetEnvironmentVariable`: el host ya monta
+ // `AddEnvironmentVariables()`, asi que la variable llega igual, y de este modo la resolucion
+ // no depende de estado ambiente del proceso — que en una suite de pruebas es una fuente de
+ // verdes y rojos que no reproducen.
+ var deEntorno = configuration[OtelEndpointEnvVar];
+
+ return string.IsNullOrWhiteSpace(deEntorno) ? null : deEntorno.Trim();
+ }
+
public static IServiceCollection AddTrackerTracing(
this IServiceCollection services, IConfiguration configuration)
{
var options = new TracingOptions();
configuration.GetSection(TracingOptions.SectionName).Bind(options);
+ var endpoint = ResolveEndpoint(options, configuration);
+
// Sin endpoint no se registra NADA. No es una optimizacion: registrar el pipeline con un
// exportador que no puede exportar produce reintentos y ruido de fondo permanente en
- // entornos donde nadie pidio trazas.
- if (string.IsNullOrWhiteSpace(options.Endpoint))
+ // entornos donde nadie pidio trazas. `T-049` lo ratifico y esta ficha NO lo revierte: lo
+ // que GT-616 arregla es que (a) el chart no ofrecia ninguna forma soportada de encenderlo,
+ // (b) la variable de entorno documentada no lo encendia, y (c) encendido no habia un solo
+ // span de dominio que mirar.
+ if (string.IsNullOrWhiteSpace(endpoint))
{
return services;
}
@@ -75,8 +142,75 @@ public static IServiceCollection AddTrackerTracing(
};
})
.AddHttpClientInstrumentation()
- .AddOtlpExporter(o => o.Endpoint = new Uri(options.Endpoint)));
+ .AddOtlpExporter(o => o.Endpoint = new Uri(endpoint)));
return services;
}
+
+ ///
+ /// GT-616 — abre un span de DOMINIO alrededor de una operación de gobierno, etiquetado con lo
+ /// que hace falta para responder una pregunta de gobierno y no una de fontanería.
+ ///
+ /// Devuelve null cuando no hay nadie escuchando (trazas apagadas), que es el caso
+ /// por defecto y no cuesta nada:
+ /// no asigna nada sin listeners. Por eso los puntos de llamada pueden instrumentarse
+ /// incondicionalmente en vez de esconderse tras un if que alguien olvidaría de mantener.
+ ///
+ /// El identificador de inquilino se etiqueta SIEMPRE: sin él una traza no se puede atribuir
+ /// y, en un producto multi-inquilino, una traza no atribuible es a la vez inútil y un riesgo —
+ /// mezcla en el mismo panel actividad de clientes distintos.
+ ///
+ public static System.Diagnostics.Activity? StartGovernanceActivity(
+ string nombre,
+ Guid tenantId,
+ Guid? actorId = null,
+ Guid? initiativeId = null,
+ string? phase = null,
+ string? agent = null)
+ {
+ var actividad = Source.StartActivity(nombre, System.Diagnostics.ActivityKind.Internal);
+ if (actividad is null)
+ {
+ return null;
+ }
+
+ actividad.SetTag(GovernanceTraceTags.TenantId, tenantId.ToString());
+
+ if (actorId is { } actor && actor != Guid.Empty)
+ {
+ actividad.SetTag(GovernanceTraceTags.ActorId, actor.ToString());
+ }
+
+ if (initiativeId is { } iniciativa && iniciativa != Guid.Empty)
+ {
+ actividad.SetTag(GovernanceTraceTags.InitiativeId, iniciativa.ToString());
+ }
+
+ if (!string.IsNullOrWhiteSpace(phase))
+ {
+ actividad.SetTag(GovernanceTraceTags.Phase, phase);
+ }
+
+ if (!string.IsNullOrWhiteSpace(agent))
+ {
+ actividad.SetTag(GovernanceTraceTags.Agent, agent);
+ }
+
+ return actividad;
+ }
+
+ ///
+ /// Sella el DESENLACE de la operación gobernada (aprobado, rechazado, eximido, el veredicto que
+ /// devolvió el Core...). Va aparte de porque el desenlace
+ /// no se conoce al abrir el span, y es justo el atributo por el que se filtra al investigar.
+ ///
+ public static void SetGovernanceOutcome(this System.Diagnostics.Activity? actividad, string? desenlace)
+ {
+ if (actividad is null || string.IsNullOrWhiteSpace(desenlace))
+ {
+ return;
+ }
+
+ actividad.SetTag(GovernanceTraceTags.Outcome, desenlace);
+ }
}
diff --git a/src/apps/tracker-api/Tracker.Tests/Presentation/Observability/ObservabilityConventionTests.cs b/src/apps/tracker-api/Tracker.Tests/Presentation/Observability/ObservabilityConventionTests.cs
index a2a271d6..c165e7f8 100644
--- a/src/apps/tracker-api/Tracker.Tests/Presentation/Observability/ObservabilityConventionTests.cs
+++ b/src/apps/tracker-api/Tracker.Tests/Presentation/Observability/ObservabilityConventionTests.cs
@@ -97,6 +97,190 @@ public void LaFuenteDeSpansPropiaTieneNombreEstable()
TrackerTracing.Source.Name.Should().Be("Evolith.Tracker");
}
+ // ── GT-616 — que las trazas se puedan encender, y que digan algo cuando se encienden ────────
+
+ ///
+ /// GT-616 — `OTEL_EXPORTER_OTLP_ENDPOINT` ENCIENDE las trazas.
+ ///
+ /// `.env.example:40` lleva declarando esa variable desde antes de que existiera el
+ /// pipeline, y el comentario de TracingOptions.Endpoint afirmaba que era «la variable
+ /// que por fin tiene consumidor». No lo era: el enlace de configuración de ASP.NET traduce
+ /// `Otlp__Endpoint`, de modo que ponerla no registraba nada y el retorno temprano seguía
+ /// disparando. Un interruptor documentado que no hace nada consume el presupuesto de
+ /// diagnóstico de quien lo prueba, y encima hace creer que la telemetría está encendida.
+ ///
+ [Fact]
+ public void ConLaVariableEstandarDeOtel_LasTrazasSIseRegistran()
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddTrackerTracing(new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://colector.local:4317"
+ })
+ .Build());
+
+ services.Should().Contain(d => d.ServiceType.FullName!.Contains("OpenTelemetry"),
+ "la variable estandar de OpenTelemetry es la unica documentada en `.env.example`; "
+ + "si no enciende nada, el interruptor documentado es una mentira");
+ }
+
+ ///
+ /// `Otlp:Endpoint` sigue GANANDO sobre la variable estándar: es la específica del despliegue,
+ /// y el chart la rellena. Sin esta comprobación, un respaldo mal ordenado dejaría que una
+ /// variable heredada del entorno del nodo sobrescribiera lo que el operador puso en el chart.
+ ///
+ [Fact]
+ public void LaSeccionDeConfiguracionGanaSobreLaVariableDeEntorno()
+ {
+ var opciones = new TracingOptions { Endpoint = "http://del-chart:4317" };
+ var configuracion = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://del-entorno:4317"
+ })
+ .Build();
+
+ TrackerTracing.ResolveEndpoint(opciones, configuracion).Should().Be("http://del-chart:4317");
+ }
+
+ ///
+ /// GT-616 — el chart tiene interruptor.
+ ///
+ /// El defecto reportado («no se ejecuta ningún `StartActivity` en el estado desplegado»)
+ /// es cierto, pero el estado apagado por defecto lo decidió `T-049` a propósito y no se
+ /// revierte. Lo que no defendía ningún ADR es que el configmap NO EMITIERA `Otlp__Endpoint` en
+ /// absoluto: un operador con colector no tenía manera soportada de encenderlas, sólo inyectar
+ /// la clave a mano por `extraEnv` — justo el tipo de configuración no declarada que este chart
+ /// evita en todo lo demás.
+ ///
+ /// Se comprueba el chart como TEXTO y no el clúster: la guarda tiene que correr en un
+ /// runner sin Helm ni Kubernetes, y lo que se protege es que la clave siga declarada, no que
+ /// un despliegue concreto la tenga puesta.
+ ///
+ [Fact]
+ public void ElChartOfreceUnInterruptorDeclaradoParaLasTrazas()
+ {
+ var raiz = RepoRoot();
+ var chart = Path.Combine(raiz, "product/infra/helm/evolith-tracker-api");
+
+ var configmap = File.ReadAllText(Path.Combine(chart, "templates/configmap.yaml"));
+ var valores = File.ReadAllText(Path.Combine(chart, "values.yaml"));
+
+ configmap.Should().Contain("Otlp__Endpoint",
+ "sin esta clave el configmap no puede encender las trazas ni aunque el operador tenga colector");
+ valores.Should().Contain("otlp:",
+ "el valor tiene que estar DECLARADO en values.yaml; un `--set` sobre una clave inexistente "
+ + "no se documenta ni se revisa");
+ valores.Should().MatchRegex(@"otlp:\s*\r?\n\s*endpoint:\s*""""",
+ "el defecto por defecto sigue siendo APAGADO (`T-049`): esta guarda existe para que "
+ + "encenderlo sea posible, no para encenderlo");
+ }
+
+ ///
+ /// GT-616 — la guarda que cierra el defecto reportado. Existe al menos un `StartActivity`
+ /// de DOMINIO en la superficie, y está en los puntos donde se decide gobierno.
+ ///
+ /// Antes de esta ficha, `TrackerTracing.Source` se registraba en el pipeline y no la
+ /// usaba NADIE: cero llamadas a `StartActivity` en todo el repositorio. Encender el exportador
+ /// habría llenado el backend de spans de ASP.NET y `HttpClient` — «esta ruta tardó 180 ms»—,
+ /// que es una pregunta de fontanería. La de este producto es «qué inquilino decidió qué
+ /// compuerta, con qué desenlace, y quién la empujó».
+ ///
+ /// Es una comprobación de FUENTE y se declara como tal, igual que la del prefijo de
+ /// métricas: no prueba que los spans sean correctos —eso lo hace
+ /// —, prueba que nadie ha desmantelado
+ /// la instrumentación de los puntos gobernados sin darse cuenta.
+ ///
+ [Fact]
+ public void LosPuntosDeGobiernoAbrenUnSpanDeDominio()
+ {
+ var raiz = RepoRoot();
+ var presentacion = Path.Combine(raiz, "src/apps/tracker-api/Tracker.Presentation");
+
+ var ficheros = Directory.GetFiles(presentacion, "*.cs", SearchOption.AllDirectories)
+ .Where(f => !f.Contains("/obj/") && !f.Contains("/bin/"))
+ .ToArray();
+
+ ficheros.Should().NotBeEmpty("una guarda que recorre cero ficheros pasa siempre");
+
+ var conSpanDeGobierno = ficheros
+ .Where(f => File.ReadAllText(f).Contains("StartGovernanceActivity(", StringComparison.Ordinal))
+ .Select(f => Path.GetFileName(f))
+ .Where(n => n != "TrackerTracing.cs") // la definicion no cuenta como punto de uso
+ .OrderBy(n => n)
+ .ToArray();
+
+ conSpanDeGobierno.Should().HaveCountGreaterThanOrEqualTo(3,
+ "la fuente `Evolith.Tracker` se registraba en el pipeline y no la invocaba NADIE: "
+ + "con el exportador encendido el backend solo habria recibido fontaneria de ASP.NET. "
+ + $"Puntos instrumentados hoy: [{string.Join(", ", conSpanDeGobierno)}]");
+
+ conSpanDeGobierno.Should().Contain("GateDecisionEndpoints.cs",
+ "decidir una compuerta es EL acto de gobierno de este producto");
+ }
+
+ ///
+ /// GT-616 — comprobación de COMPORTAMIENTO (no de fuente): el span de gobierno lleva de verdad
+ /// inquilino, actor, iniciativa, fase, agente y desenlace, con los nombres de atributo estables.
+ ///
+ /// Un atributo mal escrito no falla: se ingiere igual y crea una dimensión huérfana que
+ /// nadie consulta. Por eso los nombres viven en y por eso
+ /// esta prueba los contrasta contra un ActivityListener real en vez de contra el
+ /// literal que acaba de escribir el mismo autor.
+ ///
+ [Fact]
+ public void ElSpanDeGobiernoLlevaLosAtributosDeDominio()
+ {
+ var tenant = Guid.NewGuid();
+ var actor = Guid.NewGuid();
+ var iniciativa = Guid.NewGuid();
+
+ using var oyente = new System.Diagnostics.ActivityListener
+ {
+ ShouldListenTo = fuente => fuente.Name == TrackerTracing.Source.Name,
+ Sample = (ref System.Diagnostics.ActivityCreationOptions _)
+ => System.Diagnostics.ActivitySamplingResult.AllDataAndRecorded
+ };
+ System.Diagnostics.ActivitySource.AddActivityListener(oyente);
+
+ System.Diagnostics.Activity? capturada;
+ using (var actividad = TrackerTracing.StartGovernanceActivity(
+ "governance.gate_decision.decide", tenant, actor, iniciativa,
+ phase: "design", agent: "agent-runtime"))
+ {
+ actividad.SetGovernanceOutcome("approved_with_exception");
+ capturada = actividad;
+ }
+
+ capturada.Should().NotBeNull("con un listener suscrito la fuente tiene que producir el span");
+ capturada!.OperationName.Should().Be("governance.gate_decision.decide");
+
+ capturada.GetTagItem(GovernanceTraceTags.TenantId).Should().Be(tenant.ToString());
+ capturada.GetTagItem(GovernanceTraceTags.ActorId).Should().Be(actor.ToString());
+ capturada.GetTagItem(GovernanceTraceTags.InitiativeId).Should().Be(iniciativa.ToString());
+ capturada.GetTagItem(GovernanceTraceTags.Phase).Should().Be("design");
+ capturada.GetTagItem(GovernanceTraceTags.Agent).Should().Be("agent-runtime");
+ capturada.GetTagItem(GovernanceTraceTags.Outcome).Should().Be("approved_with_exception");
+ }
+
+ ///
+ /// Sin listener —el estado por defecto, trazas apagadas— abrir un span de gobierno devuelve
+ /// null y no cuesta nada. Por eso los puntos de llamada se instrumentan
+ /// incondicionalmente en vez de esconderse tras un `if` que alguien olvidaría de mantener.
+ ///
+ [Fact]
+ public void SinOyente_ElSpanDeGobiernoNoSeMaterializa()
+ {
+ var actividad = TrackerTracing.StartGovernanceActivity("governance.probe", Guid.NewGuid());
+
+ actividad.Should().BeNull();
+
+ // Y sellarlo tampoco explota: los puntos de llamada no comprueban null.
+ actividad.SetGovernanceOutcome("approved");
+ }
+
private static IConfiguration Config(string? endpoint)
{
var valores = new Dictionary();
@@ -104,14 +288,32 @@ private static IConfiguration Config(string? endpoint)
return new ConfigurationBuilder().AddInMemoryCollection(valores).Build();
}
+ ///
+ /// GT-616 — en un worktree de git, `.git` es un FICHERO, no un directorio.
+ ///
+ /// La versión anterior sólo miraba Directory.Exists(".git"), así que dentro de un
+ /// worktree no reconocía su propia raíz: seguía subiendo hasta el checkout PRINCIPAL y escaneaba
+ /// los ficheros de OTRA rama. Las guardas de fuente pasaban o fallaban según lo que hubiera en
+ /// un árbol de trabajo distinto del que se estaba probando — un verde que no es evidencia de
+ /// nada, y el modo de fallo más caro de todos porque no se anuncia.
+ ///
+ /// Se detectó al ver esta misma prueba seguir en rojo con el arreglo ya aplicado en el
+ /// disco: leía el fichero equivocado.
+ ///
private static string RepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, ".git")))
+ while (dir is not null && !EsRaizDeRepositorio(dir))
{
dir = dir.Parent;
}
dir.Should().NotBeNull();
return dir!.FullName;
+
+ static bool EsRaizDeRepositorio(DirectoryInfo candidato)
+ {
+ var git = Path.Combine(candidato.FullName, ".git");
+ return Directory.Exists(git) || File.Exists(git);
+ }
}
}