Ir al contenido

Proyecto independiente de I+D · Colonia

Python y FastAPI

Un monolito modular con fronteras tipadas, contratos de importación, una traza de auditoría de solo inserción en PostgreSQL y reglas como datos, con ejemplos de referencia probados.

No normativo

Versión del Companion
1.0
Corresponde a COADF Core
2.2
Estado
Vigente
Última revisión
Versión del perfil
1.0
Ejemplos de código
Ejemplos de referencia ilustrativos

Propiedades de arquitectura tratadas

  • P-1Una frontera tipada y validada alrededor de la llamada al modelo; en esta implementación de referencia, un valor autorizado solo existe tras la verificación de una persona con nombre.
  • P-4Una identidad de auditoría junto al contexto de OpenTelemetry; una traza de solo inserción en PostgreSQL cuyas escrituras absorben un reintento exacto y rechazan uno en conflicto.
  • P-5El método de extracción y la ubicación en la fuente viajan con cada valor propuesto y sobreviven a la serialización.
  • P-6Contratos de importación y un fence de publicación, cada uno con su prueba de dientes.
  • P-7Un puerto que pertenece al dominio y un adaptador que es el único módulo que conoce al proveedor.
  • P-8Decisiones a partir de material de políticas versionado, conservadas con su revisión de la política o rechazadas.
  • P-2Solo con la profundidad que publica COADF. Los ejemplos no contienen ninguna representación de la confianza.
  • P-3Solo con la profundidad que publica COADF: en esta implementación de referencia, verificación por atributo de una persona con nombre, y un valor rechazado que queda vacío. No se muestra cuándo se requiere revisión ni cómo se organizan las revisiones.

Intención de arquitectura

En un servicio Python, el cliente del modelo y el código de dominio suelen escribirlos las mismas personas, en el mismo repositorio y la misma tarde, y así el lado probabilístico y el lado autorizado acaban en un mismo módulo, llamándose directamente. FastAPI y Pydantic hacen que la frontera sea barata de expresar como tipos y de validar en tiempo de ejecución. También tienen valores por defecto que son correctos para una API web y erróneos para una frontera: un modelo de Pydantic ignora los campos que no declara y convierte tipos compatibles salvo que el modo estricto esté activado, y FastAPI filtra cada respuesta según su modelo de respuesta sin avisar.

Este perfil muestra una manera de mantener las propiedades de COADF en un único servicio Python: un monolito modular con contratos de importación, PostgreSQL para la traza de auditoría, OpenTelemetry para el contexto de ejecución y Open Policy Agent detrás de una interfaz para las políticas. Nada de esto es obligatorio. Cada pieza es un lugar donde la propiedad se mantiene o se pierde en silencio.

Correspondencia tecnológica

Python y FastAPI: Correspondencia tecnológica
Propiedad de arquitecturaPython y FastAPI
Tipo de fronteraModelos de Pydantic congelados con extra="forbid" y strict=True; una dataclass distinta para los valores verificados
Validación en tiempo de ejecuciónmodel_validate_json sobre la respuesta en bruto; validación de peticiones de FastAPI, respondida con 422
Contrato de salidaUn response_model explícito que declara la procedencia
Regla de arquitecturaContratos forbidden de import-linter, ejecutados con lint-imports
Contexto de ejecuciónPropagación de OpenTelemetry (inject, extract); contextvars copiadas a los hilos
Traza de auditoríaPostgreSQL: solo inserción y selección para el rol de la aplicación, triggers que rechazan reescrituras y una clave única cuyos conflictos se comparan antes de contar como reintentos
Aislamiento de normasUn puerto typing.Protocol; un adaptador httpx que convierte los resultados HTTP del proveedor en no encontrado, no disponible y rechazado
PolíticaOPA a través de su API REST detrás de una interfaz pequeña; el tipo de la respuesta se comprueba y nunca se convierte, y cada decisión exige la revisión de la política
Fence de publicaciónUn escaneo de la salida construida con un archivo de patrones guardado fuera de ella; una prueba de dientes comprobada por digest

Patrón de referencia

Un único paquete, refapp, en un dominio de mantenimiento sintético: un modelo propone la próxima fecha de servicio y una clase de componente a partir del informe de servicio de un técnico. Los módulos son las fronteras.

Los módulos del paquete de referencia y la propiedad que mantiene cada uno
MóduloFunciónNo puede importar
boundary.pyEl contrato que debe cumplir una respuesta del modelonada del dominio
inference.pyLlama a un modelo; devuelve un Proposalrecords, audit, api
formats.pyComprobaciones deterministas sobre un valor propuestoadaptadores, clientes HTTP
records.pyValores verificados; la entrada que pasa por revisiónadaptadores, clientes HTTP
api.pyLa superficie HTTP de la frontera
tracing.pyContexto de ejecución e identidad de auditoría
audit.py, schema.sqlLa traza de solo inserción
classification.pyEl puerto hacia un servicio externoadaptadores, clientes HTTP
adapters/El proveedor, y solo el proveedor
policy.py, policy/Material de políticas, regla y cliente
fence.pyEl fence de publicación

P-1 · La frontera

Ejemplo de referencia ilustrativoUn contrato tipado en torno a una llamada al modelo
Propósito
Dar a una llamada al modelo un contrato: qué puede devolver y qué tiene que acompañar a lo que devuelve. Una frontera de referencia deliberadamente parcial, construida solo con lo que COADF publica en este nivel.
Propiedad de arquitectura
Un componente probabilístico devuelve un valor junto con la forma en que se obtuvo, y cualquier otra cosa se rechaza en la frontera (P-1). El método viaja con el valor, y eso es lo que hace posible declarar más adelante la extracción automática (P-5).
refapp/boundary.pypythonP-1 · P-5
"""Illustrative reference example: the typed boundary around a model call. A probabilistic component returns a value together with how it was obtained,never a bare value. Anything that does not parse into a Proposal is rejectedhere, before it reaches code that treats values as facts. Deliberately partial: there is no confidence field. COADF publishes that aconfidence accompanies the value, not how it is represented, so none is invented.""" from typing import Literal from pydantic import BaseModel, ConfigDict, Field # frozen: nothing downstream can edit a proposal in place.# extra="forbid": a field the contract does not name is an error, not ignored.# strict: no coercion, so "12" is not quietly accepted where an int is expected.STRICT = ConfigDict(frozen=True, extra="forbid", strict=True)  class Derivation(BaseModel):    model_config = STRICT     method: Literal["language-model"]    model: str = Field(min_length=1)  # the model identifier as deployed    prompt_revision: str = Field(min_length=1)  # the prompt template the call used    source_digest: str = Field(pattern=r"^[0-9a-f]{64}$")  # SHA-256 of the source read    source_start: int = Field(ge=0)  # where in the source the passage begins    source_end: int = Field(ge=0)  # and where it ends  class Proposal(BaseModel):    model_config = STRICT     attribute: Literal["next_service_due", "component_class"]    value: str = Field(min_length=1, max_length=64)    provenance: Derivation  def parse_model_reply(raw: str) -> Proposal:    """Parse and validate the model's JSON in one step, or raise ValidationError."""    return Proposal.model_validate_json(raw)

Lo que omite a propósito

  • La confianza, a propósito. En COADF P-1, un componente probabilístico comunica una confianza junto con su valor; esa es la propiedad de arquitectura pública. Cómo se expresa, se asigna y se usa la confianza es un detalle de implementación reservado de P-2, que COADF publica solo como principio. Por eso esta frontera lleva el método y la procedencia, no tiene campo de confianza y no inventa ninguna representación para ella: es parcial a propósito, no completa.
  • La llamada al modelo, sus reintentos y el prompt. La frontera es la misma sea cual sea el cliente que produjo la cadena.
  • Si el valor es correcto. Un esquema comprueba la forma; no puede saber si una fecha es la correcta para este activo.
  • El almacenamiento. Un Proposal es un mensaje, no un registro.

Cómo verificarlo

Dar al parser respuestas casi correctas: un campo no declarado, un número donde el contrato dice texto, una procedencia ausente, un atributo que nadie pidió. Cada una tiene que lanzar una excepción (tests/test_boundary.py). Después, borrar extra="forbid" en una copia desechable y ver cómo falla la prueba del campo no declarado.

Modo de fallo que aborda

Una respuesta del modelo analizada con json.loads y leída como diccionario. Un "verified": true de más, un número convertido a la fuerza o una fuente ausente pasan sin obstáculo, y el valor se vuelve indistinguible de un dato que alguien comprobó.

Ejemplo de referencia ilustrativoComprobaciones deterministas en torno a un valor estocástico
Propósito
Comprobar el valor propuesto con código que no depende en absoluto del modelo.
Propiedad de arquitectura
La validación determinista rodea la salida estocástica: el mismo valor recibe siempre el mismo veredicto, y el veredicto puede reproducirse a partir del valor por sí solo (P-1).
refapp/formats.pypythonP-1
"""Deterministic checks on a proposed value. They read the value, never the model.""" import refrom datetime import date from refapp.boundary import Proposal _CALENDAR_DATE = re.compile(r"\d{4}-\d{2}-\d{2}")  def check_format(proposal: Proposal) -> None:    """Raise ValueError when the value is not well formed for its attribute."""    if proposal.attribute == "next_service_due":        if not _CALENDAR_DATE.fullmatch(proposal.value):            raise ValueError("next_service_due must be written YYYY-MM-DD")        date.fromisoformat(proposal.value)  # and must be a real calendar date

Lo que omite a propósito

  • Comprobaciones que necesitan otros registros u otras fuentes. Este ejemplo comprueba un valor frente a las reglas de su propio atributo y nada más.
  • Formatos de fecha localizados. El contrato fija a propósito una única forma escrita.

Cómo verificarlo

Un valor que encaja con el patrón pero no es una fecha del calendario (2026-02-30) nunca se verifica (tests/test_boundary.py).

Modo de fallo que aborda

Confiar en que el modelo produjo un valor válido porque el prompt lo pedía: la comprobación que no se escribe es la que falla en producción.

Ejemplo de referencia ilustrativoUn camino revisado hacia los registros autorizados
Propósito
Un camino de referencia conservador por el que un valor derivado del modelo se vuelve autorizado: en este ejemplo, cada uno de esos valores va a una persona con nombre, que lo mira junto a su fuente y lo acepta o lo rechaza.
Propiedad de arquitectura
Un componente probabilístico no puede convertirse directamente en una salida autorizada (P-1). En esta implementación de referencia, un valor derivado del modelo llega a un registro solo mediante verificación humana, y un valor rechazado deja el atributo vacío (P-3, con la profundidad que publica COADF). Es una implementación, no una topología de COADF.
refapp/records.pypythonP-1 · P-3
"""A reviewed path into authoritative records, one conservative reference. This example sends every model-derived value through the verification of anamed person. When a real system requires review is its own rule, which thisexample does not define; nor does it say how reviews are organised.""" from dataclasses import dataclassfrom datetime import datetime from refapp.boundary import Proposalfrom refapp.formats import check_format  @dataclass(frozen=True)class ReviewOutcome:    reviewer: str    accepted: bool    reason: str    decided_at: datetime  @dataclass(frozen=True)class VerifiedValue:    attribute: str    value: str    proposal: Proposal  # the provenance stays attached after verification    review: ReviewOutcome  def accept(proposal: Proposal, review: ReviewOutcome) -> VerifiedValue | None:    """Rejected means absent: the attribute stays empty and nothing is guessed."""    check_format(proposal)    if not review.reviewer.strip():        raise ValueError("a verification needs a named reviewer")    if not review.accepted:        return None    return VerifiedValue(proposal.attribute, proposal.value, proposal, review)

Lo que omite a propósito

  • Cuándo se requiere revisión. COADF publica que la revisión la dispara la confianza; la regla que lo decide no se publica, y este ejemplo no define ninguna. Verifica todo valor derivado del modelo, la opción conservadora, que es también lo que el fence público F-03 exige de los valores derivados de un modelo de lenguaje.
  • Cómo se organizan las revisiones. COADF no publica si las revisiones se ponen en cola, se asignan o se disparan, y aquí no se da a entender nada de ello. Tampoco se publica ni se da a entender la secuencia en que se atienden: el código toma una propuesta y una decisión cada vez.
  • La autenticación de la persona revisora. Aquí reviewer es una cadena; en un sistema real es una identidad autenticada, comprobada donde se recibe la decisión.
  • La protección frente a un desarrollador que construya VerifiedValue a mano. El tipo hace visible una promoción accidental, el contrato de importación la hace imposible desde el módulo de inferencia, y la revisión de código cubre el resto.

Cómo verificarlo

Una propuesta rechazada devuelve None; una aceptada conserva su procedencia; un nombre vacío de persona revisora se rechaza (tests/test_boundary.py).

Modo de fallo que aborda

Un indicador verified en el objeto que produjo el modelo, verdadero por defecto o fijado por cualquier código que guarde el registro. Los valores inferidos y los verificados pasan a ser lo mismo en el almacenamiento, y nada aguas abajo puede distinguirlos.

Ejemplo de referencia ilustrativoLa frontera como regla que el build hace cumplir
Propósito
Convertir la frontera, de un recuadro en un diagrama, en una comprobación que hace fallar el build.
Propiedad de arquitectura
El módulo probabilístico no tiene ningún camino de importación hacia los registros autorizados ni hacia la traza de auditoría, y el código de dominio no tiene ninguno hacia los adaptadores de proveedores ni hacia los clientes HTTP (P-1, P-7). La regla se ejecuta en cada cambio (P-6).
pyproject.tomltomlP-1 · P-6 · P-7
# Illustrative reference example: architecture rules as data, run by `lint-imports`. [tool.importlinter]root_package = "refapp"include_external_packages = true [[tool.importlinter.contracts]]name = "The probabilistic side cannot reach authoritative records or the audit trail"type = "forbidden"source_modules = ["refapp.inference"]forbidden_modules = ["refapp.records", "refapp.audit", "refapp.api"] [[tool.importlinter.contracts]]name = "Domain code does not depend on vendor adapters or HTTP clients"type = "forbidden"source_modules = ["refapp.records", "refapp.formats", "refapp.classification"]forbidden_modules = ["refapp.adapters", "httpx"] [tool.pytest.ini_options]testpaths = ["tests"]pythonpath = ["."]

Lo que omite a propósito

  • Las importaciones hechas por nombre en tiempo de ejecución, como importlib.import_module con una cadena calculada. Un contrato de importación estático comprueba las sentencias de importación que puede leer; un cargador de plugins necesita una comprobación propia.
  • Los caminos de datos que no pasan por importaciones: una conexión de base de datos compartida, una cola a la que llegan ambos lados. De ellos se ocupan los permisos de la base de datos y la política de red del perfil Cloud-native.
  • Un contrato por capas para toda la aplicación. Dos contratos de prohibición bastan para mostrar el mecanismo.

Cómo verificarlo

lint-imports informa de que ambos contratos se mantienen. Añadir from refapp.records import accept a inference.py en una copia desechable: el primer contrato aparece como roto y el comando termina con un código distinto de cero. Al quitar la línea, vuelve a mantenerse.

Modo de fallo que aborda

La frontera existe como recuadro en un diagrama de arquitectura y en ningún otro sitio, hasta que alguien necesita con prisa un valor del otro lado.

Ejemplo de referencia ilustrativoPruebas negativas: un defecto plantado en cada una
Propósito
Verificar la frontera por lo que rechaza, no solo por lo que acepta.
Propiedad de arquitectura
Cada prueba planta un defecto que la frontera existe para detener, así que borrar una restricción pone una prueba en rojo.
tests/test_boundary.pypythonP-1 · P-6
"""The boundary refuses what it must refuse. Each test is one defect, planted.""" import jsonfrom datetime import UTC, datetime import pytestfrom pydantic import ValidationError from refapp.boundary import parse_model_replyfrom refapp.records import ReviewOutcome, accept SOURCE = "ab" * 32  # a well-formed SHA-256 hex digest, synthetic  def reply(**changes: object) -> str:    body: dict[str, object] = {        "attribute": "next_service_due",        "value": "2026-10-01",        "provenance": {            "method": "language-model",            "model": "example-model-2026-06",            "prompt_revision": "service-report-v3",            "source_digest": SOURCE,            "source_start": 118,            "source_end": 131,        },    }    body.update(changes)    return json.dumps(body)  def review(accepted: bool, reviewer: str = "j.doe") -> ReviewOutcome:    return ReviewOutcome(reviewer, accepted, "matches the report", datetime.now(UTC))  def test_a_well_formed_reply_parses() -> None:    assert parse_model_reply(reply()).provenance.method == "language-model"  @pytest.mark.parametrize(    "defect",    [        {"verified": True},  # the model claims more than it may        {"value": 20261001},  # a number where the contract says text        {"provenance": None},  # a bare value, with no provenance at all        {"attribute": "owner_name"},  # an attribute nobody asked for    ],)def test_the_boundary_rejects_it(defect: dict[str, object]) -> None:    with pytest.raises(ValidationError):        parse_model_reply(reply(**defect))  def test_a_rejected_proposal_leaves_the_attribute_empty() -> None:    assert accept(parse_model_reply(reply()), review(accepted=False)) is None  def test_verification_keeps_the_provenance() -> None:    verified = accept(parse_model_reply(reply()), review(accepted=True))    assert verified is not None and verified.proposal.provenance.source_digest == SOURCE  def test_a_value_that_is_not_a_calendar_date_never_verifies() -> None:    with pytest.raises(ValueError):        accept(parse_model_reply(reply(value="2026-02-30")), review(accepted=True))  def test_nobody_is_not_a_reviewer() -> None:    with pytest.raises(ValueError):        accept(parse_model_reply(reply()), review(accepted=True, reviewer=" "))

Lo que omite a propósito

  • Entrada malformada generada. Una lista corta de defectos con nombre es más fácil de revisar, y cada uno documenta un modo de fallo.
  • Nada sobre la calidad de las respuestas del modelo. La frontera no hace que un modelo acierte.

Cómo verificarlo

Ejecutar pytest. La suite tiene dientes solo si al quitar una restricción falla una prueba; el harness borra extra="forbid" en una copia desechable y la prueba del campo no declarado falla.

Modo de fallo que aborda

Una suite que solo recorre el camino feliz: se podrían borrar todas las restricciones y seguiría en verde.

Ejemplo de referencia ilustrativoEl modelo de respuesta forma parte del contrato
Propósito
La superficie HTTP de la frontera: qué puede enviar un worker de inferencia y qué recibe la pantalla de la persona revisora.
Propiedad de arquitectura
FastAPI valida la petición contra el modelo y filtra la respuesta según el modelo de respuesta, así que la procedencia, incluida la ubicación del pasaje que la persona revisora necesita para ver el valor junto a su fuente, tiene que declararse o nunca sale del servicio.
refapp/api.pypythonP-1 · P-5
"""The HTTP surface of the boundary. The response model is part of thecontract: a field it does not declare never leaves the service.""" from fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModel from refapp.boundary import Derivation, Proposal app = FastAPI()_pending: dict[str, list[Proposal]] = {}  # stand-in for real storage  class ProposalOut(BaseModel):    attribute: str    value: str    provenance: Derivation  # delete this line and every response loses it, silently  @app.post("/reports/{report_id}/proposals", status_code=201)def add_proposal(report_id: str, proposal: Proposal) -> None:    """The inference worker posts here. An undeclared field, say "verified", is a 422."""    _pending.setdefault(report_id, []).append(proposal)  @app.get("/reports/{report_id}/proposals", response_model=list[ProposalOut])def list_proposals(report_id: str) -> list[Proposal]:    """What a reviewer's screen needs: each value with the passage it came from."""    if report_id not in _pending:        raise HTTPException(status_code=404)    return _pending[report_id]

Lo que omite a propósito

  • La autenticación y la autorización de ambos endpoints.
  • La persistencia. Un diccionario hace las veces de almacenamiento.
  • El endpoint de decisión con el que la persona revisora acepta o rechaza; véase records.accept.
  • Ningún orden de presentación. La lista vuelve en el orden de inserción. Esa no es la secuencia de ninguna revisión: COADF no publica en qué secuencia se hace la revisión, y aquí nada la fija.

Cómo verificarlo

Enviar una respuesta con un campo verified de más: 422. Enviar una bien formada y listarla: la procedencia está en la respuesta. Después, declarar un modelo de respuesta sin la procedencia: el mismo objeto vuelve sin ella, y nada falla (tests/test_api.py lo reproduce).

Modo de fallo que aborda

Procedencia perdida en la serialización. Un modelo de respuesta que omite la procedencia la descarta en silencio, y la pantalla de revisión muestra un valor sin nada con lo que contrastarlo.

P-4 · La traza de ejecución y la traza de auditoría

Ejemplo de referencia ilustrativoContexto de ejecución e identidad de auditoría, uno junto al otro
Propósito
Llevar el contexto de ejecución a través de un mensaje y de un hilo, y llevar la identidad de auditoría a su lado, nunca en su lugar.
Propiedad de arquitectura
Una sola identidad trazable une los pasos de una transacción (P-4). El contexto de OpenTelemetry se correlaciona con ella mediante un atributo de span, y nunca la sustituye.
refapp/tracing.pypythonP-4
"""Two identifiers travel together and are never confused. The OpenTelemetry context describes one execution: a request, a message, ajob. The audit trace_id names one transaction across all of them, for as longas its records exist. They are correlated, never substituted for each other. Correlation copies the audit trace_id into telemetry, which has its ownexporters, vendors, access rules and retention. Keep it an opaque reference:never personal data, never a secret, never something that means more outside.""" import asyncioimport contextvarsfrom collections.abc import Callablefrom concurrent.futures import Executor, Futurefrom typing import Any from opentelemetry import propagate, trace tracer = trace.get_tracer("refapp")audit_trace_id: contextvars.ContextVar[str] = contextvars.ContextVar("audit_trace_id")  def publish(send: Callable[..., None], body: dict[str, Any]) -> None:    """Producer side: execution context in headers, audit identity in the body."""    headers: dict[str, str] = {}    propagate.inject(headers)  # W3C traceparent with the default propagators    send(headers=headers, body=body)  def consume(    headers: dict[str, str], body: dict[str, Any], work: Callable[[], None]) -> None:    """Consumer side: continue the execution, and require the audit identity."""    ctx = propagate.extract(headers)    token = audit_trace_id.set(body["trace_id"])  # a KeyError, never a freshly minted id    try:        with tracer.start_as_current_span("process-report", context=ctx) as span:            span.set_attribute("refapp.audit.trace_id", body["trace_id"])  # opaque reference            work()    finally:        audit_trace_id.reset(token)  async def run_blocking(fn: Callable[..., Any], *args: Any) -> Any:    return await asyncio.to_thread(fn, *args)  # documented to carry the context along  def submit_with_context(pool: Executor, fn: Callable[..., Any], *args: Any) -> Future[Any]:    ctx = contextvars.copy_context()  # pool.submit(fn) alone does not carry the caller's    return pool.submit(ctx.run, fn, *args)

Lo que omite a propósito

  • Exportadores y muestreadores; véase la configuración del Collector en el perfil Cloud-native.
  • Un cliente de broker real. send es cualquier función que reciba cabeceras y un cuerpo.
  • Dónde se acuña el trace_id de auditoría: en la recepción, una sola vez, y en ningún punto posterior.
  • Baggage. La identidad de auditoría viaja en el cuerpo del mensaje, donde forma parte del contrato, y no en el baggage, que la instrumentación transmite a los servicios posteriores, terceros incluidos.
  • Lo que expone la correlación. El atributo de span copia el trace_id de auditoría en la telemetría, que tiene sus propios exportadores, proveedores, controles de acceso y retención, normalmente más laxos que los del almacén de auditoría. Hay que clasificarlo antes de exportarlo: una referencia opaca, nunca datos personales ni un secreto. Donde un identificador de auditoría tenga significado, la correlación debe hacerse en su lugar mediante una referencia aparte y no sensible.

Cómo verificarlo

Los spans del productor y del consumidor comparten una traza distribuida, y el span del consumidor lleva la identidad de auditoría. Un mensaje sin la identidad de auditoría lanza KeyError en lugar de acuñar una nueva. Un envío directo a un pool de hilos pierde el contexto; el contexto copiado lo conserva; asyncio.to_thread lo conserva (tests/test_tracing.py).

Modo de fallo que aborda

El identificador de traza regenerado a mitad de camino: un consumidor que recurre a un identificador nuevo cuando falta el campo, o un hilo de trabajo que arranca sin el contexto de quien lo llama, deja dos mitades que ninguna consulta puede unir.

Ejemplo de referencia ilustrativoUna traza de auditoría de solo inserción en PostgreSQL
Propósito
Una traza de auditoría con los nueve campos que publica COADF, almacenada de modo que la aplicación pueda añadir y leer y la base de datos rechace las reescrituras.
Propiedad de arquitectura
La traza es de solo inserción y una corrección es una entrada nueva (P-4). Un paso tiene una sola identidad: un reintento exacto es la misma entrada, y audit.py rechaza la misma identidad con otro contenido.
schema.sqlsqlP-4
-- Illustrative reference example: an append-only audit trail in PostgreSQL.-- The nine columns are the fields COADF publishes for its audit trail schema.-- The vocabulary of event_type belongs to the application and is deliberately-- outside this example: the lesson here is storage that refuses rewrites. CREATE TABLE audit_entries (    trace_id    text        NOT NULL,    "timestamp" timestamptz NOT NULL,  -- when the step happened, not when the row arrived    event_type  text        NOT NULL,    actor       jsonb       NOT NULL,  -- {"kind": "system" | "person" | "agent", "id": …}    input       jsonb       NOT NULL,  -- what the step was about: a reference, not a copy    output      jsonb,    decision    jsonb,    source_hash text,    immutable   boolean     NOT NULL DEFAULT true CHECK (immutable),    -- The identity of one step. A retry must match the stored entry exactly;    -- refapp/audit.py refuses a second entry with this identity and other content.    UNIQUE (trace_id, "timestamp", event_type, input)); -- The application connects as a role that can add and read, nothing else.CREATE ROLE trail_appender NOLOGIN;GRANT INSERT, SELECT ON audit_entries TO trail_appender; -- Defence in depth for sessions that do hold broader rights.CREATE FUNCTION audit_entries_refuse() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN    RAISE EXCEPTION 'audit_entries is append-only: % refused', TG_OP;END $$; CREATE TRIGGER audit_entries_no_rewrite    BEFORE UPDATE OR DELETE ON audit_entries    FOR EACH ROW EXECUTE FUNCTION audit_entries_refuse(); -- TRUNCATE fires no DELETE trigger, so it needs a trigger of its own.CREATE TRIGGER audit_entries_no_truncate    BEFORE TRUNCATE ON audit_entries    FOR EACH STATEMENT EXECUTE FUNCTION audit_entries_refuse();

Lo que omite a propósito

  • Evidencia de manipulación. Los privilegios y los triggers son controles dentro de la propia frontera de confianza de la base de datos: un superusuario se salta las comprobaciones de permisos, y el propietario de la tabla puede desactivar sus triggers. Detectar una reescritura hecha por alguien con esos derechos requiere un control fuera de la base de datos, que este ejemplo no muestra.
  • Retención, archivado y exportación.
  • El vocabulario de eventos. event_type es texto obligatorio. Qué eventos registra una aplicación es cuestión de su propio diseño, y queda deliberadamente fuera de este ejemplo.
  • Un orden entre entradas con la misma marca de tiempo. La reconstrucción ordena por marca de tiempo, y este ejemplo deja intencionadamente sin ordenar las marcas de tiempo iguales: ningún campo publicado las ordena. Un único escritor por transacción, o un orden asignado por uno, es una decisión de almacenamiento que va más allá de los campos publicados.
  • La migración y el rol propietario de la tabla. La aplicación nunca se conecta con ese rol.

Cómo verificarlo

Contra un PostgreSQL real: el rol de la aplicación no puede actualizar, borrar ni truncar; el propietario se topa con los triggers; un reintento exacto no añade nada; la misma identidad con otro contenido se rechaza; una corrección añade una entrada; y, deliberadamente, el propietario puede desactivar los triggers y borrar (tests/test_audit.py).

Modo de fallo que aborda

Una corrección escrita como UPDATE, que destruye el registro de lo que se creía antes, y la misma pérdida con TRUNCATE, que no dispara ningún trigger DELETE.

Ejemplo de referencia ilustrativoInserción idempotente que rechaza un conflicto, reconstrucción en una consulta
Propósito
Hacer inofensivo un reintento y ruidosa una escritura en conflicto, y leer una transacción completa en una sola consulta.
Propiedad de arquitectura
La marca de tiempo se pone cuando se ejecuta el paso, así que un reintento reenvía la entrada idéntica. Ante un conflicto, primero se compara la entrada almacenada: el mismo contenido es un reintento, otro contenido lanza IdempotencyCollision. La reconstrucción es una consulta por trace_id, ordenada por marca de tiempo; las marcas de tiempo iguales quedan intencionadamente sin ordenar (P-4).
refapp/audit.pypythonP-4
"""Append to the audit trail, and read one transaction back. The timestamp is taken when the step runs and travels with the entry, so aretry resends the identical row. The unique key makes that retry harmless; itmust not make a different entry disappear, so a conflict is compared withwhat is stored before it is called a retry.""" from dataclasses import dataclassfrom datetime import datetimefrom typing import Any from psycopg import Connectionfrom psycopg.types.json import Jsonb _INSERT = """    INSERT INTO audit_entries (trace_id, "timestamp", event_type, actor, input,                             output, decision, source_hash, immutable)    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, true)    ON CONFLICT (trace_id, "timestamp", event_type, input) DO NOTHING"""_STORED = """    SELECT actor, output, decision, source_hash FROM audit_entries    WHERE trace_id = %s AND "timestamp" = %s AND event_type = %s AND input = %s"""  @dataclass(frozen=True)class AuditEntry:    trace_id: str    timestamp: datetime    event_type: str    actor: dict[str, Any]    input: dict[str, Any]    output: dict[str, Any] | None = None    decision: dict[str, Any] | None = None    source_hash: str | None = None  class IdempotencyCollision(Exception):    """Same step identity, different content: never a retry, never silent."""  def _json(value: dict[str, Any] | None) -> Jsonb | None:    return None if value is None else Jsonb(value)  def append(conn: Connection, entry: AuditEntry) -> bool:    """True if written; False if this exact entry is already there."""    identity = (entry.trace_id, entry.timestamp, entry.event_type, Jsonb(entry.input))    content = (entry.actor, entry.output, entry.decision, entry.source_hash)    with conn.cursor() as cur:        cur.execute(_INSERT, (entry.trace_id, entry.timestamp, entry.event_type,                              Jsonb(entry.actor), Jsonb(entry.input), _json(entry.output),                              _json(entry.decision), entry.source_hash))        if cur.rowcount == 1:            return True        cur.execute(_STORED, identity)        if cur.fetchone() != content:            raise IdempotencyCollision(f"{entry.trace_id}: {entry.event_type} differs")    return False  def chain(conn: Connection, trace_id: str) -> list[tuple[Any, ...]]:    """Every entry of one transaction, by timestamp. Entries with equal timestamps    are intentionally not ordered by this example: no published field orders them."""    with conn.cursor() as cur:        cur.execute('SELECT * FROM audit_entries WHERE trace_id = %s ORDER BY "timestamp"',                    (trace_id,))        return cur.fetchall()

Lo que omite a propósito

  • La transacción que escribe la entrada junto con el cambio de estado que registra. Escritos por separado, uno puede existir sin el otro.
  • La exportación como JSON, que P-4 también pide.
  • La gestión de conexiones, y qué rol usa la conexión.
  • Cuál de dos entradas en conflicto es la correcta. El ejemplo rechaza la segunda y conserva la primera; decidir entre ellas es tarea de alguien, no de la base de datos.
  • Escritores concurrentes del mismo paso. El ejemplo se probó con un escritor cada vez; con concurrencia, hay que comprobar qué deja ver el nivel de aislamiento a la lectura que compara.

Cómo verificarlo

Insertar la misma entrada dos veces: True, luego False, y una sola fila. Insertar la misma identidad con otra salida: IdempotencyCollision, y la entrada almacenada no cambia (tests/test_audit.py). Reconstruir con chain().

Modo de fallo que aborda

Reintentos que producen eventos ambiguos y conflictos que desaparecen: una marca de tiempo tomada en el momento de la inserción convierte cada reintento en una entrada nueva, y un ON CONFLICT DO NOTHING a secas descarta sin error una entrada distinta que por casualidad comparte la clave.

Ejemplo de referencia ilustrativoVerificar la persistencia, no los logs
Propósito
Comprobar la propiedad de solo inserción contra la capa que realmente la aplica, y fijar la limitación como prueba.
Propiedad de arquitectura
La propiedad se afirma donde se aplica, en la base de datos; lo que los controles no detienen también es una prueba, para que no pueda olvidarse.
tests/test_audit.pypythonP-4 · P-6
"""Append-only, checked against a real PostgreSQL. Skipped without one. Set REFAPP_PG_DSN to a database this test may create a table in. The eventtypes are synthetic. The last test is deliberate: it shows what the controls doNOT stop.""" import osimport pathlibfrom datetime import UTC, datetime import psycopgimport pytestfrom psycopg import errors from refapp.audit import AuditEntry, IdempotencyCollision, append, chain DSN = os.environ.get("REFAPP_PG_DSN")pytestmark = pytest.mark.skipif(not DSN, reason="REFAPP_PG_DSN is not set")SCHEMA = (pathlib.Path(__file__).parent.parent / "schema.sql").read_text()  @pytest.fixturedef conn():    with psycopg.connect(DSN, autocommit=True) as owner:        owner.execute("DROP TABLE IF EXISTS audit_entries")        owner.execute("DROP FUNCTION IF EXISTS audit_entries_refuse")        owner.execute("DROP ROLE IF EXISTS trail_appender")        owner.execute(SCHEMA)        yield owner        owner.execute("DROP TABLE audit_entries")        owner.execute("DROP FUNCTION audit_entries_refuse")        owner.execute("DROP ROLE trail_appender")  def step(event_type: str, at: datetime, **fields: object) -> AuditEntry:    return AuditEntry(trace_id="tr-3", timestamp=at, event_type=event_type,                      actor={"kind": "system", "id": "intake-worker"},                      input={"report": "r-3"}, **fields)  def test_a_retry_of_the_same_step_is_the_same_entry(conn) -> None:    entry = step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC), source_hash="ab" * 32)    assert append(conn, entry) is True    assert append(conn, entry) is False  # the retry    assert len(chain(conn, "tr-3")) == 1  def test_same_identity_other_content_is_never_a_silent_retry(conn) -> None:    at = datetime(2026, 9, 1, 8, 0, tzinfo=UTC)    append(conn, step("decision", at, output={"next_service_due": "2026-10-01"}))    with pytest.raises(IdempotencyCollision):        append(conn, step("decision", at, output={"next_service_due": "2026-11-01"}))    [stored] = chain(conn, "tr-3")    assert stored[5] == {"next_service_due": "2026-10-01"}  # the first entry, unchanged  def test_a_correction_is_a_new_entry(conn) -> None:    first = datetime(2026, 9, 1, 8, 0, tzinfo=UTC)    append(conn, step("decision", first, output={"next_service_due": "2026-10-01"}))    corrected = {"event_type": "decision", "timestamp": first.isoformat()}    append(conn, AuditEntry(        trace_id="tr-3", timestamp=datetime(2026, 9, 2, 9, 30, tzinfo=UTC),        event_type="decision", actor={"kind": "person", "id": "j.doe"},        input={"attribute": "next_service_due", "corrects": corrected},        output={"next_service_due": "2026-11-01"},        decision={"action": "corrected", "reason": "the report was misread"}))    assert len(chain(conn, "tr-3")) == 2  # both stay; the later one supersedes  def test_the_application_role_cannot_rewrite_or_erase(conn) -> None:    append(conn, step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC)))    conn.execute("SET ROLE trail_appender")    try:        for statement in ("UPDATE audit_entries SET actor = '{}'", "DELETE FROM audit_entries",                          "TRUNCATE audit_entries"):            with pytest.raises(errors.InsufficientPrivilege):                conn.execute(statement)    finally:        conn.execute("RESET ROLE")  def test_even_the_owner_meets_the_triggers(conn) -> None:    append(conn, step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC)))    for statement in ("UPDATE audit_entries SET actor = '{}'", "DELETE FROM audit_entries",                      "TRUNCATE audit_entries"):        with pytest.raises(errors.RaiseException):            conn.execute(statement)  def test_what_the_controls_do_not_stop(conn) -> None:    """The owner can switch the triggers off. The app must never connect as owner."""    append(conn, step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC)))    conn.execute("ALTER TABLE audit_entries DISABLE TRIGGER USER")    conn.execute("DELETE FROM audit_entries")    assert chain(conn, "tr-3") == []

Lo que omite a propósito

  • Una base de datos en la integración continua. Las pruebas se omiten sin REFAPP_PG_DSN, y una prueba omitida no es una prueba superada: el job que las ejecuta tiene que proporcionar la base de datos, o informar de la comprobación como no ejecutada.

Cómo verificarlo

Ejecutar contra un PostgreSQL desechable: las seis pruebas pasan, incluido el reintento en conflicto que tiene que fallar y la prueba que demuestra que el propietario puede borrar.

Modo de fallo que aborda

Falsa inmutabilidad: un trigger tomado como prueba de que la historia no puede cambiar, cuando el rol propietario de la tabla puede desactivarlo.

P-7 · El puerto y el adaptador

Ejemplo de referencia ilustrativoUn puerto que pertenece al dominio
Propósito
El lado del dominio de un servicio de clasificación externo: un puerto y los tipos que hay detrás.
Propiedad de arquitectura
Los vocabularios externos quedan detrás de un puerto que pertenece al dominio. Un servicio que falla hace el registro menos informativo y nunca lo detiene, y el registro dice si el servicio no estaba disponible o si la integración está rota (P-7).
refapp/classification.pypythonP-7
"""A port for an external classification service, and the domain's use of it. The domain speaks only these types. It never sees the vendor's client, itsresponse shape or its identifiers, so a new vendor or a new transport is anadapter change. A new version of the scheme itself can change what a codemeans, and then the domain's mapping changes too.""" from dataclasses import dataclassfrom typing import Literal, Protocol  @dataclass(frozen=True)class Classification:    scheme: str  # which external scheme answered    version: str  # a code means something only within one version    code: str    licensed: bool  # licensed content is marked, so exports can keep it apart  class LookupUnavailable(Exception):    """Transient: a timeout, an outage, a server error. It may work later."""  class LookupRejected(Exception):    """Not transient: refused credentials, a bad request, an answer in a shape    nobody agreed to. Retrying does not help; somebody has to fix it."""  class ClassificationLookup(Protocol):    def lookup(self, component_class: str) -> Classification | None: ...  @dataclass(frozen=True)class Enrichment:    classification: Classification | None    status: Literal["found", "not-found", "unavailable", "rejected"]  def enrich(lookup: ClassificationLookup, component_class: str) -> Enrichment:    """A failing service makes the record less informative. It never stops the    record, and the status keeps an outage apart from a broken integration."""    try:        found = lookup.lookup(component_class)    except LookupUnavailable:        return Enrichment(None, "unavailable")    except LookupRejected:        return Enrichment(None, "rejected")    return Enrichment(found, "found" if found is not None else "not-found")

Lo que omite a propósito

  • Lo que una respuesta externa hace con la confianza. COADF P-7 dice que un servicio externo puede elevar la confianza de un atributo y nunca puede bloquear una salida; cómo funciona esa elevación no se publica. Aquí la respuesta se registra, con su estado, y nada más.
  • La caché, y durante cuánto tiempo permite una licencia conservar una respuesta externa.
  • La correspondencia entre los códigos externos y los conceptos propios del dominio, que en un sistema real son datos versionados. Un proveedor o un transporte nuevos se quedan en el adaptador; una versión nueva del propio sistema de clasificación puede cambiar lo que significan los códigos, y entonces esta correspondencia también cambia.
  • A quién se avisa. rejected no se cura reintentando, así que necesita una alerta que unavailable quizá no necesite.

Cómo verificarlo

Un doble de prueba satisface el puerto; una caída registra unavailable y una integración rota registra rejected (tests/test_classification.py). El contrato de importación mantiene httpx y los adaptadores fuera de este módulo.

Modo de fallo que aborda

Una caída remota que bloquea procesamiento no relacionado: el dominio llama al proveedor directamente, de forma síncrona y sin timeout, y un incidente del proveedor se convierte en un incidente propio.

Ejemplo de referencia ilustrativoLos nombres del proveedor se detienen en el adaptador
Propósito
El único módulo que conoce la forma HTTP del proveedor y los nombres del proveedor.
Propiedad de arquitectura
classId y restricted del proveedor se traducen a code y licensed del dominio, la versión del sistema de clasificación se fija en cada llamada, y los resultados HTTP del proveedor se convierten en tres resultados de dominio: no encontrado, no disponible (transitorio) y rechazado (la integración está mal). El vocabulario HTTP del proveedor no se convierte en el del dominio (P-7).
refapp/adapters/http_classification.pypythonP-7
"""The adapter: the only module that knows the vendor's HTTP shape.""" import httpx from refapp.classification import Classification, LookupRejected, LookupUnavailable  class HttpClassificationLookup:    def __init__(self, client: httpx.Client, scheme: str, version: str) -> None:        self._client = client  # base URL and timeout are set by whoever builds the client        self._scheme = scheme        self._version = version     def lookup(self, component_class: str) -> Classification | None:        try:            response = self._client.get(                f"/classes/{component_class}", params={"release": self._version}            )        except httpx.TransportError as exc:  # timeouts and connection failures            raise LookupUnavailable() from exc        if response.status_code == 404:            return None        if response.status_code == 429 or response.is_server_error:            raise LookupUnavailable()        if response.status_code != 200:  # our request or our credentials are wrong            raise LookupRejected(f"vendor refused the request: {response.status_code}")        return self._translate(response)     def _translate(self, response: httpx.Response) -> Classification:        """The vendor's names stop here, and so does any shape we did not agree to."""        try:            body = response.json()            code, restricted = body["classId"], body["restricted"]        except (ValueError, KeyError, TypeError) as exc:            raise LookupRejected("unexpected vendor response") from exc        if not isinstance(code, str) or not code or not isinstance(restricted, bool):            raise LookupRejected("unexpected vendor response")        return Classification(self._scheme, self._version, code, licensed=restricted)

Lo que omite a propósito

  • Reintentos, circuit breaking y límites de tasa.
  • La autenticación ante el proveedor.
  • Los propios términos de la licencia, y cualquier contenido con licencia: este ejemplo no contiene ninguno.
  • Una taxonomía más fina. Tres resultados bastan para distinguir una caída de una integración rota; un adaptador real puede necesitar más.

Cómo verificarlo

Con httpx.MockTransport: un 200 se traduce; un 404 es None; un timeout, un error de conexión, un 429 o un 5xx es LookupUnavailable; un 400, un 401, un restricted no booleano, un campo ausente o un cuerpo que no es JSON es LookupRejected; cada petición lleva la versión fijada.

Modo de fallo que aborda

El SDK del proveedor convertido en modelo de dominio: sus clases aparecen en las firmas del dominio, sus identificadores adquieren significado interno, y un cambio de licencia o de API obliga a reescribir código de dominio. O cada fallo notificado como el mismo unavailable, de modo que una credencial revocada parece una caída y nadie la arregla.

P-8 · Reglas como datos

Ejemplo de referencia ilustrativoMaterial de políticas, con su versión
Propósito
El material que lee una regla: qué funcionalidades puede activar cada entorno, y la versión de este material.
Propiedad de arquitectura
Las reglas son datos. Cambiar lo que está permitido cambia este archivo, no el motor ni la regla (P-8).
policy/data.jsonjsonP-8
{  "refapp": {    "revision": "2026-09-01.2",    "features": {      "staging": ["report-export", "beta-search"],      "production": ["report-export"]    }  }}

Lo que omite a propósito

  • Cómo se protege el material de políticas en su camino hacia el motor y cómo se comprueba al llegar. COADF no publica esa parte de P-8, y este ejemplo no muestra nada de ella.
  • Cómo se asignan las versiones. La propia documentación de bundles de OPA usa un hash de commit de Git como versión de ejemplo. Nada en este ejemplo impide que el material cambie bajo la misma versión; lo que lo impide es derivar la versión del commit o del contenido.

Cómo verificarlo

Permitir beta-search en production: fallan las pruebas que fijan la respuesta de producción, mientras la regla sigue intacta.

Modo de fallo que aborda

Reglas duplicadas y codificadas a mano en varios servicios, cada una cambiada a su propio ritmo, de modo que la misma petición se permite en un sitio y se rechaza en otro.

Ejemplo de referencia ilustrativoUna regla que responde con su versión
Propósito
Una regla que lee su material de los datos y responde con la versión que produjo la respuesta.
Propiedad de arquitectura
La decisión lleva la procedencia de la versión de la regla (P-8).
policy/feature_access.regoregoP-8
# Illustrative reference example: which features an environment may enable.# The rule reads policy material from data; changing what is allowed is a data# change, and the rule below stays exactly as it is.package refapp.feature_access default allow := false allow if input.feature in data.refapp.features[input.environment] # The answer carries the revision of the material that produced it.decision := {	"allow": allow,	"revision": data.refapp.revision,}

Lo que omite a propósito

  • Nada sobre confianza, revisión, publicación o autonomía. El tema es deliberadamente genérico.
  • Los bundles y los logs de decisión, que en OPA pueden llevar por sí mismos una versión del bundle. El campo revision explícito mantiene este ejemplo autocontenido.
  • Si una regla real debería rechazar por defecto. Aquí el valor por defecto es false; para cada regla real, esa decisión corresponde a su responsable.

Cómo verificarlo

opa test policy/ ejecuta seis pruebas, entre ellas una clave de entrada mal escrita que cae en el valor por defecto en lugar de producir un error, y una repetición de la misma entrada bajo la misma versión.

Modo de fallo que aborda

Una recarga de reglas que cambia el significado de decisiones históricas: sin la versión en la decisión, un informe calculado hoy no puede decir qué reglas produjeron las respuestas del mes pasado.

Ejemplo de referencia ilustrativoConservar la decisión con su versión, o no conservarla
Propósito
El lado de la aplicación: preguntar al motor a través de una interfaz pequeña, y rechazar cualquier respuesta que no nombre su versión.
Propiedad de arquitectura
Una decisión se conserva con la versión que la produjo, o no se conserva (P-8). La respuesta se analiza con el mismo rigor con que se conserva: allow tiene que ser un booleano y revision una cadena no vacía, porque en Python bool("false") es True. El motor está detrás de un adaptador, así que el registro de decisión conserva su forma si cambia el motor.
refapp/policy.pypythonP-8
"""Ask a policy engine, and keep the answer together with its revision. The application depends on this small interface, not on the engine. SwapOpen Policy Agent for rule tables or a decision service and only the adapterchanges; the decision record keeps its shape.""" from dataclasses import dataclassfrom typing import Any import httpx  @dataclass(frozen=True)class PolicyDecision:    allow: bool    revision: str  # which policy material produced this answer    decision_id: str | None  # present when the engine keeps decision logs  class PolicyUnavailable(Exception):    """No usable decision. What that means is the decision owner's call; for    this feature-access example the caller refuses the feature."""  class OpaFeatureAccess:    _PATH = "/v1/data/refapp/feature_access/decision"     def __init__(self, client: httpx.Client) -> None:        self._client = client     def decide(self, environment: str, feature: str) -> PolicyDecision:        try:            response = self._client.post(                self._PATH, json={"input": {"environment": environment, "feature": feature}}            )            response.raise_for_status()            body = response.json()        except (httpx.HTTPError, ValueError) as exc:  # unreachable, refused, not JSON            raise PolicyUnavailable("no answer from the policy engine") from exc        return _decision(body)  def _decision(body: Any) -> PolicyDecision:    """Types are checked, never coerced: bool("false") is True."""    result = body.get("result") if isinstance(body, dict) else None    if result is None:  # OPA omits "result" when the path is undefined        raise PolicyUnavailable("undefined: no decision, which is not a denial")    if not isinstance(result, dict):        raise PolicyUnavailable("a result in an unexpected shape")    allow, revision = result.get("allow"), result.get("revision")    decision_id = body.get("decision_id")    if not isinstance(allow, bool):        raise PolicyUnavailable("allow is not a boolean")    if not isinstance(revision, str) or not revision.strip():        raise PolicyUnavailable("no decision that names its revision")    if decision_id is not None and not isinstance(decision_id, str):        raise PolicyUnavailable("decision_id is not a string")    return PolicyDecision(allow, revision, decision_id)

Lo que omite a propósito

  • La caché de decisiones. Una caché tiene que usar como clave la versión además de la entrada.
  • Dónde se registra la decisión: en la traza de auditoría, como salida de un paso de política.
  • El rechazo como regla universal. Rechazar es lo correcto para este ejemplo; el responsable de cada decisión decide qué significa para ella un motor no disponible.

Cómo verificarlo

Contra un servidor OPA real, la versión vuelve y la misma entrada responde del mismo modo. Contra un motor simulado, una denegación válida es una decisión; allow enviado como la cadena "false", un allow ausente, una versión vacía o en blanco, un resultado del tipo equivocado, un decision_id que no es una cadena, una ruta no definida (OPA omite result), un cuerpo que no es JSON y un motor inalcanzable lanzan cada uno PolicyUnavailable (tests/test_policy.py).

Modo de fallo que aborda

Una ruta de política no definida leída como rechazo, una respuesta malformada convertida a la fuerza en la decisión contraria, o una respuesta sin versión registrada de todos modos: la decisión existe y no puede reconstruirse.

P-6 · Un fence y su prueba de dientes

Ejemplo de referencia ilustrativoUn fence de publicación cuya lista nunca se publica
Propósito
Poner un fence a la salida construida, con una lista de patrones que queda fuera del árbol publicado.
Propiedad de arquitectura
Un estado prohibido en la salida construida detiene la publicación (P-6). No haber escaneado nada no cuenta como éxito, y un hallazgo dice dónde, no qué.
refapp/fence.pypythonP-6
"""A publication fence over a built site. The patterns it matches are read from a file kept outside the build output:a list of what is watched is a map of what is protected, so the list itselfis never published and a finding reports where, not what.""" import pathlibimport reimport sys SCANNED_SUFFIXES = {".html", ".txt", ".json", ".js", ".xml", ".svg"}  def load_patterns(pattern_file: pathlib.Path) -> list[re.Pattern[str]]:    lines = pattern_file.read_text(encoding="utf-8").splitlines()    kept = [line for line in lines if line and not line.startswith("#")]    return [re.compile(line, re.IGNORECASE) for line in kept]  def scan(build_dir: pathlib.Path, patterns: list[re.Pattern[str]]) -> tuple[int, list[str]]:    scanned, findings = 0, []    for path in sorted(build_dir.rglob("*")):        if not path.is_file() or path.suffix not in SCANNED_SUFFIXES:            continue        scanned += 1        text = path.read_text(encoding="utf-8", errors="replace")        for number, line in enumerate(text.splitlines(), start=1):            if any(p.search(line) for p in patterns):                findings.append(f"{path.relative_to(build_dir)}:{number}")    return scanned, findings  def main(build_dir: str, pattern_file: str) -> int:    build = pathlib.Path(build_dir).resolve()    patterns_path = pathlib.Path(pattern_file).resolve()    if build in patterns_path.parents:        print("fence: the pattern file is inside the build output", file=sys.stderr)        return 2    patterns = load_patterns(patterns_path)    scanned, findings = scan(build, patterns)    if not patterns or scanned == 0:  # nothing checked is not the same as nothing found        print(f"fence: {len(patterns)} patterns, {scanned} files: refusing", file=sys.stderr)        return 2    for finding in findings:        print(f"fence: match at {finding}", file=sys.stderr)    print(f"fence: {scanned} files, {len(findings)} findings", file=sys.stderr)    return 1 if findings else 0  if __name__ == "__main__":    sys.exit(main(*sys.argv[1:3]))

Lo que omite a propósito

  • Los patrones. El ejemplo incluye solo un centinela sintético. Una lista real de patrones es un mapa de lo que se protege, y por eso COADF P-6 no publica la suya.
  • La revisión semántica. Un fence de texto no puede ver un mecanismo expresado mediante flujo de control, identificadores renombrados o la geometría de un diagrama. Es necesario y no suficiente.
  • Formatos binarios, y contenido obtenido en tiempo de ejecución.

Cómo verificarlo

La prueba de dientes en tests/test_fence.py.

Modo de fallo que aborda

Un fence que pasa porque no leyó nada: un archivo de patrones vacío, un directorio de build sin archivos, o un archivo de patrones que ha acabado dentro de la salida del build.

Ejemplo de referencia ilustrativoPrueba de dientes
Propósito
Plantar un centinela sintético, ver fallar el punto de entrada real, restaurar los bytes exactos, verlo pasar.
Propiedad de arquitectura
Un fence se gana su sitio solo cuando se le ha visto fallar en el camino real (P-6).
tests/test_fence.pypythonP-6
"""Proof of teeth for the publication fence: plant a synthetic sentinel, watchthe real entry point fail, restore the exact bytes, watch it pass.""" import hashlib from refapp.fence import main # Not a real term of anything. It exists only to be caught.SENTINEL = "PLANTED-EXAMPLE-TOKEN-7F3Q"  def test_the_fence_has_teeth(tmp_path) -> None:    site = tmp_path / "site"    site.mkdir()    page = site / "index.html"    page.write_text("<main><p>Ordinary published copy.</p></main>", encoding="utf-8")    patterns = tmp_path / "patterns.txt"    patterns.write_text(SENTINEL + "\n", encoding="utf-8")    original = page.read_bytes()     assert main(str(site), str(patterns)) == 0     page.write_bytes(original + f"<p>{SENTINEL}</p>".encode())  # plant    assert main(str(site), str(patterns)) == 1     page.write_bytes(original)  # restore the exact bytes, and prove it    assert hashlib.sha256(page.read_bytes()).digest() == hashlib.sha256(original).digest()    assert main(str(site), str(patterns)) == 0  def test_nothing_scanned_is_not_a_pass(tmp_path) -> None:    (tmp_path / "empty-site").mkdir()    patterns = tmp_path / "patterns.txt"    patterns.write_text(SENTINEL + "\n", encoding="utf-8")    assert main(str(tmp_path / "empty-site"), str(patterns)) == 2  def test_a_pattern_file_inside_the_build_is_refused(tmp_path) -> None:    site = tmp_path / "site"    site.mkdir()    (site / "index.html").write_text("<p>copy</p>", encoding="utf-8")    leaked = site / "patterns.txt"    leaked.write_text(SENTINEL + "\n", encoding="utf-8")    assert main(str(site), str(leaked)) == 2

Lo que omite a propósito

  • Vocabulario protegido real. El centinela es sintético y no significa nada.
  • Cualquier afirmación de que un fence que pasa hace segura la salida. La prueba muestra que el camino del guardián funciona, no que la revisión semántica esté completa.

Cómo verificarlo

Ejecutarla. Después, hacer que scan retorne antes de tiempo en una copia desechable y verla fallar.

Modo de fallo que aborda

Una restauración que no es exacta. Restaurar de memoria, o desde la cabeza de una rama, puede cambiar o perder contenido en silencio; comparar el digest antes y después es lo que demuestra que el fixture volvió.

Modos de fallo

  1. Los valores por defecto de Pydantic tomados por una frontera

    Por defecto, un modelo ignora los campos que no declara y, fuera del modo estricto, convierte valores compatibles. Para una API eso es tolerancia; para una frontera significa que una respuesta que afirma "verified": true, o un número enviado como texto, pasa sin dejar rastro. Hay que fijar extra="forbid" y el modo estricto en los modelos de frontera, y probar ambas cosas.

  2. Un modelo de respuesta que descarta la procedencia

    FastAPI filtra cada respuesta según su modelo de respuesta. Un modelo de respuesta escrito para la pantalla, sin los campos de procedencia, los elimina de todas las respuestas, y la persona revisora ve un valor sin fuente.

  3. model_construct en una ruta crítica

    Construye un modelo sin validación. Introducido por velocidad, se convierte en la ruta que se salta la frontera.

  4. Un contexto que se detiene en un pool de hilos

    asyncio.to_thread propaga el contexto actual; un envío directo a un executor no lo hace, y tampoco run_in_executor, salvo que quien llama copie antes el contexto, como hace el propio to_thread. Los spans y la identidad de auditoría desaparecen entonces justo donde el trabajo es más lento.

  5. Una default factory que acuña identificadores de traza

    Field(default_factory=uuid4) en un modelo de mensaje parece inofensivo y convierte cada identificador que falta en uno nuevo y sin relación. Después de la recepción, la identidad de auditoría es obligatoria y nunca recibe un valor por defecto.

  6. Escrituras de auditoría aplazadas hasta después de la respuesta

    Las tareas en segundo plano de FastAPI se ejecutan después de devolver la respuesta. Una entrada de auditoría escrita ahí puede perderse con el proceso cuando al cliente ya se le ha dicho que el paso tuvo éxito. Hay que escribirla en la misma transacción que el cambio.

  7. Un solo modelo ORM para propuestas y registros

    Una columna de estado en la entidad que todos leen. El día que alguien olvida un filtro, las propuestas aparecen como hechos.

  8. Un protocolo tomado por una comprobación en tiempo de ejecución

    Un typing.Protocol es un contrato estático; una comprobación en tiempo de ejecución contra él solo constata que los métodos existen. El comportamiento es lo que comprueban las pruebas de contrato.

  9. Pruebas que se omiten, leídas como pruebas que pasan

    Una prueba de base de datos que se omite cuando falta la cadena de conexión convierte una base de datos ausente en la CI en una ejecución en verde. Hay que contar lo que se ejecutó y hacer fallar el job cuando las pruebas de auditoría no se ejecutaron.

Verificación

  • Prueba unitaria

    Pasa cuando: pytest ejecuta las pruebas negativas de la frontera: campo no declarado, tipo erróneo, procedencia ausente, atributo inesperado, fecha imposible, revisión rechazada.

    Prueba de dientes: Borrar extra="forbid" en una copia desechable: la prueba del campo no declarado falla. El arnés lo hace en cada ejecución.

  • Prueba de arquitectura

    Pasa cuando: lint-imports mantiene ambos contratos.

    Prueba de dientes: Introducir from refapp.records import accept en inference.py: el contrato se rompe y el comando termina con un código distinto de cero.

  • Prueba de contrato

    Pasa cuando: La API rechaza con 422 los campos no declarados, y sus respuestas llevan la procedencia.

    Prueba de dientes: Un modelo de respuesta sin el campo de procedencia devuelve el objeto sin él; la prueba que afirma su presencia falla.

  • Prueba de integración

    Pasa cuando: Contra un PostgreSQL real: el rol de la aplicación no puede reescribir la traza, el propietario se topa con los triggers, los reintentos exactos se absorben, un reintento en conflicto se rechaza y las correcciones se suman.

    Prueba de dientes: La última prueba de auditoría desactiva los triggers como propietario y borra, lo que demuestra lo que los controles no impiden.

  • Prueba de integración

    Pasa cuando: Contra un servidor OPA real, la decisión lleva su revisión de la política y se reproduce de forma idéntica; contra un motor simulado, se rechazan las respuestas sin revisión de la política o de tipo erróneo.

    Prueba de dientes: Convertir allow con bool() en lugar de comprobar su tipo: la prueba que envía la cadena "false" falla. El arnés lo hace en cada ejecución.

  • Prueba de extremo a extremo

    Pasa cuando: El fence de publicación pasa sobre la salida construida.

    Prueba de dientes: Se introduce un centinela sintético, el fence falla, se restauran los bytes exactos y se comparan, y el fence pasa.

Realizaciones alternativas

  • Otras bibliotecas de validación. attrs con cattrs, o msgspec, ofrecen la misma frontera con otros compromisos de velocidad y rigor. La propiedad es el tipo estricto, validado y portador de procedencia, no la biblioteca.
  • Otros frameworks. Los serializadores de Django REST framework o Litestar expresan la misma frontera. El filtrado de la salida se comporta distinto en cada framework; hay que probarlo en el propio.
  • Inmutabilidad a nivel de aplicación (un repositorio de solo inserción, sin método de actualización) en lugar de triggers de base de datos, cuando la base de datos es compartida o los triggers no pueden gestionarse. Más débil frente a un segundo cliente, y más sencilla de operar.
  • Tablas de reglas en PostgreSQL en lugar de OPA, cuando las reglas son pocas y el equipo ya es dueño de la base de datos. La exigencia de registrar la revisión de la política es la misma.

Compromisos

  • El rigor cuesta fricción. Los modelos estrictos rechazan entradas que los laxos habrían reparado, y cada rechazo exige una decisión. El modo estricto es además más permisivo con la entrada JSON que con los objetos Python: los tipos de fecha aceptan cadenas incluso en modo estricto.
  • Dos identificadores cuestan atención. Cada desarrollador tiene que saber cuál está mirando. Conviene nombrarlos de forma distinta en el código, como hacen los ejemplos, y no usar nunca uno para rellenar el otro.
  • La inserción exclusiva impuesta por la base de datos ata a las funciones de esa base de datos. Aquí, los triggers y los permisos son específicos de PostgreSQL; una segunda base de datos necesita su propio equivalente y sus propias pruebas.
  • Un motor de políticas detrás de HTTP añade un salto de red y un modo de fallo. El cliente tiene que decidir qué significa un motor inalcanzable, para cada decisión.

Limitaciones

  • Los ejemplos son ilustrativos, pequeños y sintéticos. Dejan fuera la autenticación, la autorización, las migraciones, el pool de conexiones, el acceso asíncrono a la base de datos y el despliegue.
  • Se probaron en el entorno indicado arriba, el 11 de septiembre de 2026, y en ningún otro.
  • El proceso único que se muestra es una topología. El perfil Cloud-native muestra qué cambia cuando el lado probabilístico se despliega por separado.
  • Nada de lo que aquí figura muestra cómo se representa la confianza, cuándo se requiere revisión ni cómo se organiza la revisión; COADF no publica esas partes de P-2 y P-3.

Lo que este perfil no establece

Seguir este perfil no establece cumplimiento normativo, certificación ni evaluación de la conformidad, y COADF no exige nada de lo que aquí figura. Ejecutar estos ejemplos establece que estos ejemplos se ejecutaron.

Entorno de referencia probado

  • Python 3.12.13 and 3.14.4 · cada prueba de los ejemplos se ejecutó en ambas
  • FastAPI 0.141.1 · con Starlette 1.6.0 y su cliente de pruebas
  • Pydantic 2.13.5
  • httpx 0.28.1 · incluido su transporte simulado
  • OpenTelemetry API and SDK 1.44.0 · exportador de spans en memoria
  • import-linter 2.15 · contratos cumplidos, y rotos por una importación introducida a propósito
  • psycopg 3.3.5
  • PostgreSQL 18.6 · un clúster local desechable; las pruebas de auditoría se ejecutan contra él
  • Open Policy Agent 1.20.2 · opa test, opa check --strict y un servidor en marcha para la prueba del cliente
  • pytest 9.1.1

Fuentes

COADF Engineering Companion 1.0 · no normativo · corresponde a COADF Core 2.2

Derechos de publicación reservados. Por ahora no se concede ninguna licencia pública para el COADF Engineering Companion 1.0 ni para sus ejemplos de referencia.

Estado de propiedad intelectual y de publicación