Genera
el plan o el diff
Un modelo produce el plan o el diff que se va a mergear.
Evidencia versionada para la revisión adversarial de código con IA
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 disensordisensor guide.disensor round ejecuta el revisor que tengas instalado. En los dos casos hace falta un árbitro humano; sin él el artefacto no valida.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
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.
el plan o el diff
Un modelo produce el plan o el diff que se va a mergear.
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.
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.
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
Requerido en todo evento. Sin él la declaración no cumple el protocolo.
§ 03
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.
| total_findings | 19 |
|---|---|
| incorporated | 18 |
| debt_recorded | 0 |
| owner_decision | 0 |
| refuted_verifiable | 1 |
| refuted_interpretive | 0 |
| escalated_open | 0 |
| residue.items | 4 |
{
"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
}
}
}§ 04
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.
§ 05
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.
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