Saltar al contenido
disensor.devv0.5.0

Artefacto · Validador · Gate de CI

Lo que la revisión
no cerró, queda escrito.

Disensor emite, valida y hace cumplir en CI la declaración de residuo: el registro de cómo terminó cada evento de revisión adversarial y, sobre todo, de lo que el ciclo no pudo cerrar por sí mismo.

Instalación

pip install disensor

Después, disensor init en la raíz del repo. No pide claves de API ni manda código a ningún servicio.

01

La declaración lista residuo, no cobertura.

Dirige el escrutinio del revisor humano en lugar de leerse como sello de calidad. Es lo contrario de un badge verde: no dice cuánto se revisó, dice qué quedó sobre la mesa y a nombre de quién.

02

Desacuerdo controlado

Dentro de un evento el ciclo es lineal: el revisor ataca una vez. La herramienta no lo orquesta — define y hace cumplir el artefacto con el que ese ciclo termina.

El ciclo de un evento de revisiónCuatro estaciones en línea: 01 Genera, por un modelo de familia A, produce el plan o el diff. 02 Ataca, por uno o más revisores de otra familia. 03 Verifica, de nuevo familia A, contra el repositorio o la ejecución. 04 Declara. Debajo, una banda: el árbitro humano, requerido en todo evento. Dos vías bajan hasta él desde la verificación: los hallazgos escalados sin decisión y las refutaciones interpretativas. Más abajo, y solo cuando el evento es de compuerta de plan, una corrección que quedó pendiente de verificar encadena con el evento siguiente, de compuerta de diff, que tiene su propia declaración.Un evento de revisión · el revisor ataca una vez01Generafamilia Ael plan o el diff02Atacaotra familiauno o más revisores (R4)03Verificafamilia Acontra el repositorio o la ejecución04Declaracierra el ciclo, no el problemaescalated_openrefuted_interpretivelos dos descansan en el juicio de alguienÁrbitro humanoRequerido en todo evento: sin él la declaración no cumple el protocoloR0fix_verification: pending_in_diff_gatesolo cuando el evento es de compuerta de planLa corrección se verifica en el evento siguiente, no en éste.01020304Evento siguiente · compuerta de diff · con su propia declaración
  1. 01

    Genera

    Un modelo produce el plan o el diff que se va a mergear.

  2. 02

    Ataca

    Un modelo de otra familia lo revisa con una consigna adversarial. La familia distinta es el punto: dos modelos del mismo linaje fallan en los mismos lugares.

  3. 03

    Verifica

    El generador contrasta cada hallazgo contra el repositorio o la ejecución y lo lleva a un estado terminal: incorporado, deuda registrada, decisión del dueño, refutado o escalado.

  4. 04

    Declara

    El ciclo cierra cuando todo hallazgo llegó a un estado terminal. Lo que no cerró se escribe, se versiona y viaja con el commit.

03

El artefacto

Un JSON versionado en el repositorio, junto al código que juzga. Registra los actores y su familia, cada hallazgo con su estado terminal y las métricas del evento.

El bloque que importa es residue. En este evento real, los 3 hallazgos cerraron —los números dan— y aun así dejaron 3 ítems de residuo, uno por cada clase. Residuo no es lo que quedó abierto: es lo que descansa sobre el juicio de alguien.

No corre modelos, no pide claves de API en CI y ningún código sale del repositorio: valida un archivo que ya está versionado.

spec/examples/example_2_diff_gate.jsonv0.5.0
  1. r1Escalado sin decisiónatención humana

    El limite de filas de la exportacion quedo escalado a producto y sin resolver al cierre del ciclo.

  2. r2Refutación del principal

    Refutacion con enlace al objeto de consulta compartido, para que el revisor humano pueda no darla por buena.

  3. r3Gap de ejecuciónatención humana

    El comportamiento bajo concurrencia real no se probo porque el entorno de desarrollo no la reproduce.

Los conteos cierran. El residuo, igual, tiene 3 ítems.
total_findings3
incorporated1
debt_recorded0
owner_decision0
refuted_verifiable1
refuted_interpretive0
escalated_open1
{
  "schema": "residue/v0.2",
  "profile": "full",
  "event": {
    "event_id": "7c8d9e0f-1a2b-4c3d-8e5f-6a7b8c9d0e1f",
    "created_at": "2026-07-15T18:40:00-03:00",
    "repository": "https://github.com/ejemplo/sistema-interno",
    "pr": "https://github.com/ejemplo/sistema-interno/pull/214",
    "base_commit": "c0ffee1",
    "head_commit": "beef042",
    "gate": "diff",
    "criticality_level": "B",
    "abbreviated_path": {
      "used": false
    }
  },
  "actors": {
    "generator": {
      "family": "anthropic",
      "model": "claude-code"
    },
    "reviewers": [
      {
        "reviewer_id": "r1",
        "family": "openai",
        "model": "gpt-5.4",
        "prompt_hash": "sha256:1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
        "confinement": {
          "mode": "read_only_by_instruction",
          "verified": true,
          "verification_method": "clean_git_status"
        }
      }
    ],
    "human_arbiter": {
      "present": true,
      "id": "dueno-feature-02"
    }
  },
  "findings": [
    {
      "id": "h1",
      "origin": "r1",
      "severity": "major",
      "title": "El flujo de escritura no se libera si la consulta falla a mitad de camino",
      "description": "En la exportacion, una falla intermedia de la consulta dejaba el flujo de escritura tomado.",
      "final_state": "incorporated",
      "verification": {
        "against": "repository",
        "detail": "Confirmado en el codigo del diff: faltaba la liberacion en el camino de error."
      },
      "fix_verification": {
        "type": "specific_test",
        "reference": "tests/Exportacion_FallaIntermedia_LiberaFlujo"
      }
    },
    {
      "id": "h2",
      "origin": "r1",
      "severity": "major",
      "title": "Limite de filas de la exportacion sin definir",
      "description": "El comportamiento ante rangos que superan el maximo permitido no esta definido por producto.",
      "final_state": "escalated_open",
      "verification": {
        "against": "repository",
        "detail": "El codigo no impone limite y el requerimiento no lo especifica: la decision no es tecnica."
      }
    },
    {
      "id": "h3",
      "origin": "r1",
      "severity": "minor",
      "title": "Validacion de fechas supuestamente faltante",
      "description": "El revisor reporto que la exportacion no valida el rango de fechas de entrada.",
      "final_state": "refuted_verifiable",
      "verification": {
        "against": "repository",
        "detail": "La validacion existe en el objeto de consulta compartido que este flujo reutiliza."
      },
      "evidence": {
        "text": "El objeto de consulta compartido valida el rango antes de ejecutar; la exportacion lo reutiliza sin excepcion.",
        "link": "src/consultas/RangoFechas.cs#L41-L58"
      }
    }
  ],
  "residue": {
    "items": [
      {
        "id": "r1",
        "class": "escalation_without_decision",
        "finding_ref": "h2",
        "requires_human_attention": true,
        "description": "El limite de filas de la exportacion quedo escalado a producto y sin resolver al cierre del ciclo."
      },
      {
        "id": "r2",
        "class": "principal_refutation",
        "finding_ref": "h3",
        "refutation_type": "verifiable",
        "requires_human_attention": false,
        "description": "Refutacion con enlace al objeto de consulta compartido, para que el revisor humano pueda no darla por buena.",
        "evidence": {
          "text": "La validacion vive en el objeto de consulta compartido reutilizado por la exportacion.",
          "link": "src/consultas/RangoFechas.cs#L41-L58"
        }
      },
      {
        "id": "r3",
        "class": "execution_gap",
        "requires_human_attention": true,
        "gap_reason": "environment_not_reproducible",
        "description": "El comportamiento bajo concurrencia real no se probo porque el entorno de desarrollo no la reproduce."
      }
    ]
  },
  "metrics": {
    "counts": {
      "total_findings": 3,
      "valid": {
        "incorporated": 1,
        "debt_recorded": 0,
        "owner_decision": 0
      },
      "false_positives": {
        "refuted_verifiable": 1,
        "refuted_interpretive": 0
      },
      "escalated_open": 1
    },
    "extra_time_sec": 2400
  }
}
El artefacto completo, bajado del tag publicado en tiempo de build · ver en el repositorio

04

Qué hace cumplir el gate

Una GitHub Action que valida las declaraciones que el PR agrega, aplica la política de alcance del repositorio y publica el resultado como comentario.

Falla cerrado
Si el gate no puede resolver el rango del PR, no da verde. Un control de cumplimiento que no puede decidir, no aprueba.
La evidencia es de solo agregar
Un PR no puede modificar, borrar ni renombrar declaraciones que ya estaban, ni reutilizar un identificador de evento.
Una declaración rancia no cubre nada
Si la ruta cambió después del commit revisado, la declaración deja de cubrirla. Revisar y seguir escribiendo no alcanza.
Todo sale de git
El alcance se deriva de los objetos del rango revisado, nunca del working tree: leer del disco clasificaría un árbol y validaría otro.

05

El límite, dicho por adelantado

El validador detecta el campo vacío y el marcador genérico. No detecta la declaración falsa, y ninguna herramienta puede. El muestreo humano de PR cerrados sigue siendo la única defensa real contra el cumplimiento cosmético.

Tampoco protege contra un workflow modificado, salteado o sustituido: eso lo resuelve la plataforma, con required checks estrictos, CODEOWNERS y el pin de la Action por SHA.

Probalo sin tocar tu CI

El gate corre igual en tu máquina y dice exactamente lo mismo que diría en CI. Recién cuando quieras que haga cumplir, se escribe el workflow.

pip install disensor && disensor init --no-workflow