Saltar al contenido
disensor.devv0.14.0

Evidencia versionada para la revisión adversarial de código con IA

Tu IA escribió el código. Otra IA lo revisó.

Lo que pasó en esa revisión termina como un archivo JSON en tu repo, al lado del código que juzga. Ni un badge verde ni un puntaje de cobertura: qué hallazgos se verificaron, cuáles se refutaron, qué no se pudo ejecutar y qué sigue descansando sobre el juicio de alguien. disensor es el CLI y la GitHub Action que lo revisan contra el esquema, sus reglas y el cambio que el pull request realmente hace.

¿Primera vez acá? Mirá una ronda de punta a punta, sin instalar nada

Instalación

pip install disensor
  • init escribe cinco cosas en tu repo. Un archivo de config, un workflow de CI, una línea de .gitignore para el informe y, para Claude Code, una skill y una instrucción, así el registro se escribe al cerrar cada ronda. Cualquier otro agente recibe la misma guía con disensor guide.
  • El gate no corre ningún modelo ni pide claves de API. Valida lo que ya está en el repositorio. La ronda sí corre uno: disensor round ejecuta el revisor que tengas instalado. En los dos casos hace falta un árbitro humano; sin él el artefacto no valida.
  • Probalo sin tocar tu CI. init --no-workflow escribe todo menos el workflow, y el gate corre igual en tu máquina. Si nunca lo viste, cómo funciona, explicado despacio.

§ 01

Ese archivo se llama declaración de residuo. El residuo es lo que la revisión deja anotado para que una persona sepa dónde mirar: los hallazgos que quedaron sin decidir, los que el generador refutó, incluso con pruebas, lo que no se pudo comprobar ejecutando y los límites del propio revisor. La declaración lista eso en lugar de cobertura, y la página del método explica por qué no se parece en nada a un badge verde.

Guía de revisión adversarial de código Qué es el desacuerdo controlado

§ 02

Desacuerdo controlado

Dentro de un evento el ciclo es lineal: el revisor ataca una vez. disensor se queda afuera de ese ciclo. Define el artefacto con el que termina, y lo revisa.

01familia A

Genera

el plan o el diff

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

02otra familia

Ataca

uno o más revisores (R4)

Un modelo de otra familia lo revisa con una consigna adversarial. Dos modelos del mismo linaje fallan en los mismos lugares, y por eso la familia tiene que cambiar.

03familia A

Verifica

contra el repositorio o la ejecución

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.

04

Declara

cierra el ciclo, no el problema

El ciclo cierra cuando todo hallazgo llegó a un estado terminal. Lo que no cerró queda escrito y viaja versionado con el commit. Ese archivo lo escribe tu asistente, no vos.

escalated_open, refuted_interpretive

Árbitro humano

Requerido en todo evento. Sin él la declaración no cumple el protocolo.

R0
fix_verification: pending_in_diff_gateSi lo revisado fue el plan, la corrección todavía no existe y no hay dónde verificarla. Queda pendiente para el evento del diff, que tiene su propia declaración.

Ver correr una ronda, sobre un ejemplo

§ 03

El artefacto

Un JSON versionado en el repositorio, junto al código que juzga. Registra los actores, su familia y cuán independiente fue realmente el revisor, cada hallazgo con su estado terminal y las métricas del evento. Una ronda que no pudo llegar a otra familia de modelo queda registrada como el modo degradado que es, y arrastra su propio ítem de residuo.

Leé primero final_state: el estado terminal de cada hallazgo, uno de seis. En este evento real, los 19 hallazgos llegaron todos a uno, y aun así la ronda dejó 4 ítems de residuo. El extracto muestra dos hallazgos de una misma corrida del revisor: uno incorporado, con la prueba que verificó la corrección, y uno refutado con evidencia, que cierra y entra igual al residuo para que el árbitro pueda no darlo por bueno.

El gate no corre ningún modelo ni pide claves de API: valida un archivo que ya está versionado. Correr la revisión es un paso aparte, y ese sí le entrega tu diff a un modelo de otra familia; desde la 0.9 lo orquesta disensor round con el revisor que tengas instalado.

Los conteos cierran. El residuo, igual, tiene 4 ítems.
total_findings19
incorporated18
debt_recorded0
owner_decision0
refuted_verifiable1
refuted_interpretive0
escalated_open0
residue.items4
.residue/27089d19-ee91-4c6c-b517-63b9627969e0.jsonv0.14.0
{
  "schema": "residue/v0.4",
  "event": {
    "event_id": "27089d19-ee91-4c6c-b517-63b9627969e0",
    "created_at": "2026-09-29T10:21:11-03:00",
    "repository": "github.com/NicolasRocchia/disensor",
    "gate": "diff",
    "criticality_level": "B",
    "base_commit": "b340cfc30fc418957f2dfafc82c1e14bf8e87884",
    "head_commit": "c2970915df2501b5eb214e3272174bb03638f610"
  },
  "actors": {
    "generator": {
      "family": "anthropic",
      "model": "claude-fable-5-1"
    },
    "reviewers": [
      {
        "reviewer_id": "r8",
        "family": "openai",
        "model": "gpt-5.6-sol",
        "independence": "cross_family",
        "confinement": {
          "mode": "read_only_by_instruction",
          "verified": true,
          "verification_method": "clean_git_status"
        },
        "hardening": "verified",
        "prompt_hash": "sha256:3494df5ebd20cc793ff9c5a214a2e715758e69c41bbc9e09b70f7d3064ee6a1c"
      }
    ],
    "human_arbiter": {
      "present": true
    }
  },
  "findings": [
    {
      "id": "h15",
      "origin": "r8",
      "severity": "major",
      "title": "Una declaracion que el gate rechaza por sus propias comprobaciones daba por cubierto el merge",
      "description": "El recorrido validaba el esquema y aplicaba G5, G6 y G7, pero no las comprobaciones por artefacto del gate: una declaracion de nivel C en un repositorio de nivel B (G2), un id no canonico o un archivo con otro nombre (G8), o un nivel A sin confinamiento admitido (G4) cubrian igual. Las comprobaciones del gate se extrajeron a gate.judge_artifact, que _run_gate y el recorrido comparten; el recorrido no aplica G9 porque una version anterior del esquema era la vigente cuando un merge viejo ocurrio, y lo dice. Las declaraciones que el PR agrego se juzgan como en el gate, y una mutacion de evidencia o un historico ilegible tambien lo dejan sin cobertura, como al PR.",
      "location": "src/disensor/gate.py (judge_artifact); src/disensor/branch.py (_judge_merge, _historical_ids)",
      "final_state": "incorporated",
      "verification": {
        "against": "repository",
        "detail": "Comparadas las comprobaciones inline de _run_gate (G9, G8, G2, G3, G4, G5) con lo que aplicaba _judge_merge; la extraccion conserva el orden y los textos, y la suite del gate sigue en verde."
      },
      "fix_verification": {
        "type": "specific_test",
        "reference": "tests/test_branch.py::test_a_declaration_the_gate_rejects_on_its_own_checks_covers_nothing, que corre run_gate sobre el mismo PR y afirma que devuelve 1"
      }
    },
    {
      "id": "h16",
      "origin": "r8",
      "severity": "info",
      "title": "La resolucion de un merge con conflictos no entra en la cobertura",
      "description": "Hipotesis del revisor: el recorrido evalua el rango desde el merge base hasta el segundo padre y no el arbol del commit de merge, asi que codigo introducido al resolver un conflicto puede diferir del head revisado y el merge figurar cubierto.",
      "location": "src/disensor/branch.py (_judge_merge); src/disensor/gate.py (resolve_context, evaluate_coverage)",
      "final_state": "refuted_verifiable",
      "verification": {
        "against": "repository",
        "detail": "Es el contrato del gate, no del panel: resolve_context toma head_oid como head del PR y evaluate_coverage y check_range_membership juzgan ese head; el gate nunca ve el arbol del merge que la plataforma crea al integrar. El panel replica exactamente lo que el gate juzgo. Lo que el revisor senala es un limite del gate que existe desde G6, y no cambia con este PR."
      },
      "evidence": {
        "text": "gate.py, resolve_context: head_oid = gitctx.resolve_commit(head, repo_dir); status = gitctx.changed_status(mb, head_oid, repo_dir). El rango juzgado es merge base a head del PR."
      }
    }
  ],
  "residue": {
    "items": [
      {
        "id": "r4",
        "class": "principal_refutation",
        "finding_ref": "h16",
        "refutation_type": "verifiable",
        "requires_human_attention": false,
        "description": "El revisor sugirio que la cobertura mirara el arbol del commit de merge y no el head del PR; se refuto con el codigo del gate, que juzga el head. El limite que senala (lo que se escribe al resolver un conflicto no pasa por ninguna revision) es del gate, no de este panel, y queda anotado."
      }
    ]
  },
  "metrics": {
    "counts": {
      "total_findings": 19,
      "valid": {
        "incorporated": 18,
        "debt_recorded": 0,
        "owner_decision": 0
      },
      "false_positives": {
        "refuted_verifiable": 1,
        "refuted_interpretive": 0
      },
      "escalated_open": 0
    }
  }
}
Extracto de un evento real del repositorio de la herramienta, la declaración de su pull request #101: cada valor que se ve es el del archivo, en su orden, y el build lo comprueba. Las claves y los valores enumerados son el contrato; la prosa está en castellano porque así escriben sus autores. Ver el artefacto completo

§ 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. Cuando da verde escribe además un informe HTML de todas las declaraciones del repositorio, con lo que quedó abierto primero y sin red: el documento es lo último del evento.

Falla cerrado
Si el gate no puede resolver el rango del PR, no da verde: no pudo leer el cambio, así que no tiene nada que aprobar.
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

Lo que no detecta

El validador detecta el campo vacío y el marcador genérico. No detecta la declaración falsa, y ninguna herramienta puede. Alguien tiene que abrir PR ya mergeados al azar y leerlos.

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

Qué exige el artefacto y qué todavía no puede verificar cambia con cada versión del esquema. Los límites conocidos se declaran en la referencia.

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