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
69 changes: 68 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ jobs:
done

- name: Test
run: dotnet test Tracker.sln --no-build -c Release --logger "trx;LogFileName=test-results.trx"
# GT-588 — se EXCLUYE la categoria `Interop`, que necesita el CLI del Core construido.
# Corre en su propio job (`transparency-interop`), que si lo aporta. Sin esta exclusion,
# este job seria rojo; con el `return` silencioso que tenia antes, era verde sin comprobar
# nada, que es peor.
run: dotnet test Tracker.sln --no-build -c Release --filter "Category!=Interop" --logger "trx;LogFileName=test-results.trx"

- name: Smoke — la aplicacion arranca LIMPIA contra Postgres
# Anadido tras la regresion del 2026-07-19: el filtro global por tenant de CD-27
Expand All @@ -113,6 +117,69 @@ jobs:
path: src/apps/tracker-api/**/TestResults/*.trx
if-no-files-found: ignore

# GT-588 — la unica prueba de que el firmante C# y el verificador TypeScript producen y leen los
# MISMOS bytes. Entre ambos hay JSON canonico, CBOR determinista, cabeceras COSE y un arbol de
# Merkle: cuatro capas donde un solo byte de diferencia lo rompe todo, y ninguna de ellas se
# comprueba comparando estructuras C# contra estructuras C#.
#
# Va en su propio job porque necesita construir el CLI del Core, que es caro y no tiene por que
# frenar al resto. Y MUERDE: los tests fallan —no se saltan— cuando no encuentran el CLI, porque
# una prueba de interoperabilidad que pasa sin la otra mitad presente reporta como verificado algo
# que nadie miro.
transparency-interop:
name: Transparency interop (C# firma · TypeScript verifica)
runs-on: ubuntu-latest

env:
DOTNET_NOLOGO: "true"
DOTNET_CLI_TELEMETRY_OPTOUT: "true"

steps:
- name: Checkout Tracker
uses: actions/checkout@v4
with:
path: tracker

- name: Checkout Evolith Core
uses: actions/checkout@v4
with:
repository: beyondnetcode/evolith_arch32
token: ${{ secrets.CORE_REPO_TOKEN }}
path: core

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
cache-dependency-path: core/package-lock.json

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: Build the Core CLI (the verifier)
working-directory: core
run: |
npm ci
npm run build

- name: Verify the CLI exposes `audit verify`
working-directory: core
run: |
set -euo pipefail
# Se comprueba ANTES de correr los tests para que un CLI construido pero sin el comando
# de un mensaje claro, en vez de un fallo de proceso dentro de una asercion.
test -f src/sdk/cli/dist/main.js
node src/sdk/cli/dist/main.js audit verify --help > /dev/null

- name: Cross-language interop tests
working-directory: tracker/src/apps/tracker-api
env:
EVOLITH_CLI_MAIN: ${{ github.workspace }}/core/src/sdk/cli/dist/main.js
run: dotnet test Tracker.sln --filter "Category=Interop" --logger "trx;LogFileName=interop-results.trx"

frontend:
name: Frontend (lint + typecheck + build)
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions DECISIONS.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ _Nota: Las decisiones universales se heredan del Upstream Base ([evolith_arch32]
| T-053 | Consumir la identidad UMS: JWKS/OIDC preferido, simétrico como interino | Definir | Proposed | [T-053](./docs/adrs/T-053-consume-ums-identity.es.md) | El tracker-api valida OIDC/JWKS pero el UMS no lo expone. Preferir que el UMS publique JWKS (aguas arriba); interino simétrico implementado y verificado con mock. |
| T-054 | Gate de frontera en tiempo de edición: adoptar acotado, diferido a EAG-11 | Definir | Accepted | [T-054](./docs/adrs/T-054-edit-time-gate-adoption.es.md) | Adopta el edit-gate del Core pero DIFERIDO: `.claude/` está en `.gitignore`, `EAG-11` aún no da la fuente única de reglas, y el matcher puede misfirear. Activación condicionada a `EAG-11` + versionar `.claude/settings.json` + acotar rutas + vía de escape. |
| T-055 | El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave | Definir | Accepted | [T-055](./docs/adrs/T-055-core-initiated-evidence-ingest.es.md) | Segunda dirección del tráfico de evidencia: `POST /core-evaluation-transactions` autenticado por clave de máquina atada al esquema POR NOMBRE, permiso propio `:ingest` SIN `:read`, tenant derivado de QUÉ CLAVE encajó (un `tenantId` en el cuerpo se rechaza con 400, no se ignora), idempotencia por `(tenant, correlationId)` respaldada por índice único, motor de cada regla VERBATIM (vocabulario abierto) y los dos responsables —quien pidió y quien debe arreglar— en columnas distintas. Estado `ingested`, distinto de `completed`. El DTO derivado a mano cumple `T-038` con guarda de deriva por fixture. Sigue siendo advisory (`T-039`). Cierra `GT-604`. |
| T-056 | El sellado no lleva lógica, la validación de contenido es del tenant, y la IA propone pero nunca decide | Definir | Accepted | [T-056](./docs/adrs/T-056-three-layer-separation.es.md) | Tres capas con frontera dura, ratificadas al cablear `GT-588`. **(1) El sellado no mira el contenido:** `payload` es opaco y lo único que se fija es la regla de ESCRITURA (claves ordenadas en toda profundidad, comparación ordinal para que el idioma de la máquina no las reordene, una codificación). Por eso la interoperabilidad C#/TypeScript no crea un problema de estandarización por tenant — un notario sella documentos distintos con un procedimiento invariable. **(2) La validación de contenido la configura el tenant:** en cuanto la semántica de un cliente llega al motor como código, el motor pasa a ser un catálogo de casos particulares y cada cliente nuevo es una release. Misma frontera que `T-039` aplicada al contenido. **(3) La IA propone, nunca decide:** un decisor probabilístico vuelve la garantía incomprobable, porque dos ejecuciones podrían diferir y nadie sabría cuál vale; detrás tiene que haber un verificador determinista y, si la propuesta es probabilística, su confianza se muestra a quien la ratifica (`GT-590`, `GT-584`). |
1 change: 1 addition & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,6 @@ _Nota: Las decisiones universales se heredan del Upstream Base ([evolith_arch32]
| T-054 | Gate de frontera en tiempo de edición: adoptar acotado, diferido a EAG-11 | Definir | Accepted | [T-054](./docs/adrs/T-054-edit-time-gate-adoption.md) | Adopta el edit-gate del Core pero DIFERIDO: `.claude/` está en `.gitignore`, `EAG-11` aún no da la fuente única de reglas, y el matcher puede misfirear. Activación condicionada a `EAG-11` + versionar `.claude/settings.json` + acotar rutas + vía de escape. |

| T-055 | El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave | Definir | Accepted | [T-055](./docs/adrs/T-055-core-initiated-evidence-ingest.md) | Segunda dirección del tráfico de evidencia: `POST /core-evaluation-transactions` autenticado por clave de máquina atada al esquema POR NOMBRE, permiso propio `:ingest` SIN `:read`, tenant derivado de QUÉ CLAVE encajó (un `tenantId` en el cuerpo se rechaza con 400, no se ignora), idempotencia por `(tenant, correlationId)` respaldada por índice único, motor de cada regla VERBATIM (vocabulario abierto) y los dos responsables —quien pidió y quien debe arreglar— en columnas distintas. Estado `ingested`, distinto de `completed`. El DTO derivado a mano cumple `T-038` con guarda de deriva por fixture. Sigue siendo advisory (`T-039`). Cierra `GT-604`. |
| T-056 | El sellado no lleva lógica, la validación de contenido es del tenant, y la IA propone pero nunca decide | Definir | Accepted | [T-056](./docs/adrs/T-056-three-layer-separation.md) | Tres capas con frontera dura, ratificadas al cablear `GT-588`. **(1) El sellado no mira el contenido:** `payload` es opaco y lo único que se fija es la regla de ESCRITURA (claves ordenadas en toda profundidad, comparación ordinal para que el idioma de la máquina no las reordene, una codificación). Por eso la interoperabilidad C#/TypeScript no crea un problema de estandarización por tenant — un notario sella documentos distintos con un procedimiento invariable. **(2) La validación de contenido la configura el tenant:** en cuanto la semántica de un cliente llega al motor como código, el motor pasa a ser un catálogo de casos particulares y cada cliente nuevo es una release. Misma frontera que `T-039` aplicada al contenido. **(3) La IA propone, nunca decide:** un decisor probabilístico vuelve la garantía incomprobable, porque dos ejecuciones podrían diferir y nadie sabría cuál vale; detrás tiene que haber un verificador determinista y, si la propuesta es probabilística, su confianza se muestra a quien la ratifica (`GT-590`, `GT-584`). |

> **Plantilla para nuevos ADRs:** Al crear un nuevo documento para "ADR Local", utilice el esquema de Frontmatter definido en los estándares de Evolith Core y ubíquelo en la carpeta de gobernanza correspondiente.
2 changes: 1 addition & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Plataforma](https://img.shields.io/badge/.NET_10_%7C_PostgreSQL_%7C_React_19-informational?style=for-the-badge)]()
[![Arquitectura](https://img.shields.io/badge/Evolith-Satélite-blueviolet?style=for-the-badge)](https://github.com/beyondnetcode/evolith_arch32)
<!-- BEGIN GENERATED: adr-count — derivado por .harness/scripts/doc-inventory.mjs; NO editar a mano -->
[![ADRs](https://img.shields.io/badge/ADRs-55_decisiones-orange?style=for-the-badge)](./DECISIONS.md)
[![ADRs](https://img.shields.io/badge/ADRs-56_decisiones-orange?style=for-the-badge)](./DECISIONS.md)
<!-- END GENERATED: adr-count -->
[![Licencia](https://img.shields.io/badge/Licencia-Dual_License-informational?style=for-the-badge)](./LICENSE)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Platform](https://img.shields.io/badge/.NET_10_%7C_PostgreSQL_%7C_React_19-informational?style=for-the-badge)]()
[![Architecture](https://img.shields.io/badge/Evolith-Satellite_Product-blueviolet?style=for-the-badge)](https://github.com/beyondnetcode/evolith_arch32)
<!-- BEGIN GENERATED: adr-count — derivado por .harness/scripts/doc-inventory.mjs; NO editar a mano -->
[![ADRs](https://img.shields.io/badge/ADRs-55_decisions-orange?style=for-the-badge)](./DECISIONS.md)
[![ADRs](https://img.shields.io/badge/ADRs-56_decisions-orange?style=for-the-badge)](./DECISIONS.md)
<!-- END GENERATED: adr-count -->
[![License](https://img.shields.io/badge/License-Dual_License-informational?style=for-the-badge)](./LICENSE)

Expand Down
107 changes: 107 additions & 0 deletions docs/adrs/T-056-three-layer-separation.es.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
adr: T-056
title: El sellado no lleva lógica, la validación de contenido es del tenant, y la IA propone pero nunca decide
status: Accepted
date: 2026-08-01
tags: [EvolithSatellite, governance, transparency, ai, multi-tenancy, architecture]
authority: Decisión de producto, ratificada el 2026-08-01 al cablear GT-588
relates: [T-039 el Core recomienda el tenant decide, T-055 ingesta de evidencia iniciada por el Core, core/ADR-0111 costura del proveedor probabilístico, core/ADR-0101 Core sin estado]
gaps: [GT-588, GT-590, GT-584]
---

# ADR T-056 — Tres capas que no deben mezclarse

## Status

Aceptado (2026-08-01). Ratificado por el propietario del producto al decidir dónde vive el cable de
firma de GT-588, a raíz de una pregunta directa: *«¿no te parece que estandarizar cada contenido es
muy complejo y variable entre tenants?»*. La respuesta es que **no estandarizamos el contenido en
absoluto** — y escribirlo es el sentido de este registro, porque el error que previene es uno que un
ingeniero razonable cometería a propósito.

## Context

Tres asuntos distintos se venían discutiendo como si fueran uno, y confundirlos produce un fallo
concreto y caro: un motor que acumula un catálogo de reglas por cliente, donde cada tenant nuevo
obliga a tocar el núcleo.

El detonante inmediato fue el ledger de transparencia. Una decisión firmada tiene que escribirse
byte a byte igual por un firmante en C# y un verificador en TypeScript, lo que invita a pensar que
«la forma de una decisión» debe estandarizarse entre tenants. No debe, y la razón vale mucho más
allá del sellado.

Por separado surgió la posibilidad de que un tenant enchufe **su propia IA**, anclada a nuestra
configuración, para recibir contenido y contexto y validarlo. Es una buena idea en la capa correcta
y peligrosa en la equivocada.

## Decision

**Tres capas, con frontera dura entre cada una.**

### 1. El sellado no lleva lógica ni opinión

La capa de transparencia/firma **no** mira el contenido, no lo juzga y no lo interpreta. Coge lo que
llegue y lo escribe siempre igual para poder sellarlo — como una báscula que pesa un paquete sin
abrirlo.

En concreto: `DecisionStatement.payload` es opaco. Lo que se fija es la regla de **escritura**
(claves ordenadas en toda profundidad, comparación ordinal para que el idioma de una máquina no las
reordene, una sola codificación), no el contenido. Esa regla es universal: sirve para cualquier
payload, incluido uno que un tenant invente mañana y que nunca hayamos visto.

Por eso la interoperabilidad **no** crea un problema de estandarización por tenant. Un notario sella
documentos radicalmente distintos con un único procedimiento invariable. Lo que varía es el
documento; lo que no varía es el acto de sellar.

### 2. La validación de contenido la configura el tenant, no la cableamos nosotros

No escribimos lógica propia que juzgue contenido cuyo significado define cada tenant. Rulesets,
criterios y compuertas son configuración. En cuanto la semántica de un cliente llega al motor como
código, el motor se convierte en un catálogo de casos particulares y cada cliente nuevo es una
release.

Es la misma frontera que `T-039` ya traza para la autoridad de excepción (el Core recomienda, el
tenant decide), aplicada al contenido.

### 3. La IA propone; nunca es la autoridad final

La IA de un tenant puede leer contenido y contexto y **sugerir**: «esto parece incompleto», «esto no
encaja con lo que declaraste». No puede decidir, porque un decisor probabilístico vuelve la garantía
incomprobable — dos ejecuciones podrían diferir y nadie sabría cuál vale.

El reparto sano es **la IA propone → algo determinista verifica → un humano confirma cuando importa**,
que es la costura que `core/ADR-0111` ya define y que las aprobaciones de runtime de `GT-590` ya
implementan, incluido el campo `confidence`, que existe precisamente para decirle al humano que está
ratificando una **conjetura** y no un hecho.

## Consequences

- La capa de sellado se mantiene pequeña y estable. No puede adquirir comportamiento por tenant,
porque no tiene dónde ponerlo.
- Los vectores dorados del renderizado canónico son **cortos y no crecen con los tenants**: fijan la
regla (acentos, anidamiento, campos vacíos, textos largos), no los casos de negocio.
- Cualquier propuesta de «validar X para el tenant Y» en código del motor queda rechazada por este
registro; su sitio es la configuración.
- Cualquier propuesta de que un modelo emita un veredicto vinculante queda rechazada; detrás tiene
que haber un verificador determinista, y cuando la propuesta sea probabilística su confianza debe
mostrarse a quien la ratifica.
- El coste es real y se asume: la validación por configuración es más difícil de autorar que unas
reglas cableadas, y la costura de la IA necesita una superficie de confirmación. Ambos se pagan una
vez, no por tenant.

## Validation

- El ledger de `GT-588` lleva `payload` como miembro opaco; ningún camino de código lo lee para
decidir. El renderizador canónico ordena claves y jamás inspecciona valores.
- Las aprobaciones de runtime de `GT-590` ya llevan `confidence`, y la superficie de aprobación lo
muestra como una conjetura que se ratifica.
- Una violación futura se detecta preguntando de cualquier validación nueva: *¿está leyendo
significado definido por el tenant dentro de código del motor?* Si la respuesta es sí, su sitio es
la capa 2 como configuración.

## References

- `T-039` — el Core recomienda, el tenant decide.
- `core/ADR-0101` — el Core es un motor de evaluación sin estado; no puede sostener un ledger.
- `core/ADR-0111` — la costura del proveedor probabilístico.
- RFC 9943 — el Servicio de Transparencia es una entidad separada del Issuer.
Loading
Loading